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
3f75124
Improve zFCP config schema to generate TS types
joseivanlopez Mar 5, 2026
e75b253
Small fixes in DASD hooks
joseivanlopez Mar 5, 2026
a50a0c9
Add model and hooks for zFCP
joseivanlopez Mar 5, 2026
b54cd4b
fix(web): adapt dasd test to latest changes
dgdavid Mar 6, 2026
584b9e9
fix(web): add missing test for zFCP models and hooks
dgdavid Mar 6, 2026
bf6f528
refactor(web): use radashi replaceOrAppend
dgdavid Mar 6, 2026
57db2ca
Small fixes in tests
joseivanlopez Mar 6, 2026
69ab007
Add zFCP hooks for setting controllers and removing devices
joseivanlopez Mar 12, 2026
e131d85
Reimplement zFCP page
joseivanlopez Mar 12, 2026
ed53442
Allow SimpleSelector to receive untranslated strings
joseivanlopez Mar 12, 2026
51564ee
Enable button for configuring zFCP
joseivanlopez Mar 12, 2026
3c36bd6
Alert about zFCP issues in the summary
joseivanlopez Mar 12, 2026
6a3a790
Add zFCP issues to ProposalPage
joseivanlopez Mar 12, 2026
34b404f
Delete old zFCP code
joseivanlopez Mar 12, 2026
8ea64fe
Simplify test expectations
joseivanlopez Mar 12, 2026
2b317b4
Fix component documentation
joseivanlopez Mar 12, 2026
d62ee63
Fix hook
joseivanlopez Mar 12, 2026
6442d7b
Remove leftover
joseivanlopez Mar 12, 2026
accb59b
Use reduce instead of recursive calls
joseivanlopez Mar 12, 2026
78466a0
feat(web): override CSS variable for content link colors
dgdavid Mar 13, 2026
47d9f04
feat(web): improve zFCP link markup in storage summary
dgdavid Mar 13, 2026
5ba594a
reafactor(web): change controllers form markup
dgdavid Mar 13, 2026
5012b26
Fix eslint
joseivanlopez Mar 13, 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
53 changes: 28 additions & 25 deletions rust/agama-lib/share/zfcp.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,31 +17,34 @@
"devices": {
"description": "List of zFCP devices.",
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["channel", "wwpn", "lun"],
"properties": {
"channel": {
"description": "zFCP controller channel id.",
"type": "string",
"examples": ["0.0.fa00"]
},
"wwpn": {
"description": "WWPN of the target port.",
"type": "string",
"examples": ["0x500507630300c562"]
},
"lun": {
"description": "LUN of the SCSI device.",
"type": "string",
"examples": ["0x4010403300000000"]
},
"active": {
"description": "Whether to activate the device.",
"type": "boolean",
"default": true
}
"items": { "$ref": "#/$defs/device" }
}
},
"$defs": {
"device": {
"type": "object",
"additionalProperties": false,
"required": ["channel", "wwpn", "lun"],
"properties": {
"channel": {
"description": "zFCP controller channel id.",
"type": "string",
"examples": ["0.0.fa00"]
},
"wwpn": {
"description": "WWPN of the target port.",
"type": "string",
"examples": ["0x500507630300c562"]
},
"lun": {
"description": "LUN of the SCSI device.",
"type": "string",
"examples": ["0x4010403300000000"]
},
"active": {
"description": "Whether to activate the device.",
"type": "boolean",
"default": true
}
}
}
Expand Down
1 change: 1 addition & 0 deletions web/src/assets/styles/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@
--pf-t--global--background--color--disabled--default: #dcdbdc;
--pf-t--global--border--radius--pill: var(--pf-t--global--border--radius--small);
--pf-t--global--color--brand--clicked: var(--agm-t--color--pine);
--pf-t--global--text--color--link--default: var(--agm-t--color--pine);
}

