* Copyright (C) 2023 Red Hat, Inc.
* SPDX-License-Identifier: LGPL-2.1-or-later
*/
import cockpit from "cockpit";
import React, { useState } from "react";
import { ActionList, ActionListItem } from "@patternfly/react-core/dist/esm/components/ActionList/index.js";
import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
import { Form, FormGroup } from "@patternfly/react-core/dist/esm/components/Form/index.js";
import { InputGroup, InputGroupItem } from "@patternfly/react-core/dist/esm/components/InputGroup/index.js";
import { Modal, ModalBody, ModalFooter, ModalHeader } from "@patternfly/react-core/dist/esm/components/Modal/index.js";
import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
import { EyeIcon } from "@patternfly/react-icons/dist/esm/icons/eye-icon";
import { EyeSlashIcon } from "@patternfly/react-icons/dist/esm/icons/eye-slash-icon";
import { LockIcon } from "@patternfly/react-icons/dist/esm/icons/lock-icon";
import {
findExistingSystems,
unlockDevice,
} from "../../../apis/storage_devicetree.js";
import { getDevicesAction } from "../../../actions/storage-actions.js";
import { FormHelper } from "cockpit-components-form-helper.jsx";
import { InlineNotification } from "cockpit-components-inline-notification.jsx";
const _ = cockpit.gettext;
const LockedDevices = ({ id, lockedEncryptedDevices }) => {
return (
<Flex id={id} spaceItems={{ default: "spaceItemsLg" }}>
{lockedEncryptedDevices.map(device => (
<Flex key={device} spaceItems={{ default: "spaceItemsXs" }} alignItems={{ default: "alignItemsCenter" }}>
<LockIcon />
<FlexItem>{device}</FlexItem>
</Flex>
))}
</Flex>
);
};
export const EncryptedDevices = ({ dispatch, idPrefix, lockedEncryptedDevices }) => {
const [showUnlockDialog, setShowUnlockDialog] = useState(false);
return (
<>
<Alert
isInline
title={_("Destination is encrypted")}
variant="warning"
actionLinks={
<ActionList>
<ActionListItem>
<Button id={idPrefix + "-unlock-devices-btn"} variant="primary" onClick={() => setShowUnlockDialog(true)}>
{_("Unlock")}
</Button>
</ActionListItem>
</ActionList>
}
>
{_("Unlock encrypted partitions to keep existing data and show more installation methods.")}
</Alert>
{showUnlockDialog &&
<UnlockDialog
dispatch={dispatch}
onClose={() => setShowUnlockDialog(false)}
lockedEncryptedDevices={lockedEncryptedDevices} />}
</>
);
};
const UnlockDialog = ({ dispatch, lockedEncryptedDevices, onClose }) => {
const [passphrase, setPassphrase] = useState("");
const [passphraseHidden, setPassphraseHidden] = useState(true);
const [dialogWarning, setDialogWarning] = useState();
const [dialogSuccess, setDialogSuccess] = useState();
const [inProgress, setInProgress] = useState(false);
const idPrefix = "unlock-device-dialog";
const onSubmit = () => {
setInProgress(true);
return Promise.allSettled(
lockedEncryptedDevices.map(device => (
unlockDevice({ device, passphrase })
))
).then(
res => {
if (res.every(r => r.status === "fulfilled")) {
if (res.every(r => r.value)) {
findExistingSystems({
onFail: exc => setDialogWarning(exc.message),
onSuccess: () => {
onClose();
dispatch(getDevicesAction());
},
});
} else {
const unlockedDevs = res.reduce((acc, r, i) => {
if (r.value) {
acc.push(lockedEncryptedDevices[i]);
}
return acc;
}, []);
if (unlockedDevs.length > 0) {
setDialogSuccess(cockpit.format(_("Successfully unlocked $0."), unlockedDevs.join(", ")));
setDialogWarning(undefined);
setPassphrase("");
} else {
setDialogSuccess(undefined);
setDialogWarning(_("Passphrase did not match any locked device"));
}
setInProgress(false);
}
dispatch(getDevicesAction());
}
},
exc => {
setDialogWarning(exc.message);
setInProgress(false);
}
);
};
return (
<Modal
description={_("All devices using this passphrase will be unlocked")}
id={idPrefix}
position="top" variant="small" isOpen onClose={() => onClose()}
>
<ModalHeader
title={_("Unlock encrypted devices")}
/>
<ModalBody>
<Form
onSubmit={e => {
e.preventDefault();
onSubmit();
}}>
{dialogSuccess && <InlineNotification type="info" text={dialogSuccess} />}
<FormGroup fieldId={idPrefix + "-locked-devices"} label={_("Locked devices")}>
<LockedDevices id={idPrefix + "-locked-devices"} lockedEncryptedDevices={lockedEncryptedDevices} />
</FormGroup>
<FormGroup fieldId={idPrefix + "-passphrase"} label={_("Passphrase")}>
<InputGroup>
<InputGroupItem isFill>
<TextInput
isRequired
id={idPrefix + "-passphrase"}
type={passphraseHidden ? "password" : "text"}
aria-label={_("Passphrase")}
value={passphrase}
onChange={(_event, val) => setPassphrase(val)}
/>
</InputGroupItem>
<InputGroupItem>
<Button
variant="control"
onClick={() => setPassphraseHidden(!passphraseHidden)}
aria-label={passphraseHidden ? _("Show passphrase") : _("Hide passphrase")}
>
{passphraseHidden ? <EyeIcon /> : <EyeSlashIcon />}
</Button>
</InputGroupItem>
</InputGroup>
<FormHelper helperText={dialogWarning} variant="warning" />
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
<Button variant="primary" onClick={onSubmit} isAriaDisabled={inProgress} isLoading={inProgress} id={idPrefix + "-submit-btn"}>
{_("Unlock")}
</Button>
<Button variant="secondary" onClick={() => onClose()} id={idPrefix + "-close-btn"}>
{_("Close")}
</Button>
</ModalFooter>
</Modal>
);
};