Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
e5ed127
web: make Page.Submit#form prop optional
dgdavid Jan 22, 2026
f3ee603
web: improvements for core/SelectableDataTable
dgdavid Jan 22, 2026
3b3667f
WIP: draft towards a revamped iSCSI interface
dgdavid Jan 22, 2026
0428510
web: make SwitchEnhanced label clickable as a form label
dgdavid Jan 22, 2026
d29309a
web: add an util for merge collections and tracking object sources
dgdavid Jan 22, 2026
59766ba
web: wip: missing files
dgdavid Jan 22, 2026
d4cd6db
Move software openapi types
joseivanlopez Jan 29, 2026
1208ed4
Add schema for iSCSI system
joseivanlopez Jan 29, 2026
fa3498f
Add model and hooks for iSCSI
joseivanlopez Jan 29, 2026
06b32dd
Add iSCSI discover action
joseivanlopez Jan 29, 2026
f1c54da
WIP: fixing stuff in UI
ancorgs Feb 2, 2026
2bf33c7
chore(web): silence another SCSS deprecation warning
dgdavid Feb 2, 2026
6371442
chore(web): stop silencing Sass import deprecation
dgdavid Feb 2, 2026
4e162f4
chore(web): finish fixing TS complains
dgdavid Feb 2, 2026
91463a9
chore(web): please SwitchEnhanced unit test
dgdavid Feb 2, 2026
c6b418d
web: fine-tuning heading size in breadcrumb
dgdavid Feb 2, 2026
4d807fe
web: fix broken tests
dgdavid Feb 3, 2026
62026bd
Fix iSCSI system schema and type
joseivanlopez Feb 3, 2026
7df22d9
WIP: Better data origin for TargetsTable
ancorgs Feb 3, 2026
b11ca79
WIP: Progress in the iSCSI page
ancorgs Feb 3, 2026
784150a
WIP: make discovery work
ancorgs Feb 3, 2026
5947412
WIP: Make login work
ancorgs Feb 3, 2026
871c1be
web: wip: allow deleting iscsi target from config
dgdavid Feb 3, 2026
b342ceb
web: change heading level of ResourceNotFound
dgdavid Feb 4, 2026
eb0296a
web: fine-tune and tests iSCSI target login page
dgdavid Feb 4, 2026
4d52a82
web: fine-tune and test iSCSI discover page
dgdavid Feb 4, 2026
6c6083d
web: adapt utils#mergeSources to allow multi-field primary key
dgdavid Feb 4, 2026
29edd0e
web: refinements related to initiator section
dgdavid Feb 4, 2026
5952ebe
web: drop dead code
dgdavid Feb 4, 2026
929e3ef
web: move startup options to TargetsLoginPage
dgdavid Feb 5, 2026
76e7763
web: minor changes
dgdavid Feb 5, 2026
889fc37
Fix connect and disconnect actions
ancorgs Feb 5, 2026
5068726
web: fine-tune and tests iSCSI targets table
dgdavid Feb 5, 2026
2725ab3
web: adapt test after rebasing changes
dgdavid Feb 5, 2026
722088f
Fix
ancorgs Feb 5, 2026
93c4996
Fix bug when index is zero
ancorgs Feb 5, 2026
675fb66
refactor(web): decouple buildActions from API and navigation logic
dgdavid Feb 5, 2026
86c82bc
fix(web): adapt broken TargetLoginPage test
dgdavid Feb 5, 2026
8425855
Merge branch 'master' into revam-iscsi-ui-draft
dgdavid Feb 5, 2026
f1ae615
web: add "Connection failed" status for iSCSI target
dgdavid Feb 6, 2026
3d57526
web: conditionally show the iBFT column
dgdavid Feb 6, 2026
e4a7db2
web: pre-populates target login form with existing target info
dgdavid Feb 6, 2026
3db7b8a
web: add "Missing" status and filter
dgdavid Feb 6, 2026
6c11c14
Fix and simplify iscsi.ts
ancorgs Feb 6, 2026
8ec71db
Sort functions to please eslint
ancorgs Feb 6, 2026
374697f
feat(web): add support for sibling menus on breadcrumb items
dgdavid Feb 9, 2026
ed21288
fix(web): bring back the storage options menu
dgdavid Feb 9, 2026
f956384
web: drop dead code
dgdavid Feb 9, 2026
52fda65
web: refine iSCSI target status classification and filtering
dgdavid Feb 10, 2026
fb53d2b
Properly serialize ISCSI scope
joseivanlopez Feb 10, 2026
6615e32
fix(web): pass down all breadcrumb props from header
dgdavid Feb 10, 2026
7cca1fd
feat(web): allow rendering custom/page menu in header
dgdavid Feb 10, 2026
05b41bd
fix(web): rename InstallerOptions to InstallerL10nOptions
dgdavid Feb 10, 2026
f8b601b
refactor(web): introduce slots for Header
dgdavid Feb 10, 2026
64ac5c2
refactor(web): minor changes in "ConnectedDevicesMenu"
dgdavid Feb 10, 2026
7bc5e6f
fix(web): rellocate Storage menu in the Header#centerSlot
dgdavid Feb 10, 2026
f1838dd
Merge branch 'master' into revam-iscsi-ui-draft
dgdavid Feb 10, 2026
033e7bc
fix(web): reduce page title font size
dgdavid Feb 10, 2026
f0e692d
web: drop dead code
dgdavid Feb 11, 2026
918ba30
fix(web): prevent default slots content in installation finished
dgdavid Feb 11, 2026
3d0ff10
fix(web): use minimal variant for InstallationFinished
dgdavid Feb 11, 2026
ad9ff30
fix(web): show label of options menu in InstallationFinished
dgdavid Feb 11, 2026
500cdb6
refactor(web): display Overview options in sidebar
dgdavid Feb 11, 2026
eaa173a
Improve iSCSI status filter
ancorgs Feb 11, 2026
a9b885b
Reorganize header buttons again
ancorgs Feb 11, 2026
f18ee75
Adapt progress tracking to recent change in backend
ancorgs Feb 11, 2026
3a0881c
web: Small changes in TargetsTable
ancorgs Feb 11, 2026
84e7a67
web: Better strings for iSCSI authentication
ancorgs Feb 11, 2026
25392f8
web: Fix test for connectedDevicesMenu
ancorgs Feb 11, 2026
b4a6df2
doc(web): add entry in changes file
dgdavid Feb 12, 2026
99930c0
Merge branch 'master' into revam-iscsi-ui-draft
dgdavid Feb 12, 2026
50046af
Merge branch 'master' into revam-iscsi-ui-draft
dgdavid Feb 12, 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
2 changes: 2 additions & 0 deletions rust/agama-utils/src/api/scope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ pub enum Scope {
Software,
Storage,
Files,
#[strum(serialize = "iscsi")]
#[serde(rename = "iscsi")]
ISCSI,
#[strum(serialize = "dasd")]
#[serde(rename = "dasd")]
Expand Down
53 changes: 53 additions & 0 deletions rust/share/system.iscsi.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"$schema": "https://json-schema.org/draft/2019-09/schema",
"$id": "https://github.com/openSUSE/agama/blob/master/rust/share/system.storage.schema.json",
"title": "System",
"description": "API description of the iSCSI system",
"type": "object",
"additionalProperties": false,
"required": ["initiator", "targets"],
"properties": {
"initiator": {
"title": "Initiator",
"type": "object",
"additionalProperties": false,
"required": ["name", "ibft"],
"properties": {
"name": { "type": "string" },
"ibft": { "type": "boolean" }
}
},
"targets": {
"type": "array",
"items": { "$ref": "#/$defs/target" }
}
},
"$defs": {
"target": {
"type": "object",
"additionalProperties": false,
"required": ["name", "address", "port", "interface", "ibtf", "startup", "connected", "locked"],
"properties": {
"name": { "type": "string" },
"address": { "type": "string" },
"port": { "type": "number" },
"interface": { "type": "string" },
"ibft": {
"description": "Whether the target was initiated by iBFT.",
"type": "boolean"
},
"startup": {
"enum": ["onboot", "manual", "automatic"]
},
"connected": {
"description": "Whether the node is connected (there is a session).",
"type": "boolean"
},
"locked": {
"description": "Whether the node is locked (cannot be deactivated)",
"type": "boolean"
}
}
}
}
}
7 changes: 7 additions & 0 deletions web/package/agama-web-ui.changes
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
-------------------------------------------------------------------
Thu Feb 12 11:56:57 UTC 2026 - David Diaz <dgonzalez@suse.com>