/*
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/core/SimpleSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import type { TranslatedString } from "~/i18n";
type SimpleSelectorProps = {
label: TranslatedString;
value: string;
options: Record<string, TranslatedString>;
options: Record<string, string>;
onChange: SelectProps["onSelect"];
};

Expand Down
40 changes: 28 additions & 12 deletions web/src/components/overview/StorageSummary.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import {
useFlattenDevices as useProposalFlattenDevices,
useActions,
} from "~/hooks/model/proposal/storage";
import { useIssues } from "~/hooks/model/issue";
import * as issueHooks from "~/hooks/model/issue";
import { STORAGE } from "~/routes/paths";
import StorageSummary from "./StorageSummary";

Expand All @@ -40,7 +40,8 @@ const mockUseDevicesFn: jest.Mock<ReturnType<typeof useDevices>> = jest.fn();
const mockUseProposalFlattenDevicesFn: jest.Mock<ReturnType<typeof useProposalFlattenDevices>> =
jest.fn();
const mockUseActionsFn: jest.Mock<ReturnType<typeof useActions>> = jest.fn();
const mockUseIssuesFn: jest.Mock<ReturnType<typeof useIssues>> = jest.fn();
const mockStorageIssuesFn: jest.Mock<ReturnType<typeof issueHooks.useIssues>> = jest.fn();
const mockZFCPIssuesFn: jest.Mock<ReturnType<typeof issueHooks.useIssues>> = jest.fn();

// Mock all the hooks
jest.mock("~/hooks/model/storage/config-model", () => ({
Expand All @@ -61,10 +62,11 @@ jest.mock("~/hooks/model/proposal/storage", () => ({
useActions: () => mockUseActionsFn(),
}));

jest.mock("~/hooks/model/issue", () => ({
...jest.requireActual("~/hooks/model/issue"),
useIssues: () => mockUseIssuesFn(),
}));
jest.spyOn(issueHooks, "useIssues").mockImplementation((scope) => {
if (scope === "storage") return mockStorageIssuesFn();
if (scope === "zfcp") return mockZFCPIssuesFn();
return [];
});

// Mock device for tests
const mockDevice = {
Expand Down Expand Up @@ -99,7 +101,8 @@ describe("StorageSummary", () => {
mockUseFlattenDevicesFn.mockReturnValue([]);
mockUseProposalFlattenDevicesFn.mockReturnValue([]);
mockUseActionsFn.mockReturnValue([]);
mockUseIssuesFn.mockReturnValue([]);
mockStorageIssuesFn.mockReturnValue([]);
mockZFCPIssuesFn.mockReturnValue([]);
});

afterEach(() => {
Expand Down Expand Up @@ -170,26 +173,39 @@ describe("StorageSummary", () => {
});

it("shows invalid settings warning when config issues exist", () => {
mockUseIssuesFn.mockReturnValue([
mockStorageIssuesFn.mockReturnValue([
{
description: "Fake Issue",
description: "Fake issue",
class: "generic",
details: "Fake Issue details",
details: "Fake issue details",
scope: "storage",
},
]);
installerRender(<StorageSummary />);
screen.getByText("Invalid settings");
});

it("shows invalid ZFCP settings warning when ZFCP issues exist", () => {
mockZFCPIssuesFn.mockReturnValue([
{
description: "Fake issue",
class: "generic",
scope: "zfcp",
},
]);
installerRender(<StorageSummary />);
screen.getByText(/Invalid/);
screen.getByText(/zFCP/);
});

it("shows advanced configuration message when model is unavailable", () => {
mockUseConfigModelFn.mockReturnValue(null);
installerRender(<StorageSummary />);
screen.getByText("Using an advanced storage configuration");
});

it("ignores proposal class issues when checking config validity", () => {
mockUseIssuesFn.mockReturnValue([
mockStorageIssuesFn.mockReturnValue([
{
description: "Fake Issue",
class: "proposal",
Expand Down Expand Up @@ -254,7 +270,7 @@ describe("StorageSummary", () => {
});

it("hides description when config issues exist", () => {
mockUseIssuesFn.mockReturnValue([
mockStorageIssuesFn.mockReturnValue([
{
description: "Fake Issue",
class: "generic",
Expand Down
36 changes: 30 additions & 6 deletions web/src/components/overview/StorageSummary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,12 @@
*/

