Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a0539b7
web: add wrapper for PatternFly DescriptionList
dgdavid Dec 26, 2025
2c6c27e
web: drop almost everything related to overview
dgdavid Dec 26, 2025
82df203
web: start implementing new overview page
dgdavid Dec 26, 2025
a752ad7
web: add initial network overview summary
dgdavid Dec 28, 2025
53a7eb3
web: add initial software summary for overview revamp
dgdavid Dec 28, 2025
4d9be0f
web: add useProgressTracking hook to avoid code duplication
dgdavid Dec 28, 2025
64dcaba
web: wording improvements
dgdavid Dec 29, 2025
82786d1
web: add missing link in hostname summary
dgdavid Dec 29, 2025
03e3244
web: rework hostname summary display
dgdavid Dec 31, 2025
4086946
web: re-rework Hostname summary display
dgdavid Dec 31, 2025
7341c2a
web: reorder summaries in overview
dgdavid Dec 31, 2025
f407942
web: add inormation about system IPs in summary
dgdavid Dec 31, 2025
bfff62f
web: improve network overview summary
dgdavid Jan 2, 2026
e49a41c
web: add tests for useIpAddresses network hook
dgdavid Jan 5, 2026
f502623
Shorter summary for storage
ancorgs Jan 5, 2026
957da83
web: add test for network FormattedIPsList component
dgdavid Jan 5, 2026
578a378
web: improve the software summary
dgdavid Jan 5, 2026
cecdecb
web: fine-tune unit test
dgdavid Jan 5, 2026
6722b0c
web: make usage of useProgressTracking at storage summary
dgdavid Jan 7, 2026
6d80cea
web: extract common summary layout to a component
dgdavid Jan 7, 2026
698e523
web: add unit tests
dgdavid Jan 7, 2026
57d5390
feat(web): replace installation confirmation popup with a dedicated C…
dgdavid Jan 8, 2026
1dd4df9
web: add missing file and update translation
dgdavid Jan 8, 2026
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
10 changes: 10 additions & 0 deletions web/src/assets/styles/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,16 @@ button:focus-visible {
}
}

span.in-quotes {
&::before {
content: open-quote;
}

&::after {
content: close-quote;
}
}

