Skip to content

Commit

Permalink
[web] Code improvements for InstallationFinished page
Browse files Browse the repository at this point in the history
In spite of the InstallationPage needs to be reworked a bit more (and
possibly to include a loading internal state or similar), this commit
introduces minor code improvements that were cheaper to apply right now.

First of all, the TPM reminder is only shown if encryption was set.

Additionally, the reminder is improved to avoid it changing the width
when the user expands it. Done by relaying in CSS minmax function and
CSS container queries. Both with wide support of modern browsers.

This commit also stops centering vertically the page content because
the very same reason: having dinamic components make things moving
top / down depending on their state.

It also moves the TPM id to an enum created in the client/storage for
the encryption methods, following the same approach as network code.
It can be moved to an specific file like it is already done in
client/phase or client/status, though. Something to be defined/polished
in next iterations.

[1] https://developer.mozilla.org/es/docs/Web/CSS/minmax
[2] https://developer.mozilla.org/es/docs/Web/CSS/CSS_container_queries
  • Loading branch information
dgdavid authored and ancorgs committed Jan 17, 2024
1 parent 3de37a3 commit a7e4b60
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 61 deletions.
5 changes: 5 additions & 0 deletions web/src/assets/styles/blocks.scss
Original file line number Diff line number Diff line change
Expand Up @@ -430,3 +430,8 @@ ul[data-of="agama/timezones"] {
}
}
}

.tpm-hint {
grid-template-columns: minmax(100%, 70cqw);
text-align: start;
}
2 changes: 2 additions & 0 deletions web/src/assets/styles/layout.scss
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@
grid-area: body;
overflow: auto;
padding-block: var(--spacer-normal);
container-type: inline-size;
container-name: agama-page-content;
}

footer {
Expand Down
15 changes: 13 additions & 2 deletions web/src/client/storage.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) [2022-2023] SUSE LLC
* Copyright (c) [2022-2024] SUSE LLC
*
* All Rights Reserved.
*
Expand Down Expand Up @@ -46,6 +46,17 @@ const ZFCP_CONTROLLER_IFACE = "org.opensuse.Agama.Storage1.ZFCP.Controller";
const ZFCP_DISKS_NAMESPACE = "/org/opensuse/Agama/Storage1/zfcp_disks";
const ZFCP_DISK_IFACE = "org.opensuse.Agama.Storage1.ZFCP.Disk";

/**
* Enum for the encryption method values
*
* @readonly
* @enum { string }
*/
const EncryptionMethods = Object.freeze({
LUKS2: "luks2",
TPM: "tpm_fde"
});