- Restore the interface for managing iSCSI in the web client
(gh#agama-project/agama#3092).
- Restore the "Rescan devices" storage option (bsc#1258089).

-------------------------------------------------------------------
Wed Feb 11 14:24:03 UTC 2026 - Imobach Gonzalez Sosa <igonzalezsosa@suse.com>

Expand Down
5 changes: 4 additions & 1 deletion web/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import type { Proposal } from "~/model/proposal";
import type { Question } from "~/model/question";
import type { Status } from "~/model/status";
import type { System } from "~/model/system";
import type { Action, L10nSystemConfig } from "~/model/action";
import type { Action, L10nSystemConfig, DiscoverISCSIConfig } from "~/model/action";
import type { AxiosResponse } from "axios";
import type { Job } from "~/types/job";

Expand Down Expand Up @@ -78,6 +78,8 @@ const activateStorageAction = () => postAction({ activateStorage: null });

const probeStorageAction = () => postAction({ probeStorage: null });

const discoverISCSIAction = (config: DiscoverISCSIConfig) => postAction({ discoverISCSI: config });

const finishInstallation = () => postAction({ finish: "reboot" });

/**
Expand All @@ -102,6 +104,7 @@ export {
configureL10nAction,
activateStorageAction,
probeStorageAction,
discoverISCSIAction,
finishInstallation,
getStorageJobs,
};
Expand Down
5 changes: 5 additions & 0 deletions web/src/assets/styles/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,11 @@ span.in-quotes {
}
}

nav.agm-breadcrumb h1 {
// Allow heading in breadcrumb use fontSize from its parent
font-size: inherit;
}

// 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
12 changes: 12 additions & 0 deletions web/src/components/core/Breadcrumbs.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,4 +109,16 @@ describe("Breadcrumbs.Item", () => {
expect(label).not.toHaveClass(textStyles.fontSizeLg);
expect(label).not.toHaveClass(textStyles.fontWeightBold);
});

it("renders menu when provided", () => {
installerRender(
<Breadcrumbs.Item
label="Software"
path="/software"
menu={<div role="menu" aria-label="Software menu" />}
/>,
);

screen.getByRole("menu", { name: "Software menu" });
});
});
43 changes: 40 additions & 3 deletions web/src/components/core/Breadcrumbs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,47 @@ import Icon from "~/components/layout/Icon";
import { TranslatedString, _ } from "~/i18n";

import textStyles from "@patternfly/react-styles/css/utilities/Text/text";
import displayStyles from "@patternfly/react-styles/css/utilities/Display/display";

export type BreadcrumbProps = {
/** The label to display for the breadcrumb item */
label: React.ReactNode | TranslatedString;
/** The URL path the breadcrumb item links to */
path?: string;
/**
* Optional menu providing contextual navigation for the breadcrumb item.
*
* When provided, it appears adjacent to the breadcrumb label and is intended
* to contain navigation options related to the area or section represented by
* the breadcrumb.
*
* @remarks
*
* **Accessibility requirement**
*
* The menu trigger must be fully accessible.
* Missing or incorrect ARIA attributes may result in an inaccessible breadcrumb.
*
* At a minimum, the trigger is expected to define:
* - aria-label="[Descriptive label]"
* - aria-haspopup="true"
* - aria-expanded="true | false"
*
* @example
* ```tsx
* <Breadcrumbs.Item
* label="Storage"
* path="/storage"
* menu={
* <Dropdown>
* <DropdownItem to="/storage/settings">Advanced settings</DropdownItem>
* <DropdownItem to="/storage/iscsi">iSCSI configuration</DropdownItem>
* </Dropdown>
* }
* />
* ```
*/
menu?: React.ReactNode;
/** Option to hide the divider (e.g., chevron icon) between breadcrumb items */
hideDivider?: boolean;
/** Flag to indicate if the breadcrumb item should have special editorial
Expand All @@ -51,6 +86,7 @@ export type BreadcrumbProps = {
const Breadcrumb = ({
label,
path,
menu,
isCurrent = false,
hideDivider = false,
isEditorial = false,
Expand All @@ -70,12 +106,13 @@ const Breadcrumb = ({
>
{!hideDivider && <Icon name="chevron_right" aria-hidden />}
{isCurrent ? (
<h1>{content}</h1>
<h1 className={displayStyles.displayInline}>{content}</h1>
) : (
<Link to={path} variant="link" isInline>
{content}
</Link>
)}
{menu}
</Flex>
);
};
Expand All @@ -92,10 +129,10 @@ const Breadcrumb = ({
*/
const Breadcrumbs = ({ a11yName = _("Breadcrumbs"), children }) => {
return (
<nav aria-label={a11yName}>
<nav aria-label={a11yName} className="agm-breadcrumb">
<Flex
component="ol"
gap={{ default: "gapXs" }}
gap={{ default: "gapNone" }}
alignItems={{ default: "alignItemsCenter" }}
alignContent={{ default: "alignContentCenter" }}
>
Expand Down
103 changes: 84 additions & 19 deletions web/src/components/core/ChangeProductOption.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@

import React from "react";
import { screen } from "@testing-library/react";
import { installerRender } from "~/test-utils";
import { installerRender, mockStage } from "~/test-utils";
import { useSystem } from "~/hooks/model/system";
import { Product } from "~/model/system";
import { PRODUCT as PATHS } from "~/routes/paths";
import ChangeProductOption from "./ChangeProductOption";
import { Product } from "~/model/system";

const tumbleweed: Product = {
id: "Tumbleweed",
Expand All @@ -46,38 +46,74 @@ const microos: Product = {
modes: [],
};

// let registrationInfoMock: RegistrationInfo;
const tumbleweedWithModes: Product = {
...tumbleweed,
modes: [
{ id: "mode1", name: "Mode 1", description: "Mode 1 desc" },
{ id: "mode2", name: "Mode 2", description: "Mode 2 desc" },
],
};

const mockSystemProducts: jest.Mock<Product[]> = jest.fn();
const mockSoftware: jest.Mock = jest.fn();

jest.mock("~/hooks/model/system", () => ({
...jest.requireActual("~/hooks/model/system"),
useSystem: (): ReturnType<typeof useSystem> => ({
products: mockSystemProducts(),
software: mockSoftware(),
}),
}));

describe("ChangeProductOption", () => {
beforeEach(() => {
mockSoftware.mockReturnValue(null);
mockStage("configuring");
});

describe("when there is more than one product available", () => {
beforeEach(() => {
mockSystemProducts.mockReturnValue([tumbleweed, microos]);
});

it("renders a menu item for navigating to product selection page", () => {
it("renders a link by default for navigating to product selection page", () => {
installerRender(<ChangeProductOption />);
const link = screen.getByRole("menuitem", { name: "Change product" });
const link = screen.getByRole("link", { name: "Change product" });
expect(link).toHaveAttribute("href", PATHS.changeProduct);
});

// FIXME: activate it again when registration is ready in api v2
describe.skip("but a product is registered", () => {
// beforeEach(() => {
// registrationInfoMock = {
// registered: true,
// key: "INTERNAL-USE-ONLY-1234-5678",
// email: "",
// url: "",
// };
// });
it("renders a menu item when component prop is 'dropdownitem'", () => {
installerRender(<ChangeProductOption component="dropdownitem" />);
screen.getByRole("menuitem", { name: "Change product" });
});

it("renders with an icon when showIcon is true", () => {
const { container } = installerRender(<ChangeProductOption showIcon />);
const icon = container.querySelector("svg");
expect(icon).toHaveAttribute("data-icon-name", "edit_square");
});

it("does not render an icon by default", () => {
installerRender(<ChangeProductOption />);
const link = screen.getByRole("link", { name: "Change product" });
expect(link.querySelector("svg")).toBeNull();
});

describe("but a product is registered", () => {
beforeEach(() => {
mockSoftware.mockReturnValue({ registration: true });
});

it("renders nothing", () => {
const { container } = installerRender(<ChangeProductOption />);
expect(container).toBeEmptyDOMElement();
});
});

describe("but the stage is not 'configuring'", () => {
beforeEach(() => {
mockStage("installing");
});

it("renders nothing", () => {
const { container } = installerRender(<ChangeProductOption />);
Expand All @@ -87,13 +123,42 @@ describe("ChangeProductOption", () => {
});

describe("when there is only one product available", () => {
describe("without modes", () => {
beforeEach(() => {
mockSystemProducts.mockReturnValue([tumbleweed]);
});

it("renders nothing", () => {
const { container } = installerRender(<ChangeProductOption />);
expect(container).toBeEmptyDOMElement();
});
});

describe("with modes", () => {
beforeEach(() => {
mockSystemProducts.mockReturnValue([tumbleweedWithModes]);
});

it("renders with 'Change mode' label", () => {
installerRender(<ChangeProductOption />);
screen.getByRole("link", { name: "Change mode" });
});
});
});

describe("when there are multiple products and at least one has modes", () => {
beforeEach(() => {
mockSystemProducts.mockReturnValue([tumbleweed]);
mockSystemProducts.mockReturnValue([tumbleweedWithModes, microos]);
});

it("renders with 'Change product or mode' label", () => {
installerRender(<ChangeProductOption />);
screen.getByRole("link", { name: "Change product or mode" });
});

it("renders nothing", () => {
const { container } = installerRender(<ChangeProductOption />);
expect(container).toBeEmptyDOMElement();
it("renders with icon when showIcon is true", () => {
installerRender(<ChangeProductOption showIcon component="dropdownitem" />);
screen.getByRole("menuitem", { name: /Change product or mode/ });
});
});
});
Loading
Loading