import React from "react";
import { isEmpty } from "radashi";
import { sprintf } from "sprintf-js";
import { Content } from "@patternfly/react-core";
import Summary from "~/components/core/Summary";
import Link from "~/components/core/Link";
import Text from "~/components/core/Text";
import { useProgressTracking } from "~/hooks/use-progress-tracking";
import { useConfigModel } from "~/hooks/model/storage/config-model";
import {
Expand Down Expand Up @@ -82,16 +85,33 @@ const ModelSummary = ({ model }: { model: ConfigModel.Config }): React.ReactNode
return <SingleDeviceSummary target={targets[0]} />;
};

const InvalidZFCP = (): React.ReactNode => {
// TRANSLATORS: The text in [] is used as a link.
const text = _("Invalid [zFCP] settings");
const [textStart, textLink, textEnd] = text.split(/[[\]]/);
return (
<Content>
{textStart}
<Link to={STORAGE.zfcp.root} variant="link" isInline>
<Text isBold>{textLink}</Text>
</Link>
{textEnd}
</Content>
);
};

const Value = () => {
const availableDevices = useAvailableDevices();
const model = useConfigModel();
const issues = useIssues("storage");
const zfcpIssues = useIssues("zfcp");
const configIssues = issues.filter((i) => i.class !== "proposal");

if (!availableDevices.length) return _("There are no disks available for the installation");
if (configIssues.length) {
if (isEmpty(availableDevices)) return _("There are no disks available for the installation");
if (!isEmpty(configIssues)) {
return _("Invalid settings");
}
if (!isEmpty(zfcpIssues)) return <InvalidZFCP />;

if (!model) return _("Using an advanced storage configuration");

Expand All @@ -103,17 +123,18 @@ const Description = () => {
const staging = useProposalFlattenDevices();
const actions = useActions();
const issues = useIssues("storage");
const zfcpIssues = useIssues("zfcp");
const configIssues = issues.filter((i) => i.class !== "proposal");
const manager = new DevicesManager(system, staging, actions);

if (configIssues.length) return;
if (!actions.length) return _("Failed to calculate a storage layout");
if (!isEmpty(configIssues) || !isEmpty(zfcpIssues)) return;
if (isEmpty(actions)) return _("Failed to calculate a storage layout");

const deleteActions = manager.actions.filter((a) => a.delete && !a.subvol).length;
if (!deleteActions) return _("No data loss is expected");

const systems = manager.deletedSystems();
if (systems.length) {
if (!isEmpty(systems)) {
return sprintf(
// TRANSLATORS: %s will be replaced by a formatted list of affected systems
// like "Windows and openSUSE Tumbleweed".
Expand Down Expand Up @@ -155,7 +176,10 @@ const Description = () => {
*/
export default function StorageSummary() {
const { loading } = useProgressTracking("storage");
const hasIssues = !!useIssues("storage").length;
const issues = useIssues("storage");
const zfcpIssues = useIssues("zfcp");

const hasIssues = !isEmpty(issues) || !isEmpty(zfcpIssues);

return (
<Summary
Expand Down
18 changes: 10 additions & 8 deletions web/src/components/storage/ConnectedDevicesMenu.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ jest.mock("~/hooks/model/system/dasd", () => ({
useSystem: () => mockUseDASDSystem(),
}));

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

async function openMenu() {
const { user } = installerRender(<ConnectedDevicesMenu />);
const button = screen.getByRole("button", { name: "More storage options" });
Expand Down Expand Up @@ -68,11 +74,9 @@ it("allows users to configure iSCSI", async () => {
});

describe("if zFCP is not supported", () => {
/*
beforeEach(() => {
mockUseZFCPSupported.mockReturnValue(false);
mockUseZFCPSystem.mockReturnValue(null);
});
*/

it("does not allow users to configure zFCP", async () => {
const { menu } = await openMenu();
Expand All @@ -81,12 +85,10 @@ describe("if zFCP is not supported", () => {
});
});

describe.skip("if zFCP is supported", () => {
/*
describe("if zFCP is supported", () => {
beforeEach(() => {
mockUseZFCPSupported.mockReturnValue(true);
mockUseZFCPSystem.mockReturnValue({});
});
*/

it("allows users to configure zFCP", async () => {
const { user, menu } = await openMenu();
Expand All @@ -98,7 +100,7 @@ describe.skip("if zFCP is supported", () => {

describe("if DASD is not supported", () => {
beforeEach(() => {
mockUseDASDSystem.mockReturnValue(undefined);
mockUseDASDSystem.mockReturnValue(null);
});

it("does not allow users to configure DASD", async () => {
Expand Down
5 changes: 3 additions & 2 deletions web/src/components/storage/ConnectedDevicesMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,13 @@ import { STORAGE } from "~/routes/paths";
import Icon from "~/components/layout/Icon";
import MenuButton from "~/components/core/MenuButton";
import { useSystem as useDASDSystem } from "~/hooks/model/system/dasd";
import { useSystem as useZFCPSystem } from "~/hooks/model/system/zfcp";
import { _ } from "~/i18n";

export default function ConnectedDevicesMenu() {
const navigate = useNavigate();
const isZFCPSupported = false;
const dasdSystem = useDASDSystem();
const zfcpSystem = useZFCPSystem();

return (
<MenuButton
Expand All @@ -55,7 +56,7 @@ export default function ConnectedDevicesMenu() {
>
{_("Configure iSCSI")}
</MenuButton.Item>,
isZFCPSupported && (
zfcpSystem && (
<MenuButton.Item
key="zfcp-link"
onClick={() => navigate(STORAGE.zfcp.root)}
Expand Down
22 changes: 14 additions & 8 deletions web/src/components/storage/ProposalPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ const mockUseConfigModel = jest.fn();
const mockUseProposal = jest.fn();
const mockUseIssues = jest.fn();
const mockUseDASDSystem = jest.fn();
const mockUseZFCPSystem = jest.fn();

jest.mock("~/hooks/model/system/storage", () => ({
...jest.requireActual("~/hooks/model/system/storage"),
Expand Down Expand Up @@ -96,6 +97,11 @@ jest.mock("~/hooks/model/system/dasd", () => ({
useSystem: () => mockUseDASDSystem(),
}));

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

jest.mock("./ProposalFailedInfo", () => () => <div>proposal failed info</div>);
jest.mock("./UnsupportedModelInfo", () => () => <div>unsupported model info</div>);
jest.mock("./FixableConfigInfo", () => () => <div>fixable config info</div>);
Expand Down Expand Up @@ -133,9 +139,9 @@ describe("if there are no devices", () => {
});

describe("if zFCP is not supported", () => {
// beforeEach(() => {
// mockUseZFCPSupported.mockReturnValue(false);
// });
beforeEach(() => {
mockUseZFCPSystem.mockReturnValue(null);
});

it("does not render an option for activating zFCP", () => {
installerRender(<ProposalPage />);
Expand All @@ -145,7 +151,7 @@ describe("if there are no devices", () => {

describe("if DASD is not supported", () => {
beforeEach(() => {
mockUseDASDSystem.mockReturnValue(undefined);
mockUseDASDSystem.mockReturnValue(null);
});

it("does not render an option for activating DASD", () => {
Expand All @@ -154,10 +160,10 @@ describe("if there are no devices", () => {
});
});

describe.skip("if zFCP is supported", () => {
// beforeEach(() => {
// mockUseZFCPSupported.mockReturnValue(true);
// });
describe("if zFCP is supported", () => {
beforeEach(() => {
mockUseZFCPSystem.mockReturnValue({});
});

it("renders an option for activating zFCP", () => {
installerRender(<ProposalPage />);
Expand Down
Loading
Loading