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 @@
-------------------------------------------------------------------
Tue Mar 17 17:07:58 UTC 2026 - David Diaz <dgonzalez@suse.com>

- Show "Unknown" for missing hardware fields
(gh#agama-project/agama#3294).

-------------------------------------------------------------------
Tue Mar 17 12:15:23 UTC 2026 - Imobach Gonzalez Sosa <igonzalezsosa@suse.com>

Expand Down
103 changes: 103 additions & 0 deletions web/src/components/overview/SystemInformationSection.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* Copyright (c) [2026] 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 { installerRender } from "~/test-utils";
import { useSystem } from "~/hooks/model/system";
import SystemInformationSection from "./SystemInformationSection";

const mockUseSystem: jest.Mock<ReturnType<typeof useSystem>> = jest.fn();

jest.mock("~/hooks/model/system", () => ({
...jest.requireActual("~/hooks/model/system"),
useSystem: () => mockUseSystem(),
}));

jest.mock("~/components/network/FormattedIpsList", () => ({
__esModule: true,
default: () => <span>192.168.1.1</span>,
}));

describe("SystemInformationSection", () => {
describe("when hardware data is available", () => {
beforeEach(() => {
mockUseSystem.mockReturnValue({
hardware: {
model: "ThinkPad X1 Carbon",
cpu: "Intel Core i7",
memory: 16 * 1024 * 1024 * 1024,
},
});
});

it("renders the model", () => {
installerRender(<SystemInformationSection />);
screen.getByText("ThinkPad X1 Carbon");
});

it("renders the CPU", () => {
installerRender(<SystemInformationSection />);
screen.getByText("Intel Core i7");
});

it("renders the formatted memory", () => {
installerRender(<SystemInformationSection />);
screen.getByText("16.00 GiB");
});
});

describe("when hardware data is missing", () => {
beforeEach(() => {
mockUseSystem.mockReturnValue({
hardware: {
model: undefined,
cpu: undefined,
memory: undefined,
},
});
});

it("renders 'Unknown' for each missing hardware field", () => {
installerRender(<SystemInformationSection />);
expect(screen.getAllByText("Unknown")).toHaveLength(3);
});
});

describe("when only some hardware data is missing", () => {
beforeEach(() => {
mockUseSystem.mockReturnValue({
hardware: {
model: "ThinkPad X1 Carbon",
cpu: undefined,
memory: undefined,
},
});
});

it("renders available fields and 'Unknown' for missing ones", () => {
installerRender(<SystemInformationSection />);
screen.getByText("ThinkPad X1 Carbon");
expect(screen.getAllByText("Unknown")).toHaveLength(2);
});
});
});
74 changes: 41 additions & 33 deletions web/src/components/overview/SystemInformationSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,17 @@
*/

import React from "react";
import xbytes from "xbytes";
import { isEmpty } from "radashi";
import {
Card,
CardBody,
CardTitle,
DescriptionList,
DescriptionListDescription,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe there is some logic in naming ... but that symmetry is master piece ;-)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, they (PatternFly) have a DescriptionList for wrapping the native HTML dl component and the rest of components for wrapping expected nodes. I guess they are using the base name as prefix for all these components, resulting in that one for the dd 🤷‍♂️ I would had choice DescriptionListDetails but I guess they have reasons for that naming 😉

DescriptionListGroup,
DescriptionListTerm,
DescriptionListDescription,
Flex,
DescriptionListTermProps,
DescriptionListDescriptionProps,
} from "@patternfly/react-core";
import xbytes from "xbytes";
import FormattedIPsList from "~/components/network/FormattedIpsList";
import NestedContent from "~/components/core/NestedContent";
import { useSystem } from "~/hooks/model/system";
Expand All @@ -43,53 +41,63 @@ import textStyles from "@patternfly/react-styles/css/utilities/Text/text";

type ItemProps = {
/** The label/term for this field */
label: DescriptionListDescriptionProps["children"];
label: string;
/** The value/description for this field */
children: DescriptionListDescriptionProps["children"];
/** Additional props passed to the DescriptionListTerm component */
termProps?: Omit<DescriptionListTermProps, "children">;
/** Additional props passed to the DescriptionListDescription component */
descriptionProps?: Omit<DescriptionListDescriptionProps, "children">;
children: React.ReactNode;
};

/**
* A single item in a `Details` description list.
*
* Wraps a PatternFly `DescriptionListGroup` with `DescriptionListTerm` and
* `DescriptionListDescription`.
* A single item in a `SystemInformationSection` description list.
*/
const Item = ({ label, children, termProps = {}, descriptionProps = {} }: ItemProps) => {
const Item = ({ label, children }: ItemProps) => {
return (
<DescriptionListGroup>
<DescriptionListTerm {...termProps}>{label}</DescriptionListTerm>
<DescriptionListDescription {...descriptionProps}>
<small className={textStyles.textColorSubtle}>{children}</small>
<DescriptionListTerm>{label}</DescriptionListTerm>
<DescriptionListDescription>
<small className={textStyles.textColorSubtle}>
{isEmpty(children) ? _("Unknown") : children}
</small>
</DescriptionListDescription>
</DescriptionListGroup>
);
};

/**
* Displays basic hardware information (model, CPU, memory, IPs) in a card.
*
* Fields with missing or undefined values fall back to "Unknown" rather than
* rendering a blank space.
*
* @note A11y: `DescriptionList` renders a `<dl>/<dt>/<dd>` structure. Screen
* reader support is generally good, with the exception of Safari/VoiceOver on
* macOS where virtual cursor navigation exposes each term and description as
* plain text without list semantics. A table-based alternative was evaluated
* but discarded by now: `<th scope="row">` combined with `dataLabel` might
* caused double label announcement for screen reader users, and the component's
* default white background conflicted with the card's secondary variant
* styling. This is a known limitation to revisit if a better PF alternative
* becomes available.
* @see https://adrianroselli.com/2022/12/brief-note-on-description-list-support.html
*/
export default function SystemInformationSection() {
const { hardware } = useSystem();

return (
<Card variant="secondary">
<CardTitle component="h3">{_("System Information")}</CardTitle>
<CardBody>
<Flex gap={{ default: "gapMd" }} direction={{ default: "column" }}>
<NestedContent margin="mxSm">
<DescriptionList isCompact>
<Item label={_("Model")}>{hardware.model}</Item>
<Item label={_("CPU")}>{hardware.cpu}</Item>
<Item label={_("Memory")}>
{hardware.memory ? xbytes(hardware.memory, { iec: true }) : undefined}
</Item>
<Item label={_("IPs")}>
<FormattedIPsList />
</Item>
</DescriptionList>
</NestedContent>
</Flex>
<NestedContent margin="mxSm">
<DescriptionList isCompact>
<Item label={_("Model")}>{hardware.model}</Item>
<Item label={_("CPU")}>{hardware.cpu}</Item>
<Item label={_("Memory")}>
{hardware.memory ? xbytes(hardware.memory, { iec: true }) : undefined}
</Item>
<Item label={_("IPs")}>
<FormattedIPsList />
</Item>
</DescriptionList>
</NestedContent>
</CardBody>
</Card>
);
Expand Down
Loading