// FIXME: make ir more generic, if possible, or even without CSS but not
// rendering such a label if "storage instructions" are more than one
.storage-structure:has(> li:nth-child(2)) span.action-text {
Expand Down
103 changes: 103 additions & 0 deletions web/src/components/core/ConfirmPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* Copyright (c) [2025] SUSE LLC
*
* All Rights Reserved.
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 2 of the License, or (at your option)
* any later version.
*
* This program 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 General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, contact SUSE LLC.
*
* To contact SUSE LLC about this file by physical or electronic mail, you may
* find current contact information at www.suse.com.
*/

import React from "react";
import { Navigate } from "react-router";
import { Button, Content, Flex, Split } from "@patternfly/react-core";
import Page from "~/components/core/Page";
import Text from "~/components/core/Text";
import Details from "~/components/core/Details";
import HostnameDetailsItem from "~/components/system/HostnameDetailsItem";
import L10nDetailsItem from "~/components/l10n/L10nDetailsItem";
import StorageDetailsItem from "~/components/storage/StorageDetailsItem";
import NetworkDetailsItem from "~/components/network/NetworkDetailsItem";
import SoftwareDetailsItem from "~/components/software/SoftwareDetailsItem";
import PotentialDataLossAlert from "~/components/storage/PotentialDataLossAlert";
import { startInstallation } from "~/model/manager";
import { useProduct } from "~/hooks/model/config";
import { PRODUCT } from "~/routes/paths";
import { useDestructiveActions } from "~/hooks/use-destructive-actions";
import { _ } from "~/i18n";

export default function ConfirmPage() {
const product = useProduct();
const { actions } = useDestructiveActions();
const hasDestructiveActions = actions.length > 0;

if (!product) {
return <Navigate to={PRODUCT.root} />;
}

// TRANSLATORS: title shown in the confirmation page before
// starting the installation. %s will be replaced with the product name.
const [titleStart, titleEnd] = _("Start %s installation?").split("%s");

return (
<Page>
<Page.Content>
<Flex
direction={{ default: "column" }}
gap={{ default: "gapMd" }}
alignContent={{ default: "alignContentCenter" }}
alignItems={{ default: "alignItemsFlexStart" }}
justifyContent={{ default: "justifyContentCenter" }}
>
<Content component="h1">
{titleStart} <span className="in-quotes">{product.name}</span> {titleEnd}
</Content>
<Content component="p" isEditorial>
{
// TRANSLATORS: Part of the introductory text shown in the confirmation page before
// starting the installation.
_(
"Review the summary below. If anything seems incorrect or you have doubts, go back and adjust the settings before proceeding.",
)
}
</Content>
<PotentialDataLossAlert />
<Details isHorizontal isCompact>
<HostnameDetailsItem withoutLink />
<L10nDetailsItem withoutLink />
<StorageDetailsItem withoutLink />
<NetworkDetailsItem withoutLink />
<SoftwareDetailsItem withoutLink />
</Details>

<Split hasGutter style={{ marginBlock: "2rem" }}>
<Button
size="lg"
variant={hasDestructiveActions ? "danger" : "primary"}
onClick={startInstallation}
>
<Text isBold>
{hasDestructiveActions
? _("Install now with potential data loss")
: _("Install now")}
</Text>
</Button>
<Page.Back size="lg">{_("Go back")}</Page.Back>
</Split>
</Flex>
</Page.Content>
</Page>
);
}
165 changes: 165 additions & 0 deletions web/src/components/core/Details.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/*
* Copyright (c) [2025] SUSE LLC
*
* All Rights Reserved.
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 2 of the License, or (at your option)
* any later version.
*
* This program 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 General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, contact SUSE LLC.
*
* To contact SUSE LLC about this file by physical or electronic mail, you may
* find current contact information at www.suse.com.
*/

import React from "react";
import { screen } from "@testing-library/react";
import { plainRender } from "~/test-utils";
import Details from "./Details";

describe("Details", () => {
describe("basic rendering", () => {
it("renders a PatternFly description list", () => {
const { container } = plainRender(
<Details>
<Details.Item label="Name">John Doe</Details.Item>
</Details>,
);

const descriptionList = container.querySelector("dl");
expect(descriptionList).toBeInTheDocument();
expect(descriptionList.classList).toContain("pf-v6-c-description-list");
});

it("renders multiple items", () => {
plainRender(
<Details>
<Details.Item label="Name">John Doe</Details.Item>
<Details.Item label="Email">john@example.com</Details.Item>
<Details.Item label="Role">Developer</Details.Item>
</Details>,
);

screen.getByText("Name");
screen.getByText("John Doe");
screen.getByText("Email");
screen.getByText("john@example.com");
screen.getByText("Role");
screen.getByText("Developer");
});

it("renders with no items", () => {
const { container } = plainRender(<Details />);

const descriptionList = container.querySelector("dl");
expect(descriptionList).toBeInTheDocument();
expect(descriptionList).toBeEmptyDOMElement();
});
});

describe("Details.Item", () => {
it("renders label and children correctly", () => {
plainRender(
<Details>
<Details.Item label="CPU">AMD Ryzen 7</Details.Item>
</Details>,
);

screen.getByText("CPU");
screen.getByText("AMD Ryzen 7");
});

describe("PatternFly props passthrough", () => {
it("passes props to DescriptionList", () => {
const { container } = plainRender(
<Details isHorizontal isCompact>
<Details.Item label="Name">John</Details.Item>
</Details>,
);

const descriptionList = container.querySelector("dl");
expect(descriptionList).toHaveClass("pf-m-horizontal");
expect(descriptionList).toHaveClass("pf-m-compact");
});
});

describe("termProps and descriptionProps", () => {
it("passes termProps to DescriptionListTerm", () => {
const { container } = plainRender(
<Details>
<Details.Item label="Name" termProps={{ className: "custom-term-class" }}>
John
</Details.Item>
</Details>,
);

const term = container.querySelector("dt");
expect(term).toHaveClass("custom-term-class");
});

it("passes descriptionProps to DescriptionListDescription", () => {
const { container } = plainRender(
<Details>
<Details.Item label="Name" descriptionProps={{ className: "custom-desc-class" }}>
John
</Details.Item>
</Details>,
);

const description = container.querySelector("dd");
expect(description).toHaveClass("custom-desc-class");
});
});
});

describe("Details.StackItem", () => {
it("renders given data within an opinionated flex layout", () => {
const { container } = plainRender(
<Details>
<Details.StackItem
label="Storage"
content={<a href="#/storage">Use device vdd (20 GiB)</a>}
description="Potential data loss"
/>
</Details>,
);

const flexContainer = container.querySelector(
'[class*="pf-v"][class*="-l-flex"][class*=pf-m-column]',
);

screen.getByText("Storage");
screen.getByRole("link", { name: /Use device vdd/i });
const small = container.querySelector("small");
expect(small).toHaveTextContent("Potential data loss");
expect(small).toHaveClass(/pf-v.-u-text-color-subtle/);
});

it("renders skeleton placeholders instead of content when isLoading is true", () => {
const { container } = plainRender(
<Details>
<Details.StackItem
label="Storage"
content={<a href="#/storage">Use device vdd (20 GiB)</a>}
description="Potential data loss"
isLoading
/>
</Details>,
);

expect(screen.queryByRole("link")).not.toBeInTheDocument();
expect(screen.queryByText("Use device vdd (20 GiB)")).not.toBeInTheDocument();
expect(screen.queryByText("Potential data loss")).not.toBeInTheDocument();
const skeletons = container.querySelectorAll('[class*="pf-v"][class*="-c-skeleton"]');
expect(skeletons.length).toBe(2);
});
});
});
Loading
Loading