Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added DASDs format confirmation dialog #1618

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
6 changes: 6 additions & 0 deletions web/package/agama-web-ui.changes
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
-------------------------------------------------------------------
Mon Sep 23 09:04:56 UTC 2024 - Knut Anderssen <kanderssen@suse.com>

- Added confirmation dialog when formatting DASD devices as it is
considered a dangerous action (gh#openSUSE/agama#1618).

-------------------------------------------------------------------
Fri Sep 20 11:42:25 UTC 2024 - Imobach Gonzalez Sosa <igonzalezsosa@suse.com>

Expand Down
60 changes: 59 additions & 1 deletion web/src/components/storage/dasd/DASDTable.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,20 @@ describe("DASDTable", () => {
beforeEach(() => {
mockDASDDevices = [
{
id: "0.0.0200",
id: "0.0.0160",
enabled: false,
deviceName: "",
deviceType: "",
formatted: false,
diag: false,
status: "offline",
accessType: "",
partitionInfo: "",
hexId: 0x160,
},
{
id: "0.0.0200",
enabled: true,
deviceName: "dasda",
deviceType: "eckd",
formatted: false,
Expand All @@ -57,5 +69,51 @@ describe("DASDTable", () => {
installerRender(<DASDTable />);
screen.getByText("active");
});

it("does not allow to perform any action if not selected any device", () => {
installerRender(<DASDTable />);
const button = screen.getByRole("button", { name: "Perform an action" });
expect(button).toHaveAttribute("disabled");
});

describe("when there are some DASD selected", () => {
it("allows to perform a set of actions over them", async () => {
const { user } = installerRender(<DASDTable />);
const selection = screen.getByRole("checkbox", { name: "Select row 0" });
await user.click(selection);
const button = screen.getByRole("button", { name: "Perform an action" });
expect(button).not.toHaveAttribute("disabled");
await user.click(button);
screen.getByRole("menuitem", { name: "Format" });
});

describe("and the user click on format", () => {
it("shows a confirmation dialog if all the devices are online", async () => {
const { user } = installerRender(<DASDTable />);
const selection = screen.getByRole("checkbox", { name: "Select row 1" });
await user.click(selection);
const button = screen.getByRole("button", { name: "Perform an action" });
expect(button).not.toHaveAttribute("disabled");
await user.click(button);
const format = screen.getByRole("menuitem", { name: "Format" });
await user.click(format);
screen.getByRole("dialog", { name: "Format selected devices?" });
});

it("shows a warning dialog if some device is offline", async () => {
const { user } = installerRender(<DASDTable />);
let selection = screen.getByRole("checkbox", { name: "Select row 0" });
await user.click(selection);
selection = screen.getByRole("checkbox", { name: "Select row 1" });
await user.click(selection);
const button = screen.getByRole("button", { name: "Perform an action" });
expect(button).not.toHaveAttribute("disabled");
await user.click(button);
const format = screen.getByRole("menuitem", { name: "Format" });
await user.click(format);
screen.getByRole("dialog", { name: "Cannot format all selected devices" });
});
});
});
});
});
146 changes: 101 additions & 45 deletions web/src/components/storage/dasd/DASDTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ import {
Dropdown,
DropdownItem,
DropdownList,
List,
ListItem,
MenuToggle,
Stack,
Text,
TextInputGroup,
TextInputGroupMain,
TextInputGroupUtilities,
Expand All @@ -36,6 +40,7 @@ import {
ToolbarGroup,
ToolbarItem,
} from "@patternfly/react-core";
import { Popup } from "~/components/core";
import { Table, Thead, Tr, Th, Tbody, Td } from "@patternfly/react-table";
import { Page } from "~/components/core";
import { Icon } from "~/components/layout";
Expand All @@ -56,7 +61,7 @@ const columnData = (device: DASDDevice, column: { id: string; sortId?: string; l
if (!device.enabled) data = "";
break;
case "partitionInfo":
data = data.split(",").map((d) => <div key={d}>{d}</div>);
data = data.split(",").map((d: string) => <div key={d}>{d}</div>);
break;
}

Expand All @@ -79,29 +84,64 @@ const columns = [
{ id: "formatted", label: _("Formatted") },
{ id: "partitionInfo", label: _("Partition Info") },
];
const DevicesList = ({ devices }) => (
<List>
{devices.map((d: DASDDevice) => (
<ListItem key={d.id}>{d.id}</ListItem>
))}
</List>
);
const FormatNotPossible = ({ devices, onAccept }) => (
<Popup isOpen title={_("Cannot format all selected devices")}>
<Stack hasGutter>
<Text>
{_(
"Offline devices must be activated before formatting them. Please, unselect or activate the devices listed below and try it again",
)}
</Text>
<DevicesList devices={devices} />
</Stack>
<Popup.Actions>
<Popup.Confirm onClick={onAccept}>{_("Accept")}</Popup.Confirm>
</Popup.Actions>
</Popup>
);

const FormatConfirmation = ({ devices, onCancel, onConfirm }) => (
<Popup isOpen title={_("Format selected devices?")}>
<Stack hasGutter>
<Text>
{_(
"This action could destroy any data stored on the devices listed below. Please, confirm that you really want to continue.",
)}
</Text>
<DevicesList devices={devices} />
</Stack>
<Popup.Actions>
<Popup.Confirm onClick={onConfirm} />
<Popup.Cancel onClick={onCancel} autoFocus />
</Popup.Actions>
</Popup>
);

const Actions = ({ devices, isDisabled }: { devices: DASDDevice[]; isDisabled: boolean }) => {
const { mutate: updateDASD } = useDASDMutation();
const { mutate: formatDASD } = useFormatDASDMutation();
const [isOpen, setIsOpen] = useState(false);
const [requestFormat, setRequestFormat] = useState(false);

const onToggle = () => setIsOpen(!isOpen);
const onSelect = () => setIsOpen(false);
const cancelFormatRequest = () => setRequestFormat(false);

const deviceIds = devices.map((d) => d.id);
const offlineDevices = devices.filter((d) => !d.enabled);
const offlineDevicesSelected = offlineDevices.length > 0;
const activate = () => updateDASD({ action: "enable", devices: deviceIds });
const deactivate = () => updateDASD({ action: "disable", devices: deviceIds });
const setDiagOn = () => updateDASD({ action: "diagOn", devices: deviceIds });
const setDiagOff = () => updateDASD({ action: "diagOff", devices: deviceIds });
const format = () => {
const offline = devices.filter((d) => !d.enabled);

if (offline.length > 0) {
return false;
}

return formatDASD(devices.map((d) => d.id));
};
const format = () => formatDASD(devices.map((d) => d.id));

const Action = ({ children, ...props }) => (
<DropdownItem component="button" {...props}>
Expand All @@ -110,41 +150,57 @@ const Actions = ({ devices, isDisabled }: { devices: DASDDevice[]; isDisabled: b
);

return (
<Dropdown
isOpen={isOpen}
onSelect={onSelect}
toggle={(toggleRef) => (
<MenuToggle ref={toggleRef} variant="primary" isDisabled={isDisabled} onClick={onToggle}>
{/* TRANSLATORS: drop down menu label */}
{_("Perform an action")}
</MenuToggle>
<>
{requestFormat && offlineDevicesSelected && (
<FormatNotPossible devices={offlineDevices} onAccept={cancelFormatRequest} />
)}

{requestFormat && !offlineDevicesSelected && (
<FormatConfirmation
devices={devices}
onCancel={cancelFormatRequest}
onConfirm={() => {
cancelFormatRequest();
format();
}}
/>
)}
>
<DropdownList>
{/** TRANSLATORS: drop down menu action, activate the device */}
<Action key="activate" onClick={activate}>
{_("Activate")}
</Action>
{/** TRANSLATORS: drop down menu action, deactivate the device */}
<Action key="deactivate" onClick={deactivate}>
{_("Deactivate")}
</Action>
<Divider key="first-separator" />
{/** TRANSLATORS: drop down menu action, enable DIAG access method */}
<Action key="set_diag_on" onClick={setDiagOn}>
{_("Set DIAG On")}
</Action>
{/** TRANSLATORS: drop down menu action, disable DIAG access method */}
<Action key="set_diag_off" onClick={setDiagOff}>
{_("Set DIAG Off")}
</Action>
<Divider key="second-separator" />
{/** TRANSLATORS: drop down menu action, format the disk */}
<Action key="format" onClick={format}>
{_("Format")}
</Action>
</DropdownList>
</Dropdown>
<Dropdown
isOpen={isOpen}
onSelect={onSelect}
toggle={(toggleRef) => (
<MenuToggle ref={toggleRef} variant="primary" isDisabled={isDisabled} onClick={onToggle}>
{/* TRANSLATORS: drop down menu label */}
{_("Perform an action")}
</MenuToggle>
)}
>
<DropdownList>
{/** TRANSLATORS: drop down menu action, activate the device */}
<Action key="activate" onClick={activate}>
{_("Activate")}
</Action>
{/** TRANSLATORS: drop down menu action, deactivate the device */}
<Action key="deactivate" onClick={deactivate}>
{_("Deactivate")}
</Action>
<Divider key="first-separator" />
{/** TRANSLATORS: drop down menu action, enable DIAG access method */}
<Action key="set_diag_on" onClick={setDiagOn}>
{_("Set DIAG On")}
</Action>
{/** TRANSLATORS: drop down menu action, disable DIAG access method */}
<Action key="set_diag_off" onClick={setDiagOff}>
{_("Set DIAG Off")}
</Action>
<Divider key="second-separator" />
{/** TRANSLATORS: drop down menu action, format the disk */}
<Action key="format" onClick={() => setRequestFormat(true)}>
{_("Format")}
</Action>
</DropdownList>
</Dropdown>
</>
);
};

Expand Down Expand Up @@ -326,7 +382,7 @@ export default function DASDTable() {
</ToolbarContent>
</Toolbar>

<Page.Section>
<Page.Section aria-label={_("DASDs table section")}>
<Content />
</Page.Section>
</>
Expand Down
Loading