diff --git a/src/Containers.jsx b/src/Containers.jsx index e145814a3..b18f40675 100644 --- a/src/Containers.jsx +++ b/src/Containers.jsx @@ -1,9 +1,9 @@ -import React, { useState } from 'react'; +import React from 'react'; import { Badge } from "@patternfly/react-core/dist/esm/components/Badge"; import { Button } from "@patternfly/react-core/dist/esm/components/Button"; import { Card, CardBody, CardHeader, CardTitle } from "@patternfly/react-core/dist/esm/components/Card"; import { Divider } from "@patternfly/react-core/dist/esm/components/Divider"; -import { Dropdown, DropdownItem, DropdownSeparator, KebabToggle } from '@patternfly/react-core/dist/esm/deprecated/components/Dropdown/index.js'; +import { DropdownItem } from '@patternfly/react-core/dist/esm/components/Dropdown/index.js'; import { Flex } from "@patternfly/react-core/dist/esm/layouts/Flex"; import { Popover } from "@patternfly/react-core/dist/esm/components/Popover"; import { LabelGroup } from "@patternfly/react-core/dist/esm/components/Label"; @@ -39,18 +39,17 @@ import { PodActions } from './PodActions.jsx'; import { PodCreateModal } from './PodCreateModal.jsx'; import PruneUnusedContainersModal from './PruneUnusedContainersModal.jsx'; +import { KebabDropdown } from "cockpit-components-dropdown.jsx"; + const _ = cockpit.gettext; const ContainerActions = ({ container, healthcheck, onAddNotification, localImages, updateContainer }) => { const Dialogs = useDialogs(); const { version } = utils.usePodmanInfo(); - const [isActionsKebabOpen, setActionsKebabOpen] = useState(false); const isRunning = container.State.Status == "running"; const isPaused = container.State.Status === "paused"; const deleteContainer = (event) => { - setActionsKebabOpen(false); - if (container.State.Status == "running") { const handleForceRemoveContainer = () => { const id = container ? container.Id : ""; @@ -78,8 +77,6 @@ const ContainerActions = ({ container, healthcheck, onAddNotification, localImag const stopContainer = (force) => { const args = {}; - setActionsKebabOpen(false); - if (force) args.t = 0; client.postContainer(container.isSystem, "stop", container.Id, args) @@ -90,8 +87,6 @@ const ContainerActions = ({ container, healthcheck, onAddNotification, localImag }; const startContainer = () => { - setActionsKebabOpen(false); - client.postContainer(container.isSystem, "start", container.Id, {}) .catch(ex => { const error = cockpit.format(_("Failed to start container $0"), container.Name); // not-covered: OS error @@ -100,8 +95,6 @@ const ContainerActions = ({ container, healthcheck, onAddNotification, localImag }; const resumeContainer = () => { - setActionsKebabOpen(false); - client.postContainer(container.isSystem, "unpause", container.Id, {}) .catch(ex => { const error = cockpit.format(_("Failed to resume container $0"), container.Name); // not-covered: OS error @@ -110,8 +103,6 @@ const ContainerActions = ({ container, healthcheck, onAddNotification, localImag }; const pauseContainer = () => { - setActionsKebabOpen(false); - client.postContainer(container.isSystem, "pause", container.Id, {}) .catch(ex => { const error = cockpit.format(_("Failed to pause container $0"), container.Name); // not-covered: OS error @@ -120,15 +111,11 @@ const ContainerActions = ({ container, healthcheck, onAddNotification, localImag }; const commitContainer = () => { - setActionsKebabOpen(false); - Dialogs.show(); }; const runHealthcheck = () => { - setActionsKebabOpen(false); - client.runHealthcheck(container.isSystem, container.Id) .catch(ex => { const error = cockpit.format(_("Failed to run health check on container $0"), container.Name); // not-covered: OS error @@ -139,8 +126,6 @@ const ContainerActions = ({ container, healthcheck, onAddNotification, localImag const restartContainer = (force) => { const args = {}; - setActionsKebabOpen(false); - if (force) args.t = 0; client.postContainer(container.isSystem, "restart", container.Id, args) @@ -151,8 +136,6 @@ const ContainerActions = ({ container, healthcheck, onAddNotification, localImag }; const renameContainer = () => { - setActionsKebabOpen(false); - if (container.State.Status !== "running" || version.localeCompare("3.0.1", undefined, { numeric: true, sensitivity: 'base' }) >= 0) { Dialogs.show( { - setActionsKebabOpen(false); - Dialogs.show(); }; const restoreContainer = () => { - setActionsKebabOpen(false); - Dialogs.show(); }; @@ -222,7 +201,7 @@ const ContainerActions = ({ container, healthcheck, onAddNotification, localImag if (container.isSystem && !isPaused) { actions.push( - , + , checkpointContainer()}> {_("Checkpoint")} @@ -243,7 +222,7 @@ const ContainerActions = ({ container, healthcheck, onAddNotification, localImag } if (container.isSystem && container.State?.CheckpointPath) { actions.push( - , + , restoreContainer()}> {_("Restore")} @@ -256,7 +235,7 @@ const ContainerActions = ({ container, healthcheck, onAddNotification, localImag } } - actions.push(); + actions.push(); actions.push( commitContainer()}> @@ -265,7 +244,7 @@ const ContainerActions = ({ container, healthcheck, onAddNotification, localImag ); if (isRunning && healthcheck !== "") { - actions.push(); + actions.push(); actions.push( runHealthcheck()}> @@ -274,7 +253,7 @@ const ContainerActions = ({ container, healthcheck, onAddNotification, localImag ); } - actions.push(); + actions.push(); actions.push( ); - const kebab = ( - setActionsKebabOpen(isOpen)} />} - isOpen={isActionsKebabOpen} - isPlain - position="right" - dropdownItems={actions} /> - ); - - return kebab; + return ; }; export let onDownloadContainer = function funcOnDownloadContainer(container) { @@ -319,27 +290,18 @@ const localize_health = (state) => { }; const ContainerOverActions = ({ handlePruneUnusedContainers, unusedContainers }) => { - const [isActionsKebabOpen, setIsActionsKebabOpen] = useState(false); - - return ( - setIsActionsKebabOpen(!isActionsKebabOpen)} id="containers-actions-dropdown" />} - isOpen={isActionsKebabOpen} - isPlain - position="right" - dropdownItems={[ - { - setIsActionsKebabOpen(false); - handlePruneUnusedContainers(); - }} - isDisabled={unusedContainers.length === 0}> - {_("Prune unused containers")} - , - ]} /> - ); + const actions = [ + handlePruneUnusedContainers()} + isDisabled={unusedContainers.length === 0}> + {_("Prune unused containers")} + , + ]; + + return ; }; class Containers extends React.Component { diff --git a/src/Dropdown.jsx b/src/Dropdown.jsx deleted file mode 100644 index d78ffffe1..000000000 --- a/src/Dropdown.jsx +++ /dev/null @@ -1,37 +0,0 @@ -import React, { useState } from 'react'; -import { Dropdown, DropdownItem, DropdownToggle, DropdownToggleAction } from '@patternfly/react-core/dist/esm/deprecated/components/Dropdown/index.js'; - -export const DropDown = ({ actions }) => { - const [isOpen, setIsOpen] = useState(false); - const dropdownItems = actions - .map(button => { - return ( - - {button.label} - - ); - }); - - return ( - setIsOpen(!isOpen)} - id={actions[0].label + "-dropdown"} - toggle={ - - {actions[0].label} - - ]} - splitButtonVariant="action" - onToggle={(_event, open) => setIsOpen(open)} - /> - } - isOpen={isOpen} - dropdownItems={dropdownItems} - /> - ); -}; -DropDown.defaultProps = { - actions: [{ label: '' }] -}; diff --git a/src/Images.jsx b/src/Images.jsx index 72957d5fe..c559caa40 100644 --- a/src/Images.jsx +++ b/src/Images.jsx @@ -1,7 +1,7 @@ -import React, { useState } from 'react'; +import React from 'react'; import { Button } from "@patternfly/react-core/dist/esm/components/Button"; import { Card, CardBody, CardFooter, CardHeader, CardTitle } from "@patternfly/react-core/dist/esm/components/Card"; -import { Dropdown, DropdownItem, KebabToggle } from '@patternfly/react-core/dist/esm/deprecated/components/Dropdown/index.js'; +import { DropdownItem } from '@patternfly/react-core/dist/esm/components/Dropdown/index.js'; import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex"; import { ExpandableSection } from "@patternfly/react-core/dist/esm/components/ExpandableSection"; import { Text, TextVariants } from "@patternfly/react-core/dist/esm/components/Text"; @@ -23,6 +23,8 @@ import { useDialogs, DialogsContext } from "dialogs.jsx"; import './Images.css'; import '@patternfly/react-styles/css/utilities/Sizing/sizing.css'; +import { KebabDropdown } from "cockpit-components-dropdown.jsx"; + const _ = cockpit.gettext; class Images extends React.Component { @@ -328,44 +330,40 @@ class Images extends React.Component { } const ImageOverActions = ({ handleDownloadNewImage, handlePruneUsedImages, unusedImages }) => { - const [isActionsKebabOpen, setIsActionsKebabOpen] = useState(false); + const actions = [ + handleDownloadNewImage()} + > + {_("Download new image")} + , + handlePruneUsedImages()} + isDisabled={unusedImages.length === 0} + isAriaDisabled={unusedImages.length === 0} + > + {_("Prune unused images")} + + ]; return ( - setIsActionsKebabOpen(!isActionsKebabOpen)} id="image-actions-dropdown" />} - isOpen={isActionsKebabOpen} - isPlain - position="right" - dropdownItems={[ - { - setIsActionsKebabOpen(false); - handleDownloadNewImage(); - }}> - {_("Download new image")} - , - { - setIsActionsKebabOpen(false); - handlePruneUsedImages(); - }} - isDisabled={unusedImages.length === 0} - isAriaDisabled={unusedImages.length === 0}> - {_("Prune unused images")} - , - ]} /> + ); }; const ImageActions = ({ image, onAddNotification, user, systemServiceAvailable, userServiceAvailable }) => { const Dialogs = useDialogs(); - const [isActionsKebabOpen, setIsActionsKebabOpen] = useState(false); const runImage = () => { - setIsActionsKebabOpen(false); Dialogs.show( {(podmanInfo) => ( @@ -387,7 +385,6 @@ const ImageActions = ({ image, onAddNotification, user, systemServiceAvailable, }; const removeImage = () => { - setIsActionsKebabOpen(false); Dialogs.show(); }; @@ -406,31 +403,25 @@ const ImageActions = ({ image, onAddNotification, user, systemServiceAvailable, ); - const extraActions = ( - setIsActionsKebabOpen(!isActionsKebabOpen)} />} - isOpen={isActionsKebabOpen} - isPlain - position="right" - dropdownItems={[ - - {_("Create container")} - , - - {_("Delete")} - - ]} /> - ); + const dropdownActions = [ + + {_("Create container")} + , + + {_("Delete")} + + ]; return ( <> {runImageAction} - {extraActions} + ); }; diff --git a/src/PodActions.jsx b/src/PodActions.jsx index 5e7f92bfe..c01ad771b 100644 --- a/src/PodActions.jsx +++ b/src/PodActions.jsx @@ -3,12 +3,14 @@ import React, { useState } from 'react'; import { Button } from "@patternfly/react-core/dist/esm/components/Button"; import { Alert } from "@patternfly/react-core/dist/esm/components/Alert"; import { Modal } from "@patternfly/react-core/dist/esm/components/Modal"; -import { Dropdown, DropdownItem, DropdownPosition, DropdownSeparator, KebabToggle } from '@patternfly/react-core/dist/esm/deprecated/components/Dropdown/index.js'; +import { Divider } from '@patternfly/react-core/dist/esm/components/Divider/index.js'; +import { DropdownItem } from '@patternfly/react-core/dist/esm/components/Dropdown/index.js'; import { List, ListItem } from "@patternfly/react-core/dist/esm/components/List"; import { Stack } from "@patternfly/react-core/dist/esm/layouts/Stack"; import cockpit from 'cockpit'; import { useDialogs } from "dialogs.jsx"; +import { KebabDropdown } from "cockpit-components-dropdown.jsx"; import * as client from './client.js'; @@ -69,7 +71,6 @@ const PodDeleteModal = ({ pod }) => { export const PodActions = ({ onAddNotification, pod }) => { const Dialogs = useDialogs(); - const [isOpen, setOpen] = useState(false); const dropdownItems = []; // Possible Pod Statuses can be found here https://github.com/containers/podman/blob/main/libpod/define/podstate.go @@ -168,7 +169,7 @@ export const PodActions = ({ onAddNotification, pod }) => { } if (dropdownItems.length > 1) { - dropdownItems.push(); + dropdownItems.push(); } dropdownItems.push( { return null; return ( - setOpen(!isOpen)} - position={DropdownPosition.right} - toggle={ setOpen(value)} id={"pod-" + pod.Name + (pod.isSystem ? "-system" : "-user") + "-action-toggle"} />} - isOpen={isOpen} - isPlain - dropdownItems={dropdownItems} /> + ); }; diff --git a/test/check-application b/test/check-application index d3e8332af..41109d43d 100755 --- a/test/check-application +++ b/test/check-application @@ -214,11 +214,11 @@ class TestApplication(testlib.MachineCase): def performContainerAction(self, container, cmd): b = self.browser - b.click(f"#containers-containers tbody tr:contains('{container}') .pf-v5-c-dropdown__toggle") - b.click(f"#containers-containers tbody tr:contains('{container}') .pf-v5-c-dropdown__menu li:contains({cmd})") + b.click(f"#containers-containers tbody tr:contains('{container}') .pf-v5-c-menu-toggle") + b.click(f"#containers-containers tbody tr:contains('{container}') button.pf-v5-c-menu__item:contains({cmd})") def getContainerAction(self, container, cmd): - return f"#containers-containers tbody tr:contains('{container}') .pf-v5-c-dropdown__menu li:contains({cmd})" + return f"#containers-containers tbody tr:contains('{container}') button.pf-v5-c-menu__item:contains({cmd})" def toggleExpandedContainer(self, container): b = self.browser @@ -291,8 +291,8 @@ class TestApplication(testlib.MachineCase): b = self.browser b.click(f"#pod-{podName}-{podOwner}-action-toggle") - b.click(f"ul[aria-labelledby=pod-{podName}-{podOwner}-action-toggle] li > button.pod-action-{action.lower()}") - b.wait_not_present(f"ul[aria-labelledby=pod-{podName}-{podOwner}-action-toggle]") + b.click(f"ul.pf-v5-c-menu__list li > button.pod-action-{action.lower()}") + b.wait_not_present("ul.pf-v5-c-menu__list") def getStartTime(self, container: str, *, auth: bool) -> str: # don't format the raw time strings from the API, force json format @@ -578,6 +578,10 @@ class TestApplication(testlib.MachineCase): def _testBasic(self, auth): b = self.browser + def clickDeleteImage(image_sel): + b.click(f'{image_sel} .pf-v5-c-menu-toggle') + b.click(image_sel + " button.btn-delete") + if not auth: self.allow_browser_errors("Failed to start system podman.socket.*") @@ -629,7 +633,7 @@ class TestApplication(testlib.MachineCase): hello_sel = f"#containers-images tbody tr[data-row-id=\"{images[IMG_HELLO_LATEST]}{auth}\"]".lower() b.wait_visible(hello_sel) b.click(hello_sel + " td.pf-v5-c-table__toggle button") - b.click(hello_sel + " .pf-v5-c-dropdown__toggle") + b.click(hello_sel + " .pf-v5-c-menu-toggle") b.wait_visible(hello_sel + " button.btn-delete") b.wait_in_text("#containers-images tbody.pf-m-expanded tr .image-details:first-child", "Command/run.sh") # Show history @@ -799,8 +803,7 @@ class TestApplication(testlib.MachineCase): b.wait_in_text(busybox_sel + " + tr", f"{IMG_BUSYBOX}:3") b.wait_in_text(busybox_sel + " + tr", f"{IMG_BUSYBOX}:4") - b.click(busybox_sel + " .pf-v5-c-dropdown__toggle") - b.click(busybox_sel + " button.btn-delete") + clickDeleteImage(busybox_sel) self.assertTrue(b.get_checked(f".pf-v5-c-check__input[aria-label='{IMG_BUSYBOX_LATEST}']")) b.set_checked(f".pf-v5-c-check__input[aria-label='{IMG_BUSYBOX}:1']", True) b.set_checked(f".pf-v5-c-check__input[aria-label='{IMG_BUSYBOX}:3']", True) @@ -811,8 +814,7 @@ class TestApplication(testlib.MachineCase): b.wait_not_in_text(busybox_sel + " + tr", f"{IMG_BUSYBOX}:1") b.wait_not_in_text(busybox_sel + " + tr", f"{IMG_BUSYBOX}:3") - b.click(busybox_sel + " .pf-v5-c-dropdown__toggle") - b.click(busybox_sel + " button.btn-delete") + clickDeleteImage(busybox_sel) b.click("#delete-all") self.assertTrue(b.get_checked(f".pf-v5-c-check__input[aria-label='{IMG_BUSYBOX_LATEST}']")) self.assertTrue(b.get_checked(f".pf-v5-c-check__input[aria-label='{IMG_BUSYBOX}:2']")) @@ -851,8 +853,7 @@ class TestApplication(testlib.MachineCase): alpine_sel = f"#containers-images tbody tr[data-row-id=\"{images[IMG_ALPINE_LATEST]}{auth}\"]".lower() b.wait_visible(alpine_sel) b.click(alpine_sel + " td.pf-v5-c-table__toggle button") - b.click(alpine_sel + " .pf-v5-c-dropdown__toggle") - b.click(alpine_sel + " button.btn-delete") + clickDeleteImage(alpine_sel) self.confirm_modal("Delete") self.confirm_modal("Force delete") b.wait_not_present(alpine_sel) @@ -898,8 +899,7 @@ class TestApplication(testlib.MachineCase): # Delete intermediate images intermediate_image_sel = "#containers-images tbody:last-child:contains(':')" b.click(".listing-action button:contains('Show intermediate images')") - b.click(intermediate_image_sel + " .pf-v5-c-dropdown__toggle") - b.click(intermediate_image_sel + " button.btn-delete") + clickDeleteImage(intermediate_image_sel) self.confirm_modal("Delete") b.wait_not_present(intermediate_image_sel) @@ -916,8 +916,7 @@ class TestApplication(testlib.MachineCase): # Delete intermediate image which is in use self.execute(auth, f"podman untag {IMG_INTERMEDIATE}") - b.click(intermediate_image_sel + " .pf-v5-c-dropdown__toggle") - b.click(intermediate_image_sel + " button.btn-delete") + clickDeleteImage(intermediate_image_sel) self.confirm_modal("Delete") self.confirm_modal("Force delete") b.wait_not_in_text("#containers-images", ":") @@ -1144,7 +1143,7 @@ class TestApplication(testlib.MachineCase): b.click(sel + " td.pf-v5-c-table__toggle button") # Click the delete icon on the image row - b.click(sel + " .pf-v5-c-dropdown__toggle") + b.click(sel + " .pf-v5-c-menu-toggle") b.click(sel + ' button.btn-delete') if another: @@ -1254,7 +1253,7 @@ class TestApplication(testlib.MachineCase): container_sha = self.execute(auth, "podman inspect --format '{{.Id}}' swamped-crate").strip() self.waitContainer(container_sha, auth, name='swamped-crate', image=IMG_BUSYBOX, state='Exited', owner="system" if auth else "admin") - b.click("#containers-containers tbody tr:contains('swamped-crate') .pf-v5-c-dropdown__toggle") + b.click("#containers-containers tbody tr:contains('swamped-crate') .pf-v5-c-menu-toggle") if not auth: # Checkpoint/restore is not supported on user containers yet - the related buttons should not be shown @@ -1264,7 +1263,7 @@ class TestApplication(testlib.MachineCase): # Health check is not set up b.wait_not_present(self.getContainerAction('swamped-crate', 'Run health check')) - b.click("#containers-containers tbody tr:contains('swamped-crate') .pf-v5-c-dropdown__toggle") + b.click("#containers-containers tbody tr:contains('swamped-crate') .pf-v5-c-menu-toggle") # Start the container self.performContainerAction(IMG_BUSYBOX, "Start") @@ -1313,10 +1312,10 @@ class TestApplication(testlib.MachineCase): self.waitContainerRow(IMG_BUSYBOX) if not auth: # Check that the checkpoint option is not present for rootless - b.click(f"#containers-containers tbody tr:contains('{IMG_BUSYBOX}') .pf-v5-c-dropdown__toggle") + b.click(f"#containers-containers tbody tr:contains('{IMG_BUSYBOX}') .pf-v5-c-menu-toggle") b.wait_visible(self.getContainerAction(IMG_BUSYBOX, 'Force stop')) b.wait_not_present(self.getContainerAction(IMG_BUSYBOX, 'Checkpoint')) - b.click(f"#containers-containers tbody tr:contains('{IMG_BUSYBOX}') .pf-v5-c-dropdown__toggle") + b.click(f"#containers-containers tbody tr:contains('{IMG_BUSYBOX}') .pf-v5-c-menu-toggle") # Stop the container self.performContainerAction(IMG_BUSYBOX, "Force stop") @@ -1395,9 +1394,9 @@ class TestApplication(testlib.MachineCase): b.wait(lambda: self.execute(True, "podman ps --all | grep -e swamped-crate -e Exited")) # Check that the restore option is not present (i.e. start is a regular button) - b.click(f"#containers-containers tbody tr:contains('{IMG_BUSYBOX}') .pf-v5-c-dropdown__toggle") + b.click(f"#containers-containers tbody tr:contains('{IMG_BUSYBOX}') .pf-v5-c-menu-toggle") b.wait_not_present(self.getContainerAction(IMG_BUSYBOX, 'Restore')) - b.click(f"#containers-containers tbody tr:contains('{IMG_BUSYBOX}') .pf-v5-c-dropdown__toggle") + b.click(f"#containers-containers tbody tr:contains('{IMG_BUSYBOX}') .pf-v5-c-menu-toggle") # Start the container self.performContainerAction("swamped-crate", "Start") @@ -2200,7 +2199,7 @@ class TestApplication(testlib.MachineCase): # By default we have 3 unused images, start one. self.execute(auth or root, f"podman run -d --name used_image --stop-timeout 0 {IMG_ALPINE} sh") b.click("#image-actions-dropdown") - b.click("button:contains(Prune unused images)") + b.click("#prune-unused-images-button") if auth: b.wait_js_func("ph_count_check", ".pf-v5-c-modal-box__body .pf-v5-c-list li", @@ -2226,7 +2225,7 @@ class TestApplication(testlib.MachineCase): # Prune button should now be disabled b.click("#image-actions-dropdown") - b.wait_visible("button:contains(Prune unused images).pf-m-disabled") + b.wait_visible(".pf-m-disabled.pf-v5-c-menu__list-item:contains(Prune unused images)") def testPruneUnusedImagesSystemSelections(self): """ Test the prune unused images selection options""" @@ -2266,7 +2265,7 @@ class TestApplication(testlib.MachineCase): # Prune button should now be disabled b.click("#image-actions-dropdown") - b.wait_visible("button:contains(Prune unused images).pf-m-disabled") + b.wait_visible(".pf-v5-c-menu__list-item.pf-m-disabled:contains(Prune unused images)") def testPruneUnusedContainersSystem(self): self._testPruneUnusedContainersSystem(True)