/**
* Removes properties with undefined value
*
Expand Down Expand Up @@ -1433,4 +1444,4 @@ class StorageClient extends WithIssues(
), STORAGE_OBJECT
) { }

export { StorageClient };
export { StorageClient, EncryptionMethods };
85 changes: 38 additions & 47 deletions web/src/components/core/InstallationFinished.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) [2022] SUSE LLC
* Copyright (c) [2022-2024] SUSE LLC
*
* All Rights Reserved.
*
Expand Down Expand Up @@ -31,16 +31,17 @@ import {
HintBody,
} from "@patternfly/react-core";

import { Page, If } from "~/components/core";
import { Center, Icon } from "~/components/layout";
import { If, Page } from "~/components/core";
import { Icon } from "~/components/layout";
import { useInstallerClient } from "~/context/installer";
import { EncryptionMethods } from "~/client/storage";
import { _ } from "~/i18n";

const TpmHint = () => {
const [isExpanded, setIsExpanded] = useState(false);

return (
<Hint>
<Hint className="tpm-hint">
<HintBody>
<ExpandableSection
isExpanded={isExpanded}
Expand All @@ -64,63 +65,53 @@ const SuccessIcon = () => <Icon name="check_circle" className="icon-xxxl color-s

function InstallationFinished() {
const client = useInstallerClient();
const [iguana, setIguana] = useState(false);
const [tpm, setTpm] = useState(false);
const [usingIguana, setUsingIguana] = useState(false);
const [usingTpm, setUsingTpm] = useState(false);
const closingAction = () => client.manager.finishInstallation();
const buttonCaption = iguana
// TRANSLATORS: button label
? _("Finish")
// TRANSLATORS: button label
: _("Reboot");

useEffect(() => {
async function getIguana() {
const ret = await client.manager.useIguana();
setIguana(ret);
async function preparePage() {
const iguana = await client.manager.useIguana();
// FIXME: This logic should likely not be placed here, it's too coupled to storage internals.
// Something to fix when this whole page is refactored in a (hopefully near) future.
const { settings: { encryptionPassword, encryptionMethod } } = await client.storage.proposal.getResult();
setUsingIguana(iguana);
setUsingTpm(encryptionPassword?.length && encryptionMethod === EncryptionMethods.TPM);
}

// FIXME: This logic should likely not be placed here, it's too complex and too coupled to storage internals.
// Something to fix when this whole page is refactored in a (hopefully near) future.
async function getTpm() {
const result = await client.storage.proposal.getResult();
const method = result.settings.encryptionMethod;
const tpmId = "tpm_fde";
setTpm(method === tpmId);
}

getIguana();
getTpm();
// TODO: display the page in a loading mode while needed data is being fetched.
preparePage();
});

return (
// TRANSLATORS: page title
<Page icon="task_alt" title={_("Installation Finished")}>
<Center>
<EmptyState variant="xl">
<EmptyStateHeader
titleText={_("Congratulations!")}
headingLevel="h2"
icon={<EmptyStateIcon icon={SuccessIcon} />}
/>
<EmptyStateBody>
<Text>{_("The installation on your machine is complete.")}</Text>
<Text>
{
iguana
? _("At this point you can power off the machine.")
: _("At this point you can reboot the machine to log in to the new system.")
}
</Text>
<EmptyState variant="xl">
<EmptyStateHeader
titleText={_("Congratulations!")}
headingLevel="h2"
icon={<EmptyStateIcon icon={SuccessIcon} />}
/>
<EmptyStateBody>
<Text>{_("The installation on your machine is complete.")}</Text>
<Text>
<If
condition={tpm}
then={<TpmHint />}
condition={usingIguana}
then={_("At this point you can power off the machine.")}
else={_("At this point you can reboot the machine to log in to the new system.")}
/>
</EmptyStateBody>
</EmptyState>
</Center>
</Text>
<If
condition={usingTpm}
then={<TpmHint />}
/>
</EmptyStateBody>
</EmptyState>

<Page.Actions>
<Page.Action onClick={closingAction}>{buttonCaption}</Page.Action>
<Page.Action onClick={closingAction}>
{usingIguana ? _("Finish") : _("Reboot")}
</Page.Action>
</Page.Actions>
</Page>
);
Expand Down
54 changes: 42 additions & 12 deletions web/src/components/core/InstallationFinished.test.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) [2022] SUSE LLC
* Copyright (c) [2022-2024] SUSE LLC
*
* All Rights Reserved.
*
Expand All @@ -24,18 +24,21 @@ import React from "react";
import { screen } from "@testing-library/react";
import { installerRender } from "~/test-utils";
import { createClient } from "~/client";
import { EncryptionMethods } from "~/client/storage";

import InstallationFinished from "./InstallationFinished";

jest.mock("~/client");
jest.mock("~/components/core/Sidebar", () => () => <div>Agama sidebar</div>);

const finishInstallationFn = jest.fn();
let encryptionPassword;
let encryptionMethod;

describe("InstallationFinished", () => {
beforeEach(() => {
encryptionMethod = "luks2";
encryptionPassword = "n0tS3cr3t";
encryptionMethod = EncryptionMethods.LUKS2;
createClient.mockImplementation(() => {
return {
manager: {
Expand All @@ -44,7 +47,9 @@ describe("InstallationFinished", () => {
},
storage: {
proposal: {
getResult: jest.fn().mockResolvedValue({ settings: { encryptionMethod } })
getResult: jest.fn().mockResolvedValue({
settings: { encryptionMethod, encryptionPassword }
})
},
}
};
Expand All @@ -68,21 +73,46 @@ describe("InstallationFinished", () => {
expect(finishInstallationFn).toHaveBeenCalled();
});

describe("when TPM was set to decrypt automatically on each boot", () => {
describe("when TPM is set as encryption method", () => {
beforeEach(() => {
encryptionMethod = "tpm_fde";
encryptionMethod = EncryptionMethods.TPM;
});

it("shows the TPM reminder", async () => {
installerRender(<InstallationFinished />);
await screen.findAllByText(/TPM/);
describe("and encryption was set", () => {
it("shows the TPM reminder", async () => {
installerRender(<InstallationFinished />);
await screen.findAllByText(/TPM/);
});
});

describe("but encryption was not set", () => {
beforeEach(() => {
encryptionPassword = "";
});

it("does not show the TPM reminder", async () => {
const { user } = installerRender(<InstallationFinished />);
// Forcing the test to slow down a bit with a fake user interaction
// because actually the reminder will be not rendered immediately
// making the queryAllByText to produce a false positive if triggered
// too early here.
const congratsText = screen.getByText("Congratulations!");
await user.click(congratsText);
expect(screen.queryAllByText(/TPM/)).toHaveLength(0);
});
});
});

describe("when TPM was not set to decrypt automatically on each boot", () => {
it("does not show the TPM reminder", () => {
installerRender(<InstallationFinished />);
expect(screen.queryByText(/TPM/)).toBeNull();
describe("when TPM is not set as encryption method", () => {
it("does not show the TPM reminder", async () => {
const { user } = installerRender(<InstallationFinished />);
// Forcing the test to slow down a bit with a fake user interaction
// because actually the reminder will be not rendered immediately
// making the queryAllByText to produce a false positive if triggered
// too early here.
const congratsText = screen.getByText("Congratulations!");
await user.click(congratsText);
expect(screen.queryAllByText(/TPM/)).toHaveLength(0);
});
});
});

0 comments on commit a7e4b60

Please sign in to comment.