Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
-------------------------------------------------------------------
Wed Mar 26 08:25:34 UTC 2025 - David Diaz <[email protected]>

- Change switches by checkboxes (gh#agama-project/agama#2168).
- Restructure the encryption form (gh#agama-project/agama#2168).

-------------------------------------------------------------------
Mon Mar 24 10:17:30 UTC 2025 - Imobach Gonzalez Sosa <[email protected]>

Expand Down
6 changes: 6 additions & 0 deletions web/src/assets/styles/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,12 @@ label.pf-m-disabled + .pf-v6-c-check__description {
justify-self: flex-start;
}

// Nested content inside forms should respect parent grid gap
.pf-v6-c-form [class*="pf-v6-u-mx"] {
display: grid;
gap: inherit;
}

// Some utilities not found at PF
.w-14ch {
inline-size: 14ch;
Expand Down
42 changes: 16 additions & 26 deletions web/src/components/storage/EncryptionSettingsPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
*/

import React from "react";
import { screen, fireEvent } from "@testing-library/react";
import { screen } from "@testing-library/react";
import { installerRender } from "~/test-utils";
import EncryptionSettingsPage from "./EncryptionSettingsPage";
import { EncryptionHook } from "~/queries/storage/config-model";
Expand Down Expand Up @@ -78,13 +78,13 @@ describe("EncryptionSettingsPage", () => {

it("allows enabling the encryption", async () => {
const { user } = installerRender(<EncryptionSettingsPage />);
const toggle = screen.getByRole("switch", { name: "Encrypt the system" });
expect(toggle).not.toBeChecked();
await user.click(toggle);
const encryptionCheckbox = screen.getByRole("checkbox", { name: "Encrypt the system" });
expect(encryptionCheckbox).not.toBeChecked();
await user.click(encryptionCheckbox);
const passwordInput = screen.getByLabelText("Password");
const passwordConfirmationInput = screen.getByLabelText("Password confirmation");
fireEvent.change(passwordInput, { target: { value: "12345" } });
fireEvent.change(passwordConfirmationInput, { target: { value: "12345" } });
await user.type(passwordInput, "12345");
await user.type(passwordConfirmationInput, "12345");
const acceptButton = screen.getByRole("button", { name: "Accept" });
await user.click(acceptButton);
expect(mockNoEncryption.enable).toHaveBeenCalledWith("luks2", "12345");
Expand All @@ -96,25 +96,15 @@ describe("EncryptionSettingsPage", () => {
mockUseEncryption.mockReturnValue(mockLuks2Encryption);
});

describe("and user chooses to not use encryption", () => {
it("allows disabling the encryption", async () => {
const { user } = installerRender(<EncryptionSettingsPage />);
const toggle = screen.getByRole("switch", { name: "Encrypt the system" });
expect(toggle).toBeChecked();
await user.click(toggle);
const passwordInput = screen.getByLabelText("Password");
const passwordConfirmationInput = screen.getByLabelText("Password confirmation");
const tpmCheckbox = screen.getByRole("checkbox", { name: /Use.*TPM/ });

expect(passwordInput).toBeDisabled();
expect(passwordConfirmationInput).toBeDisabled();
expect(tpmCheckbox).toBeDisabled();

const acceptButton = screen.getByRole("button", { name: "Accept" });
await user.click(acceptButton);

expect(mockLuks2Encryption.disable).toHaveBeenCalled();
});
it("allows disabling the encryption", async () => {
const { user } = installerRender(<EncryptionSettingsPage />);
const encryptionCheckbox = screen.getByRole("checkbox", { name: "Encrypt the system" });
expect(encryptionCheckbox).toBeChecked();
await user.click(encryptionCheckbox);
const acceptButton = screen.getByRole("button", { name: "Accept" });
await user.click(acceptButton);

expect(mockLuks2Encryption.disable).toHaveBeenCalled();
});
});

Expand Down Expand Up @@ -142,7 +132,7 @@ describe("EncryptionSettingsPage", () => {

it("does not offer TPM", () => {
installerRender(<EncryptionSettingsPage />);
expect(screen.queryByRole("checkbox", { name: /Use.*TPM/ })).not.toBeInTheDocument();
expect(screen.queryByRole("checkbox", { name: /Use.*TPM/ })).toBeNull();
});
});
});
64 changes: 32 additions & 32 deletions web/src/components/storage/EncryptionSettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@
* find current contact information at www.suse.com.
*/

import React, { useState, useRef } from "react";
import React, { useEffect, useState, useRef } from "react";
import { useNavigate } from "react-router-dom";
import { ActionGroup, Alert, Checkbox, Content, Form, Switch } from "@patternfly/react-core";
import { Page, PasswordAndConfirmationInput } from "~/components/core";
import { ActionGroup, Alert, Checkbox, Content, Form } from "@patternfly/react-core";
import { NestedContent, Page, PasswordAndConfirmationInput } from "~/components/core";
import { useEncryptionMethods } from "~/queries/storage";
import { useEncryption } from "~/queries/storage/config-model";
import { EncryptionMethod } from "~/api/storage/types/config-model";
Expand All @@ -46,7 +46,7 @@ export default function EncryptionSettingsPage() {
const passwordRef = useRef<HTMLInputElement>();
const formId = "encryptionSettingsForm";

React.useEffect(() => {
useEffect(() => {
if (encryptionConfig) {
setIsEnabled(true);
setMethod(encryptionConfig.method);
Expand Down Expand Up @@ -86,12 +86,10 @@ export default function EncryptionSettingsPage() {
};

// TRANSLATORS: "Trusted Platform Module" is the name of the technology and TPM its abbreviation
const tpm_label = _(
"Use the Trusted Platform Module (TPM) to decrypt automatically on each boot",
);
const tpmLabel = _("Use the Trusted Platform Module (TPM) to decrypt automatically on each boot");
// TRANSLATORS: The word 'directly' is key here. For example, booting to the installer media and then choosing
// 'Boot from Hard Disk' from there will not work. Keep it sort (this is a hint in a form) but keep it clear.
const tpm_explanation = _(
const tpmExplanation = _(
"The password will not be needed to boot and access the data if the \
TPM can verify the integrity of the system. TPM sealing requires the new system to be booted \
directly on its first run.",
Expand All @@ -103,12 +101,6 @@ directly on its first run.",
<Page>
<Page.Header>
<Content component="h2">{_("Encryption settings")}</Content>
<Content component="small">
{_(
"Full Disk Encryption (FDE) allows to protect the information stored \
at the new file systems, including data, programs, and system files.",
)}
</Content>
</Page.Header>

<Page.Content>
Expand All @@ -120,28 +112,36 @@ at the new file systems, including data, programs, and system files.",
))}
</Alert>
)}
<Switch
<Checkbox
id="encryption"
label={_("Encrypt the system")}
description={_(
"Full Disk Encryption (FDE) allows to protect the information stored \
at the new file systems, including data, programs, and system files.",
)}
isChecked={isEnabled}
onChange={() => setIsEnabled(!isEnabled)}
/>
<PasswordAndConfirmationInput
inputRef={passwordRef}
initialValue={encryptionConfig?.password}
value={password}
onChange={changePassword}
isDisabled={!isEnabled}
showErrors={false}
/>
{isTpmAvailable && (
<Checkbox
id="tpm_encryption_method"
label={tpm_label}
description={tpm_explanation}
isChecked={method === "tpmFde"}
isDisabled={!isEnabled}
onChange={changeMethod}
/>
{isEnabled && (
<NestedContent margin="mxLg">
<PasswordAndConfirmationInput
inputRef={passwordRef}
initialValue={encryptionConfig?.password}
value={password}
onChange={changePassword}
isDisabled={!isEnabled}
showErrors={false}
/>
{isTpmAvailable && (
<Checkbox
id="tpmEncryptionMethod"
label={tpmLabel}
description={tpmExplanation}
isChecked={method === "tpmFde"}
onChange={changeMethod}
/>
)}
</NestedContent>
)}
<ActionGroup>
<Page.Submit form={formId} />
Expand Down
12 changes: 6 additions & 6 deletions web/src/components/users/RootUserForm.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ describe("RootUserForm", () => {

it("allows clearing the password", async () => {
const { user } = installerRender(<RootUserForm />);
const passwordToggle = screen.getByRole("switch", { name: "Use password" });
const passwordToggle = screen.getByRole("checkbox", { name: "Use password" });
const acceptButton = screen.getByRole("button", { name: "Accept" });
expect(passwordToggle).toBeChecked();
await user.click(passwordToggle);
Expand All @@ -111,7 +111,7 @@ describe("RootUserForm", () => {

it("allows setting a public SSH Key ", async () => {
const { user } = installerRender(<RootUserForm />);
const sshPublicKeyToggle = screen.getByRole("switch", { name: "Use public SSH Key" });
const sshPublicKeyToggle = screen.getByRole("checkbox", { name: "Use public SSH Key" });
const acceptButton = screen.getByRole("button", { name: "Accept" });
await user.click(sshPublicKeyToggle);
const sshPublicKeyInput = screen.getByRole("textbox", { name: "File upload" });
Expand All @@ -126,7 +126,7 @@ describe("RootUserForm", () => {

it("does not allow setting an empty public SSH Key", async () => {
const { user } = installerRender(<RootUserForm />);
const sshPublicKeyToggle = screen.getByRole("switch", { name: "Use public SSH Key" });
const sshPublicKeyToggle = screen.getByRole("checkbox", { name: "Use public SSH Key" });
const acceptButton = screen.getByRole("button", { name: "Accept" });
await user.click(sshPublicKeyToggle);
expect(sshPublicKeyToggle).toBeChecked();
Expand All @@ -139,7 +139,7 @@ describe("RootUserForm", () => {
it("allows clearing the public SSH Key", async () => {
mockPublicKey = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDM+ test@example";
const { user } = installerRender(<RootUserForm />);
const sshPublicKeyToggle = screen.getByRole("switch", { name: "Use public SSH Key" });
const sshPublicKeyToggle = screen.getByRole("checkbox", { name: "Use public SSH Key" });
const acceptButton = screen.getByRole("button", { name: "Accept" });
expect(sshPublicKeyToggle).toBeChecked();
await user.click(sshPublicKeyToggle);
Expand All @@ -158,7 +158,7 @@ describe("RootUserForm", () => {

it("allows preserving it", async () => {
const { user } = installerRender(<RootUserForm />);
const passwordToggle = screen.getByRole("switch", { name: "Use password" });
const passwordToggle = screen.getByRole("checkbox", { name: "Use password" });
const acceptButton = screen.getByRole("button", { name: "Accept" });
expect(passwordToggle).toBeChecked();
screen.getByText("Using a hashed password.");
Expand All @@ -170,7 +170,7 @@ describe("RootUserForm", () => {

it("allows discarding it", async () => {
const { user } = installerRender(<RootUserForm />);
const passwordToggle = screen.getByRole("switch", { name: "Use password" });
const passwordToggle = screen.getByRole("checkbox", { name: "Use password" });
const acceptButton = screen.getByRole("button", { name: "Accept" });
expect(passwordToggle).toBeChecked();
await user.click(passwordToggle);
Expand Down
88 changes: 40 additions & 48 deletions web/src/components/users/RootUserForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,14 @@ import {
ActionGroup,
Alert,
Button,
Card,
CardBody,
Checkbox,
Content,
FileUpload,
Form,
FormGroup,
Switch,
} from "@patternfly/react-core";
import { useNavigate } from "react-router-dom";
import { Page, PasswordAndConfirmationInput } from "~/components/core";
import { NestedContent, Page, PasswordAndConfirmationInput } from "~/components/core";
import { useRootUser, useRootUserMutation } from "~/queries/users";
import { RootUser } from "~/types/users";
import { isEmpty } from "~/utils";
Expand Down Expand Up @@ -148,53 +146,47 @@ const RootUserForm = () => {
))}
</Alert>
)}
<FormGroup
label={
<Switch
label={_("Use password")}
isChecked={activeMethods.password}
onChange={() => toggleMethod("password")}
/>
}
>
{activeMethods.password && (
<Card isPlain isCompact>
<CardBody>
{usingHashedPassword ? (
<Content isEditorial>
{_("Using a hashed password.")}{" "}
<Button variant="link" isInline onClick={() => setUsingHashedPassword(false)}>
{_("Change")}
</Button>
</Content>
) : (
<PasswordAndConfirmationInput
inputRef={passwordRef}
value={password}
onChange={onPasswordChange}
showErrors={false}
/>
)}
</CardBody>
</Card>
)}
<FormGroup>
<Checkbox
id="setPassword"
label={_("Use password")}
isChecked={activeMethods.password}
onChange={() => toggleMethod("password")}
/>
</FormGroup>
{activeMethods.password && (
<NestedContent margin="mxLg">
{usingHashedPassword ? (
<Content isEditorial>
{_("Using a hashed password.")}{" "}
<Button variant="link" isInline onClick={() => setUsingHashedPassword(false)}>
{_("Change")}
</Button>
</Content>
) : (
<PasswordAndConfirmationInput
inputRef={passwordRef}
value={password}
onChange={onPasswordChange}
showErrors={false}
/>
)}
</NestedContent>
)}

<FormGroup
label={
<Switch
label={_("Use public SSH Key")}
isChecked={activeMethods.sshPublicKey}
onChange={() => toggleMethod("sshPublicKey")}
/>
}
>
<FormGroup>
<Checkbox
id="setSSHKey"
label={_("Use public SSH Key")}
isChecked={activeMethods.sshPublicKey}
onChange={() => toggleMethod("sshPublicKey")}
/>
</FormGroup>
<FormGroup>
{activeMethods.sshPublicKey && (
<Card isPlain isCompact>
<CardBody>
<SSHKeyField value={sshkey} onChange={setSshKey} />
</CardBody>
</Card>
<NestedContent margin="mxLg">
<SSHKeyField value={sshkey} onChange={setSshKey} />
</NestedContent>
)}
</FormGroup>

Expand Down
Loading