diff --git a/src/components/vm/confirmDialog.jsx b/src/components/vm/confirmDialog.jsx
new file mode 100644
index 000000000..f6cfad803
--- /dev/null
+++ b/src/components/vm/confirmDialog.jsx
@@ -0,0 +1,76 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see .
+ */
+import cockpit from 'cockpit';
+import React, { useEffect, useState } from 'react';
+import { Button } from "@patternfly/react-core/dist/esm/components/Button";
+import { DescriptionList, DescriptionListDescription, DescriptionListGroup, DescriptionListTerm } from "@patternfly/react-core/dist/esm/components/DescriptionList";
+import { Modal } from "@patternfly/react-core/dist/esm/components/Modal";
+import { useDialogs } from 'dialogs.jsx';
+import { distanceToNow } from 'timeformat.js';
+
+import { domainGetStartTime } from '../../libvirtApi/domain.js';
+
+const _ = cockpit.gettext;
+
+export const ConfirmDialog = ({ idPrefix, actionsList, title, titleIcon, vm }) => {
+ const Dialogs = useDialogs();
+ const [uptime, setUptime] = useState();
+
+ useEffect(() => {
+ return domainGetStartTime({ connectionName: vm.connectionName, vmName: vm.name })
+ .then(res => setUptime(res))
+ .catch(e => console.error(JSON.stringify(e)));
+ }, [vm]);
+
+ const actions = actionsList.map(action =>
+
+ );
+ actions.push(
+
+ );
+
+ return (
+
+ {uptime &&
+
+
+ {_("Uptime")}
+ {distanceToNow(new Date(uptime))}
+
+ }
+
+ );
+};
diff --git a/src/components/vm/vmActions.jsx b/src/components/vm/vmActions.jsx
index f1414161a..e48970e0b 100644
--- a/src/components/vm/vmActions.jsx
+++ b/src/components/vm/vmActions.jsx
@@ -22,7 +22,9 @@ import PropTypes from 'prop-types';
import { Button } from "@patternfly/react-core/dist/esm/components/Button";
import { Dropdown, DropdownItem, DropdownSeparator, KebabToggle } from "@patternfly/react-core/dist/esm/deprecated/components/Dropdown";
import { Tooltip } from "@patternfly/react-core/dist/esm/components/Tooltip";
+import { PowerOffIcon, RedoIcon } from '@patternfly/react-icons';
import { useDialogs } from 'dialogs.jsx';
+import { fmt_to_fragments } from 'utils.jsx';
import { updateVm } from '../../actions/store-actions.js';
import {
@@ -30,6 +32,7 @@ import {
} from "../../helpers.js";
import { CloneDialog } from './vmCloneDialog.jsx';
+import { ConfirmDialog } from './confirmDialog.jsx';
import { DeleteDialog } from "./deleteDialog.jsx";
import { MigrateDialog } from './vmMigrateDialog.jsx';
import { RenameDialog } from './vmRenameDialog.jsx';
@@ -227,21 +230,72 @@ const VmActions = ({ vm, onAddErrorNotification, isDetailsPage }) => {
variant={isDetailsPage ? 'primary' : 'secondary'}
isLoading={operationInProgress}
isDisabled={operationInProgress}
- onClick={() => { setOperationInProgress(true); onShutdown(vm, setOperationInProgress) }} id={`${id}-shutdown-button`}>
+ id={`${id}-shutdown-button`}
+ onClick={() => Dialogs.show(
+ {vm.name})}
+ titleIcon={PowerOffIcon}
+ vm={vm}
+ actionsList={[
+ {
+ variant: "primary",
+ handler: () => {
+ setOperationInProgress(true);
+ onShutdown(vm, setOperationInProgress);
+ },
+ name: _("Shut down"),
+ id: "off",
+ },
+ {
+ variant: "secondary",
+ handler: () => {
+ onReboot(vm);
+ },
+ name: _("Reboot"),
+ id: "reboot",
+ },
+ ]} />
+ )}>
{_("Shut down")}
);
dropdownItems.push(
onShutdown(vm)}>
+ onClick={() => Dialogs.show(
+ {vm.name})}
+ titleIcon={PowerOffIcon}
+ vm={vm}
+ actionsList={[
+ {
+ variant: "primary",
+ handler: () => onShutdown(vm),
+ name: _("Shut down"),
+ id: "off",
+ },
+ ]} />
+ )}>
{_("Shut down")}
);
dropdownItems.push(
onForceoff(vm)}>
+ onClick={() => Dialogs.show(
+ {vm.name})}
+ titleIcon={PowerOffIcon}
+ vm={vm}
+ actionsList={[
+ {
+ variant: "primary",
+ handler: () => onForceoff(vm),
+ name: _("Force shut down"),
+ id: "forceOff",
+ },
+ ]} />
+ )}>
{_("Force shut down")}
);
@@ -249,7 +303,19 @@ const VmActions = ({ vm, onAddErrorNotification, isDetailsPage }) => {
dropdownItems.push(
onSendNMI(vm)}>
+ onClick={() => Dialogs.show(
+ {vm.name})}
+ vm={vm}
+ actionsList={[
+ {
+ variant: "primary",
+ handler: () => onSendNMI(vm),
+ name: _("Send non-maskable interrupt"),
+ id: "sendNMI",
+ },
+ ]} />
+ )}>
{_("Send non-maskable interrupt")}
);
@@ -260,14 +326,40 @@ const VmActions = ({ vm, onAddErrorNotification, isDetailsPage }) => {
dropdownItems.push(
onReboot(vm)}>
+ onClick={() => Dialogs.show(
+ {vm.name})}
+ titleIcon={RedoIcon}
+ vm={vm}
+ actionsList={[
+ {
+ variant: "primary",
+ handler: onReboot(vm),
+ name: _("Reboot"),
+ id: "reboot",
+ },
+ ]} />
+ )}>
{_("Reboot")}
);
dropdownItems.push(
onForceReboot(vm)}>
+ onClick={() => Dialogs.show(
+ {vm.name})}
+ vm={vm}
+ titleIcon={RedoIcon}
+ actionsList={[
+ {
+ variant: "primary",
+ handler: onForceReboot(vm),
+ name: _("Force reboot"),
+ id: "forceReboot",
+ },
+ ]} />
+ )}>
{_("Force reboot")}
);
diff --git a/test/check-machines-lifecycle b/test/check-machines-lifecycle
index 6002e0ed1..a754dd224 100755
--- a/test/check-machines-lifecycle
+++ b/test/check-machines-lifecycle
@@ -183,6 +183,8 @@ class TestMachinesLifecycle(VirtualMachinesCase):
# the shut off button beside the VM name
self.waitCirrOSBooted(args['logfile'])
b.click("#vm-subVmTest1-system-shutdown-button")
+ b.wait_visible("#vm-subVmTest1-system-confirm-action-modal")
+ b.click(".pf-c-modal-box__footer #vm-subVmTest1-system-off")
b.wait_in_text("#vm-subVmTest1-system-state", "Shut off")
b.wait_visible("#vm-subVmTest1-system-run")
@@ -211,7 +213,7 @@ class TestMachinesLifecycle(VirtualMachinesCase):
m.execute("virsh define --file /tmp/subVmTest1.xml")
# start another one, should appear automatically
- self.createVm("subVmTest2")
+ args2 = self.createVm("subVmTest2")
b.wait_in_text("#vm-subVmTest2-system-state", "Running")
self.goToVmPage("subVmTest2")
b.wait_in_text("#vm-subVmTest2-cpu", "1 vCPU")
@@ -231,6 +233,23 @@ class TestMachinesLifecycle(VirtualMachinesCase):
b.click("#vm-subVmTest2-system-run")
+ # reboot through shutdown dialog
+ self.machine.execute(f"echo '' > {args2['logfile']}")
+ b.click("#vm-subVmTest2-system-shutdown-button")
+ b.wait_visible("#vm-subVmTest2-system-confirm-action-modal")
+ b.click(".pf-c-modal-box__footer button:contains(Reboot)")
+ b.wait_not_present("#vm-subVmTest2-system-confirm-action-modal")
+ b.wait_in_text("#vm-subVmTest2-system-state", "Running")
+ self.waitCirrOSBooted(args2['logfile'])
+
+ # Check uptime
+ b.click("#vm-subVmTest2-system-shutdown-button")
+ b.wait_visible("#vm-subVmTest2-system-confirm-action-modal")
+ if run_pixel_tests:
+ b.assert_pixels("#vm-subVmTest2-system-confirm-action-modal", "shutdown-confirm-dialog")
+ b.click(".pf-c-modal-box__footer button:contains(Cancel)")
+ b.wait_not_present("#vm-subVmTest2-system-confirm-action-modal")
+
b.set_input_text("#text-search", "subVmTest2")
self.waitVmRow("subVmTest2")
self.waitVmRow("subVmTest1", "system", False)
@@ -472,8 +491,10 @@ class TestMachinesLifecycle(VirtualMachinesCase):
m.execute(f"virsh undefine {name}")
b.wait_visible(f"tr[data-row-id=vm-{name}-system][data-vm-transient=true]")
b.click(f"#vm-{name}-system-action-kebab button")
- b.wait_visible(f"#vm-{name}-system-delete a.pf-m-disabled")
+ b.wait_visible(f"#vm-{name}-system-delete a.pf-m-disabled") # delete buton should be disabled
b.click(f"#vm-{name}-system-forceOff")
+ b.wait_visible(f"#vm-{name}-system-confirm-action-modal")
+ b.click(f".pf-c-modal-box__footer #vm-{name}-system-forceOff")
self.waitVmRow(name, 'system', False)
b.wait_not_present(f'#vm-{name}-system-state button:contains("view more")')
diff --git a/test/machineslib.py b/test/machineslib.py
index 70f7452e4..2d4a1910f 100644
--- a/test/machineslib.py
+++ b/test/machineslib.py
@@ -39,6 +39,12 @@ def performAction(self, vmName, action, checkExpectedState=True, connectionName=
b.wait_visible("#vm-{0}-{1}-action-kebab > .pf-c-dropdown__menu".format(vmName, connectionName))
b.click("#vm-{0}-{1}-{2} a".format(vmName, connectionName, action))
+ # Some actions, which can cause expensive downtime when clicked accidentally, have confirmation dialog
+ if action in ["off", "forceOff", "reboot", "forceReboot", "sendNMI"]:
+ b.wait_visible("#vm-{0}-{1}-confirm-action-modal".format(vmName, connectionName))
+ b.click(".pf-c-modal-box__footer #vm-{0}-{1}-{2}".format(vmName, connectionName, action))
+ b.wait_not_present("#vm-{0}-{1}-confirm-action-modal".format(vmName, connectionName))
+
if not checkExpectedState:
return
diff --git a/test/reference b/test/reference
index d5f8e3e1d..1e327c7d1 160000
--- a/test/reference
+++ b/test/reference
@@ -1 +1 @@
-Subproject commit d5f8e3e1d5c30cfb5b09c92bf9a59e032b995053
+Subproject commit 1e327c7d1aca5ccacf77ff5d9a577a8f90ebe684