Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package agama-web-ui for openSUSE:Factory checked in at 2025-06-03 19:11:34 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/agama-web-ui (Old) and /work/SRC/openSUSE:Factory/.agama-web-ui.new.16005 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "agama-web-ui" Tue Jun 3 19:11:34 2025 rev:17 rq:1282416 version:0 Changes: -------- --- /work/SRC/openSUSE:Factory/agama-web-ui/agama-web-ui.changes 2025-05-27 18:43:11.323072232 +0200 +++ /work/SRC/openSUSE:Factory/.agama-web-ui.new.16005/agama-web-ui.changes 2025-06-03 19:11:38.572169990 +0200 @@ -1,0 +2,8 @@ +Tue Jun 3 08:32:23 UTC 2025 - Knut Anderssen <kanders...@suse.com> + +- Allow to select which connections will be used only for + installation and warn the user in case that there is no one + expected to be copied to the target system (no network). + (gh#agama-project/agama#2402). + +------------------------------------------------------------------- ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ agama.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/package/agama-web-ui.changes new/agama/package/agama-web-ui.changes --- old/agama/package/agama-web-ui.changes 2025-05-27 09:03:23.000000000 +0200 +++ new/agama/package/agama-web-ui.changes 2025-06-03 17:34:25.000000000 +0200 @@ -1,4 +1,12 @@ ------------------------------------------------------------------- +Tue Jun 3 08:32:23 UTC 2025 - Knut Anderssen <kanders...@suse.com> + +- Allow to select which connections will be used only for + installation and warn the user in case that there is no one + expected to be copied to the target system (no network). + (gh#agama-project/agama#2402). + +------------------------------------------------------------------- Mon May 26 19:51:54 UTC 2025 - Imobach Gonzalez Sosa <igonzalezs...@suse.com> - Version 15 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/api/network.ts new/agama/src/api/network.ts --- old/agama/src/api/network.ts 2025-05-27 09:03:23.000000000 +0200 +++ new/agama/src/api/network.ts 2025-06-03 17:34:25.000000000 +0200 @@ -20,7 +20,7 @@ * find current contact information at www.suse.com. */ -import { del, get, patch, post, put } from "~/api/http"; +import { del, get, post, put } from "~/api/http"; import { APIAccessPoint, APIConnection, APIDevice, NetworkGeneralState } from "~/types/network"; /** @@ -77,12 +77,22 @@ /** * Performs the connect action for connection matching given name */ -const connect = (name: string) => patch(`/api/network/connections/${name}/connect`); +const connect = (name: string) => post(`/api/network/connections/${name}/connect`); /** * Performs the disconnect action for connection matching given name */ -const disconnect = (name: string) => patch(`/api/network/connections/${name}/disconnect`); +const disconnect = (name: string) => post(`/api/network/connections/${name}/disconnect`); + +/** + * Make the connection persistent after the installation + */ +const keep = (name: string) => post(`/api/network/connections/${name}/keep`); + +/** + * Make the connection to be used only for the installation + */ +const unkeep = (name: string) => post(`/api/network/connections/${name}/unkeep`); export { fetchState, @@ -96,4 +106,6 @@ deleteConnection, connect, disconnect, + keep, + unkeep, }; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/assets/styles/index.scss new/agama/src/assets/styles/index.scss --- old/agama/src/assets/styles/index.scss 2025-05-27 09:03:23.000000000 +0200 +++ new/agama/src/assets/styles/index.scss 2025-06-03 17:34:25.000000000 +0200 @@ -132,6 +132,10 @@ --pf-t--global--font--family--200: "Noto Sans KR Display", "Noto Sans KR", serif; } +strong { + font-weight: var(--pf-t--global--font--weight--400); +} + // Temporary CSS rules written during migration to PFv6 // Reserve the sidebar space also for "lg" breakpoint diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/core/Annotation.test.tsx new/agama/src/components/core/Annotation.test.tsx --- old/agama/src/components/core/Annotation.test.tsx 1970-01-01 01:00:00.000000000 +0100 +++ new/agama/src/components/core/Annotation.test.tsx 2025-06-03 17:34:25.000000000 +0200 @@ -0,0 +1,51 @@ +/* + * 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 Annotation from "./Annotation"; + +describe("Annotation", () => { + it('renders the default "emergency" icon when no icon is provided', () => { + const { container } = plainRender(<Annotation>Configured for installation only</Annotation>); + + const icon = container.querySelector("svg"); + expect(icon).toHaveAttribute("data-icon-name", "emergency"); + }); + + it("renders a custom icon when icon is provided", () => { + const { container } = plainRender( + <Annotation icon="info">Configured for installation only</Annotation>, + ); + + const icon = container.querySelector("svg"); + expect(icon).toHaveAttribute("data-icon-name", "info"); + }); + + it("renders children inside a <b> element", () => { + plainRender(<Annotation>Configured for installation only</Annotation>); + + const content = screen.getByText("Configured for installation only"); + expect(content.tagName).toBe("STRONG"); + }); +}); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/core/Annotation.tsx new/agama/src/components/core/Annotation.tsx --- old/agama/src/components/core/Annotation.tsx 1970-01-01 01:00:00.000000000 +0100 +++ new/agama/src/components/core/Annotation.tsx 2025-06-03 17:34:25.000000000 +0200 @@ -0,0 +1,53 @@ +/* + * 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 { Flex } from "@patternfly/react-core"; +import { Icon } from "~/components/layout"; +import { IconProps } from "../layout/Icon"; + +type AnnotationProps = React.PropsWithChildren<{ + /** Name of the icon to display alongside the annotation. */ + icon?: IconProps["name"]; +}>; + +/** + * Displays a short note or clarification, wrapped in a `<strong>` HTML element. + * + * Intended for non-alert annotations that still require emphasis. The icon is + * optional and defaults to "emergency" (asterisk) if not provided. + * + * For more details on the `<strong>` element, refer to the HTML specification: + * https://html.spec.whatwg.org/multipage/text-level-semantics.html#the-strong-element + * + * @example + * ```tsx + * <Annotation icon="info">Configured for installation only.</Annotation> + * ``` + */ +export default function Annotation({ icon = "emergency", children }: AnnotationProps) { + return ( + <Flex component="p" alignItems={{ default: "alignItemsCenter" }} gap={{ default: "gapXs" }}> + <Icon name={icon} /> <strong>{children}</strong> + </Flex> + ); +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/core/SwitchEnhanced.test.tsx new/agama/src/components/core/SwitchEnhanced.test.tsx --- old/agama/src/components/core/SwitchEnhanced.test.tsx 1970-01-01 01:00:00.000000000 +0100 +++ new/agama/src/components/core/SwitchEnhanced.test.tsx 2025-06-03 17:34:25.000000000 +0200 @@ -0,0 +1,67 @@ +/* + * 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 SwitchEnhanced from "./SwitchEnhanced"; + +describe("SwitchEnhanced", () => { + it("renders a switch with label and description", () => { + plainRender( + <SwitchEnhanced + id="installation-only-connection" + label="Use for installation only" + description="Not persisted to the installed system." + isChecked={false} + />, + ); + + const switchElement = screen.getByRole("switch"); + const label = screen.getByText(/Use for installation only/); + const description = screen.getByText(/Not persisted to the installed system/); + + // Ensure aria-labelledby and aria-describedby point to the correct elements + expect(switchElement).toHaveAttribute("aria-labelledby", label.id); + expect(switchElement).toHaveAttribute("aria-describedby", description.id); + }); + + it("fires onChange handler when toggled", async () => { + const onChangeMock = jest.fn(); + + const { user } = plainRender( + <SwitchEnhanced + id="installation-only-connection" + label="Use for installation only" + description="Not persisted to the installed system." + isChecked={false} + onChange={onChangeMock} + />, + ); + + const switchElement = screen.getByRole("switch"); + + await user.click(switchElement); + + expect(onChangeMock).toHaveBeenCalledTimes(1); + }); +}); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/core/SwitchEnhanced.tsx new/agama/src/components/core/SwitchEnhanced.tsx --- old/agama/src/components/core/SwitchEnhanced.tsx 1970-01-01 01:00:00.000000000 +0100 +++ new/agama/src/components/core/SwitchEnhanced.tsx 2025-06-03 17:34:25.000000000 +0200 @@ -0,0 +1,76 @@ +/* + * 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, { useId } from "react"; +import { Content, Flex, FlexItem, Switch, SwitchProps } from "@patternfly/react-core"; + +type SwitchEnhancedProps = Omit< + SwitchProps, + "ref" | "label" | "aria-labelledby" | "aria-describedby" +> & { + /** Must describe the isChecked="true" state. */ + label: React.ReactNode; + /** Description or helper text displayed below the label. */ + description: React.ReactNode; +}; + +/** + * A wrapper around PatternFly's `Switch` component that adds support for a + * description or helper text displayed below the label. + * + * This component is useful when users can benefit from additional context about + * the switch’s function. + * + * Use this component when a toggle requires more explanation. If no description + * is needed, prefer using the standard PatternFly `Switch` component directly. + * + * @example + * ```tsx + * <SwitchEnhanced + * id="installation-only-connection" + * label="Use for installation only" + * description="The connection will be used only during installation and not persisted to the installed system." + * isChecked={isEnabled} + * onChange={toggleEmailNotifications} + * /> + * ``` + */ +export default function SwitchEnhanced({ description, label, ...props }: SwitchEnhancedProps) { + const labelId = useId(); + const descriptionId = useId(); + + return ( + <Flex flexWrap={{ default: "nowrap" }}> + <FlexItem> + <Switch {...props} aria-labelledby={labelId} aria-describedby={descriptionId} /> + </FlexItem> + <FlexItem> + <Content isEditorial id={labelId}> + {label} + </Content> + <Content component="small" id={descriptionId}> + {description} + </Content> + </FlexItem> + </Flex> + ); +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/core/index.ts new/agama/src/components/core/index.ts --- old/agama/src/components/core/index.ts 2025-05-27 09:03:23.000000000 +0200 +++ new/agama/src/components/core/index.ts 2025-06-03 17:34:25.000000000 +0200 @@ -49,3 +49,5 @@ export { default as MenuHeader } from "./MenuHeader"; export { default as SplitButton } from "./SplitButton"; export { default as SkipTo } from "./SkipTo"; +export { default as Annotation } from "./Annotation"; +export { default as SwitchEnhanced } from "./SwitchEnhanced"; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/layout/Icon.tsx new/agama/src/components/layout/Icon.tsx --- old/agama/src/components/layout/Icon.tsx 2025-05-27 09:03:23.000000000 +0200 +++ new/agama/src/components/layout/Icon.tsx 2025-06-03 17:34:25.000000000 +0200 @@ -32,6 +32,7 @@ import ChevronRight from "@icons/chevron_right.svg?component"; import Delete from "@icons/delete.svg?component"; import EditSquare from "@icons/edit_square.svg?component"; +import Emergency from "@icons/emergency.svg?component"; import Error from "@icons/error.svg?component"; import ErrorFill from "@icons/error-fill.svg?component"; import ExpandCircleDown from "@icons/expand_circle_down.svg?component"; @@ -69,6 +70,7 @@ chevron_right: ChevronRight, delete: Delete, edit_square: EditSquare, + emergency: Emergency, error: Error, error_fill: ErrorFill, expand_circle_down: ExpandCircleDown, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/network/InstallationOnlySwitch.test.tsx new/agama/src/components/network/InstallationOnlySwitch.test.tsx --- old/agama/src/components/network/InstallationOnlySwitch.test.tsx 1970-01-01 01:00:00.000000000 +0100 +++ new/agama/src/components/network/InstallationOnlySwitch.test.tsx 2025-06-03 17:34:25.000000000 +0200 @@ -0,0 +1,79 @@ +/* + * 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 InstallationOnlySwitch from "./InstallationOnlySwitch"; +import { Connection, ConnectionMethod, ConnectionOptions, ConnectionState } from "~/types/network"; + +const mockKeepMutation = jest.fn(); +const mockConnection = (options: Partial<ConnectionOptions> = {}) => + new Connection("Newtwork 2", { + method4: ConnectionMethod.AUTO, + method6: ConnectionMethod.AUTO, + wireless: { + security: "none", + ssid: "Network 2", + mode: "infrastructure", + }, + state: ConnectionState.activating, + ...options, + }); + +jest.mock("~/queries/network", () => ({ + ...jest.requireActual("~/queries/network"), + useConnectionKeepMutation: () => ({ + mutateAsync: mockKeepMutation, + }), +})); + +describe("InstallationOnlySwitch", () => { + it("renders the switch with the correct label and description", () => { + plainRender(<InstallationOnlySwitch connection={mockConnection()} />); + const switchInput = screen.getByRole("switch", { name: "Use for installation only" }); + const switchDescription = screen.getByText( + /The connection will be used only during installation/, + ); + expect(switchInput).toHaveAttribute("aria-describedby", switchDescription.id); + }); + + it("renders as checked when connection is transient (`keep` is false)", () => { + plainRender(<InstallationOnlySwitch connection={mockConnection({ keep: false })} />); + const switchInput = screen.getByRole("switch", { name: "Use for installation only" }); + expect(switchInput).toBeChecked(); + }); + + it("renders as not checked when connection is permanent (`keep` is true)", () => { + plainRender(<InstallationOnlySwitch connection={mockConnection({ keep: true })} />); + const switchInput = screen.getByRole("switch", { name: "Use for installation only" }); + expect(switchInput).not.toBeChecked(); + }); + + it("triggers mutation for switching between transient and permanent", async () => { + const connection = mockConnection({ keep: true }); + const { user } = plainRender(<InstallationOnlySwitch connection={connection} />); + const switchInput = screen.getByRole("switch", { name: "Use for installation only" }); + await user.click(switchInput); + expect(mockKeepMutation).toHaveBeenCalledWith(connection); + }); +}); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/network/InstallationOnlySwitch.tsx new/agama/src/components/network/InstallationOnlySwitch.tsx --- old/agama/src/components/network/InstallationOnlySwitch.tsx 1970-01-01 01:00:00.000000000 +0100 +++ new/agama/src/components/network/InstallationOnlySwitch.tsx 2025-06-03 17:34:25.000000000 +0200 @@ -0,0 +1,55 @@ +/* + * 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 { Connection } from "~/types/network"; +import { SwitchEnhanced } from "~/components/core"; +import { useConnectionKeepMutation } from "~/queries/network"; +import { _ } from "~/i18n"; + +type InstallationOnlySwitchProps = { + /** The connection to configure as installation-only or not */ + connection: Connection; +}; + +/** + * A switch for setting a network connection as "installation only". + * + * Intended to mark connections as transient (used only during + * OS installation) or persistent (persisted to the installed system) + * + */ +export default function InstallationOnlySwitch({ connection }: InstallationOnlySwitchProps) { + const { mutateAsync: toggleKeep } = useConnectionKeepMutation(); + const onChange = () => toggleKeep(connection); + + return ( + <SwitchEnhanced + label={_("Use for installation only")} + description={_( + "The connection will be used only during installation and not available in the installed system.", + )} + onChange={onChange} + isChecked={!connection.keep} + /> + ); +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/network/NetworkPage.test.tsx new/agama/src/components/network/NetworkPage.test.tsx --- old/agama/src/components/network/NetworkPage.test.tsx 2025-05-27 09:03:23.000000000 +0200 +++ new/agama/src/components/network/NetworkPage.test.tsx 2025-06-03 17:34:25.000000000 +0200 @@ -36,6 +36,10 @@ <div>WiredConnectionsList Mock</div> )); +jest.mock("~/components/network/NoPersistentConnectionsAlert", () => () => ( + <div>NoPersistentConnectionsAlert Mock</div> +)); + const mockNetworkState = { wirelessEnabled: true, }; @@ -46,6 +50,11 @@ })); describe("NetworkPage", () => { + it("mounts alert for all connections status", () => { + installerRender(<NetworkPage />); + expect(screen.queryByText("NoPersistentConnectionsAlert Mock")).toBeInTheDocument(); + }); + it("renders a section for wired connections", () => { installerRender(<NetworkPage />); expect(screen.queryByText("WiredConnectionsList Mock")).toBeInTheDocument(); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/network/NetworkPage.tsx new/agama/src/components/network/NetworkPage.tsx --- old/agama/src/components/network/NetworkPage.tsx 2025-05-27 09:03:23.000000000 +0200 +++ new/agama/src/components/network/NetworkPage.tsx 2025-06-03 17:34:25.000000000 +0200 @@ -23,10 +23,11 @@ import React from "react"; import { Content, Grid, GridItem } from "@patternfly/react-core"; import { EmptyState, Page } from "~/components/core"; -import { _ } from "~/i18n"; import { useNetworkChanges, useNetworkState } from "~/queries/network"; import WifiNetworksList from "./WifiNetworksList"; import WiredConnectionsList from "./WiredConnectionsList"; +import NoPersistentConnectionsAlert from "./NoPersistentConnectionsAlert"; +import { _ } from "~/i18n"; const NoWifiAvailable = () => ( <Page.Section> @@ -52,6 +53,8 @@ </Page.Header> <Page.Content> + <NoPersistentConnectionsAlert /> + <Grid hasGutter> <GridItem sm={12} xl={6}> <Page.Section title={_("Wired connections")}> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/network/NoPersistentConnectionsAlert.test.tsx new/agama/src/components/network/NoPersistentConnectionsAlert.test.tsx --- old/agama/src/components/network/NoPersistentConnectionsAlert.test.tsx 1970-01-01 01:00:00.000000000 +0100 +++ new/agama/src/components/network/NoPersistentConnectionsAlert.test.tsx 2025-06-03 17:34:25.000000000 +0200 @@ -0,0 +1,95 @@ +/* + * 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 { Connection } from "~/types/network"; +import NoPersistentConnectionsAlert from "./NoPersistentConnectionsAlert"; + +let mockConnections: Connection[]; + +jest.mock("~/queries/network", () => ({ + ...jest.requireActual("~/queries/network"), + useConnections: () => mockConnections, +})); + +describe("NoPersistentConnectionsAlert", () => { + describe("when there are connections to be kept", () => { + beforeEach(() => { + mockConnections = [ + new Connection("Newtwork 2", { + wireless: { + security: "none", + ssid: "Network 2", + mode: "infrastructure", + }, + keep: true, + }), + new Connection("Newtwork 3", { + wireless: { + security: "none", + ssid: "Network 2", + mode: "infrastructure", + }, + keep: false, + }), + ]; + }); + + it("renders nothing", () => { + const { container } = plainRender(<NoPersistentConnectionsAlert />); + expect(container).toBeEmptyDOMElement(); + }); + }); + + describe("when there are no connections to be kept", () => { + beforeEach(() => { + mockConnections = [ + new Connection("Newtwork 2", { + wireless: { + security: "none", + ssid: "Network 2", + mode: "infrastructure", + }, + keep: false, + }), + new Connection("Newtwork 3", { + wireless: { + security: "none", + ssid: "Network 2", + mode: "infrastructure", + }, + keep: false, + }), + ]; + }); + + it("renders a 'full-transient' alert", () => { + plainRender(<NoPersistentConnectionsAlert />); + + screen.getByText("Warning alert:"); + screen.getByText("Installed system may not have network connections"); + screen.getByText(/All.*managed through this interface.*not be copied.*/); + }); + }); +}); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/network/NoPersistentConnectionsAlert.tsx new/agama/src/components/network/NoPersistentConnectionsAlert.tsx --- old/agama/src/components/network/NoPersistentConnectionsAlert.tsx 1970-01-01 01:00:00.000000000 +0100 +++ new/agama/src/components/network/NoPersistentConnectionsAlert.tsx 2025-06-03 17:34:25.000000000 +0200 @@ -0,0 +1,46 @@ +/* + * 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 { Alert } from "@patternfly/react-core"; +import { useConnections } from "~/queries/network"; +import { Connection } from "~/types/network"; +import { _ } from "~/i18n"; +/** + * Displays a warning alert when no network connections are set to persist in + * the installed system. + */ +export default function NoPersistentConnectionsAlert() { + const connections: Connection[] = useConnections(); + const persistentConnections: number = connections.filter((c) => c.keep).length; + + if (persistentConnections !== 0) return; + + return ( + <Alert variant="warning" title={_("Installed system may not have network connections")}> + {_( + "All network connections managed through this interface are currently set to be \ + used only during installation and will not be copied to the installed system", + )} + </Alert> + ); +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/network/WifiConnectionDetails.test.tsx new/agama/src/components/network/WifiConnectionDetails.test.tsx --- old/agama/src/components/network/WifiConnectionDetails.test.tsx 1970-01-01 01:00:00.000000000 +0100 +++ new/agama/src/components/network/WifiConnectionDetails.test.tsx 2025-06-03 17:34:25.000000000 +0200 @@ -0,0 +1,114 @@ +/* + * 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, within } from "@testing-library/react"; +import { plainRender } from "~/test-utils"; +import WifiConnectionDetails from "./WifiConnectionDetails"; +import { + Connection, + ConnectionMethod, + ConnectionType, + Device, + DeviceState, + SecurityProtocols, + WifiNetworkStatus, +} from "~/types/network"; + +jest.mock("~/components/network/InstallationOnlySwitch", () => () => ( + <div>InstallationOnlySwitch mock</div> +)); + +const wlan0: Device = { + name: "wlan0", + connection: "Network 1", + type: ConnectionType.WIFI, + state: DeviceState.CONNECTED, + addresses: [{ address: "192.168.69.201", prefix: 24 }], + nameservers: ["192.168.69.100"], + method4: ConnectionMethod.MANUAL, + method6: ConnectionMethod.AUTO, + gateway4: "192.168.69.4", + gateway6: "192.168.69.6", + macAddress: "AA:11:22:33:44::FF", + routes4: [], + routes6: [], +}; + +const mockNetwork = { + ssid: "Network 1", + strength: 25, + hwAddress: "??", + security: [SecurityProtocols.RSN], + device: wlan0, + settings: new Connection("Network 1", { + iface: "wlan0", + addresses: [{ address: "192.168.69.201", prefix: 24 }], + }), + status: WifiNetworkStatus.CONNECTED, +}; + +describe("WifiConnectionDetails", () => { + it("renders the device data", () => { + plainRender(<WifiConnectionDetails network={mockNetwork} />); + const section = screen.getByRole("region", { name: "Device" }); + within(section).getByText("wlan0"); + within(section).getByText("connected"); + within(section).getByText("AA:11:22:33:44::FF"); + }); + + it("renders the network data", () => { + plainRender(<WifiConnectionDetails network={mockNetwork} />); + const section = screen.getByRole("region", { name: "Network" }); + within(section).getByText("Network 1"); + within(section).getByText("25%"); + within(section).getByText("connected"); + within(section).getByText("WPA2"); + }); + + it("renders the IP data", () => { + plainRender(<WifiConnectionDetails network={mockNetwork} />); + const section = screen.getByRole("region", { name: "IP settings" }); + within(section).getByText("IPv4 auto"); + within(section).getByText("IPv6 auto"); + // IP + within(section).getByText("192.168.69.201/24"); + // DNS + within(section).getByText("192.168.69.100"); + // Gateway 4 + within(section).getByText("192.168.69.4"); + // Gateway 6 + within(section).getByText("192.168.69.6"); + }); + + it("renders link for editing connection", () => { + plainRender(<WifiConnectionDetails network={mockNetwork} />); + const section = screen.getByRole("region", { name: "IP settings" }); + const editLink = within(section).getByRole("link", { name: "Edit" }); + expect(editLink).toHaveAttribute("href", "/network/connections/Network 1/edit"); + }); + + it("renders the switch for making connection available only during installation", () => { + plainRender(<WifiConnectionDetails network={mockNetwork} />); + screen.getByText("InstallationOnlySwitch mock"); + }); +}); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/network/WifiConnectionDetails.tsx new/agama/src/components/network/WifiConnectionDetails.tsx --- old/agama/src/components/network/WifiConnectionDetails.tsx 2025-05-27 09:03:23.000000000 +0200 +++ new/agama/src/components/network/WifiConnectionDetails.tsx 2025-06-03 17:34:25.000000000 +0200 @@ -34,6 +34,7 @@ Stack, } from "@patternfly/react-core"; import { Link, Page } from "~/components/core"; +import InstallationOnlySwitch from "./InstallationOnlySwitch"; import { Device, WifiNetwork } from "~/types/network"; import { formatIp } from "~/utils/network"; import { NETWORK } from "~/routes/paths"; @@ -163,7 +164,7 @@ return ( <Grid hasGutter> - <GridItem md={6} order={{ default: "2", md: "1" }}> + <GridItem md={6} order={{ default: "2", md: "1" }} rowSpan={3}> <IpDetails device={network.device} settings={network.settings} /> </GridItem> <GridItem md={6} order={{ default: "1", md: "2" }}> @@ -172,6 +173,9 @@ <NetworkDetails network={network} /> </Stack> </GridItem> + <GridItem md={6} order={{ default: "3" }}> + <InstallationOnlySwitch connection={network.settings} /> + </GridItem> </Grid> ); } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/network/WifiNetworksList.test.tsx new/agama/src/components/network/WifiNetworksList.test.tsx --- old/agama/src/components/network/WifiNetworksList.test.tsx 2025-05-27 09:03:23.000000000 +0200 +++ new/agama/src/components/network/WifiNetworksList.test.tsx 2025-06-03 17:34:25.000000000 +0200 @@ -142,6 +142,82 @@ }); }); + describe("and the connection is selected to be kept", () => { + beforeEach(() => { + mockWifiConnections = [ + new Connection("Newtwork 2", { + method4: ConnectionMethod.AUTO, + method6: ConnectionMethod.AUTO, + wireless: { + security: "none", + ssid: "Network 2", + mode: "infrastructure", + }, + state: ConnectionState.activating, + keep: true, + }), + ]; + + mockWifiNetworks = [ + { + ssid: "Network 2", + strength: 88, + hwAddress: "??", + security: [SecurityProtocols.RSN], + settings: new Connection("Network 2", { + iface: "wlan1", + addresses: [{ address: "192.168.69.202", prefix: 24 }], + }), + status: WifiNetworkStatus.CONFIGURED, + }, + ]; + }); + + it("does not render any hint", () => { + // @ts-expect-error: you need to specify the aria-label + installerRender(<WifiNetworksList />); + expect(screen.queryByText("Configured for installation only")).toBeNull; + }); + }); + + describe("and the connection is not selected to be kept", () => { + beforeEach(() => { + mockWifiConnections = [ + new Connection("Newtwork 2", { + method4: ConnectionMethod.AUTO, + method6: ConnectionMethod.AUTO, + wireless: { + security: "none", + ssid: "Network 2", + mode: "infrastructure", + }, + state: ConnectionState.activating, + keep: false, + }), + ]; + + mockWifiNetworks = [ + { + ssid: "Network 2", + strength: 88, + hwAddress: "??", + security: [SecurityProtocols.RSN], + settings: new Connection("Network 2", { + iface: "wlan1", + addresses: [{ address: "192.168.69.202", prefix: 24 }], + }), + status: WifiNetworkStatus.CONFIGURED, + }, + ]; + }); + + it("renders an installation only hint", () => { + // @ts-expect-error: you need to specify the aria-label + installerRender(<WifiNetworksList />); + screen.getByText("Configured for installation only"); + }); + }); + describe.skip("and user selects a connected network", () => { it("renders basic network information and actions instead of the connection form", async () => { // @ts-expect-error: you need to specify the aria-label diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/network/WifiNetworksList.tsx new/agama/src/components/network/WifiNetworksList.tsx --- old/agama/src/components/network/WifiNetworksList.tsx 2025-05-27 09:03:23.000000000 +0200 +++ new/agama/src/components/network/WifiNetworksList.tsx 2025-06-03 17:34:25.000000000 +0200 @@ -35,15 +35,15 @@ Spinner, } from "@patternfly/react-core"; import a11yStyles from "@patternfly/react-styles/css/utilities/Accessibility/accessibility"; -import { EmptyState } from "~/components/core"; +import { Annotation, EmptyState } from "~/components/core"; import Icon, { IconProps } from "~/components/layout/Icon"; import { Connection, ConnectionState, WifiNetwork, WifiNetworkStatus } from "~/types/network"; import { useConnections, useNetworkChanges, useWifiNetworks } from "~/queries/network"; import { NETWORK as PATHS } from "~/routes/paths"; import { isEmpty } from "~/utils"; import { formatIp } from "~/utils/network"; -import { _ } from "~/i18n"; import { sprintf } from "sprintf-js"; +import { _ } from "~/i18n"; const NetworkSignal = ({ id, signal }) => { let label: string; @@ -141,6 +141,10 @@ {network.device?.addresses.map(formatIp).join(", ")} </Content> )} + + {connection && !connection.keep && ( + <Annotation>{_("Configured for installation only")}</Annotation> + )} </Flex> </DataListCell>, <DataListCell key="badges" isFilled={false} alignRight> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/network/WiredConnectionDetails.test.tsx new/agama/src/components/network/WiredConnectionDetails.test.tsx --- old/agama/src/components/network/WiredConnectionDetails.test.tsx 1970-01-01 01:00:00.000000000 +0100 +++ new/agama/src/components/network/WiredConnectionDetails.test.tsx 2025-06-03 17:34:25.000000000 +0200 @@ -0,0 +1,101 @@ +/* + * 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, within } from "@testing-library/react"; +import { plainRender } from "~/test-utils"; +import WiredConnectionDetails from "./WiredConnectionDetails"; +import { + Connection, + ConnectionMethod, + ConnectionState, + ConnectionType, + Device, + DeviceState, +} from "~/types/network"; + +jest.mock("~/components/network/InstallationOnlySwitch", () => () => ( + <div>InstallationOnlySwitch mock</div> +)); + +const mockDevice: Device = { + name: "enp1s0", + connection: "Network 1", + type: ConnectionType.ETHERNET, + state: DeviceState.CONNECTED, + addresses: [{ address: "192.168.69.201", prefix: 24 }], + nameservers: ["192.168.69.100"], + gateway4: "192.168.69.4", + gateway6: "192.168.69.6", + method4: ConnectionMethod.AUTO, + method6: ConnectionMethod.AUTO, + macAddress: "AA:11:22:33:44::FF", + routes4: [], + routes6: [], +}; + +const mockConnection: Connection = new Connection("Network 1", { + state: ConnectionState.activated, + iface: "enp1s0", +}); + +jest.mock("~/queries/network", () => ({ + ...jest.requireActual("~/queries/network"), + useNetworkDevices: () => [mockDevice], +})); + +describe("WiredConnectionDetails", () => { + it("renders the device data", () => { + plainRender(<WiredConnectionDetails connection={mockConnection} />); + const section = screen.getByRole("region", { name: "Device" }); + within(section).getByText("enp1s0"); + within(section).getByText("connected"); + within(section).getByText("AA:11:22:33:44::FF"); + }); + + it("renders the IP data", () => { + plainRender(<WiredConnectionDetails connection={mockConnection} />); + const section = screen.getByRole("region", { name: "IP settings" }); + within(section).getByText("IPv4 auto"); + within(section).getByText("IPv6 auto"); + // IP + within(section).getByText("192.168.69.201/24"); + // DNS + within(section).getByText("192.168.69.100"); + // Gateway 4 + within(section).getByText("192.168.69.4"); + // Gateway 6 + within(section).getByText("192.168.69.6"); + }); + + it("renders link for editing connection", () => { + plainRender(<WiredConnectionDetails connection={mockConnection} />); + const section = screen.getByRole("region", { name: "IP settings" }); + const editLink = within(section).getByRole("link", { name: "Edit" }); + expect(editLink).toHaveAttribute("href", "/network/connections/Network 1/edit"); + }); + + it("renders the switch for making connection available only during installation", () => { + plainRender(<WiredConnectionDetails connection={mockConnection} />); + screen.getByText("InstallationOnlySwitch mock"); + }); +}); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/network/WiredConnectionDetails.tsx new/agama/src/components/network/WiredConnectionDetails.tsx --- old/agama/src/components/network/WiredConnectionDetails.tsx 2025-05-27 09:03:23.000000000 +0200 +++ new/agama/src/components/network/WiredConnectionDetails.tsx 2025-06-03 17:34:25.000000000 +0200 @@ -34,6 +34,7 @@ Stack, } from "@patternfly/react-core"; import { Link, Page } from "~/components/core"; +import InstallationOnlySwitch from "./InstallationOnlySwitch"; import { Connection, Device } from "~/types/network"; import { formatIp } from "~/utils/network"; import { NETWORK } from "~/routes/paths"; @@ -87,7 +88,6 @@ <FlexItem> {_("IPv6")} {connection.method6} </FlexItem> - <FlexItem>{device.gateway6}</FlexItem> </Flex> </DescriptionListDescription> </DescriptionListGroup> @@ -144,7 +144,7 @@ return ( <Grid hasGutter> - <GridItem md={6} order={{ default: "2", md: "1" }}> + <GridItem md={6} order={{ default: "2", md: "1" }} rowSpan={3}> <IpDetails device={device} connection={connection} /> </GridItem> <GridItem md={6} order={{ default: "1", md: "2" }}> @@ -152,6 +152,9 @@ <DeviceDetails device={device} /> </Stack> </GridItem> + <GridItem md={6} order={{ default: "3" }}> + <InstallationOnlySwitch connection={connection} /> + </GridItem> </Grid> ); } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/network/WiredConnectionsList.test.tsx new/agama/src/components/network/WiredConnectionsList.test.tsx --- old/agama/src/components/network/WiredConnectionsList.test.tsx 1970-01-01 01:00:00.000000000 +0100 +++ new/agama/src/components/network/WiredConnectionsList.test.tsx 2025-06-03 17:34:25.000000000 +0200 @@ -0,0 +1,98 @@ +/* + * Copyright (c) [2024] SUSE LLC + * + * All Rights Reserved. + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of version 2 of the GNU General Public License as published + * by the Free Software Foundation. + * + * 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 WiredConnectionsList from "~/components/network/WiredConnectionsList"; +import { + Connection, + ConnectionMethod, + ConnectionState, + ConnectionType, + Device, + DeviceState, +} from "~/types/network"; + +const mockDevice: Device = { + name: "enp1s0", + connection: "Network 1", + type: ConnectionType.ETHERNET, + state: DeviceState.CONNECTED, + addresses: [{ address: "192.168.69.201", prefix: 24 }], + nameservers: ["192.168.69.100"], + gateway4: "192.168.69.4", + gateway6: "192.168.69.6", + method4: ConnectionMethod.AUTO, + method6: ConnectionMethod.AUTO, + macAddress: "AA:11:22:33:44::FF", + routes4: [], + routes6: [], +}; + +let mockConnections: Connection[]; + +jest.mock("~/queries/network", () => ({ + ...jest.requireActual("~/queries/network"), + useNetworkChanges: jest.fn(), + useNetworkDevices: () => [mockDevice], + useConnections: () => mockConnections, +})); + +describe("WiredConnectionsList", () => { + describe("and the connection is selected to be kept", () => { + beforeEach(() => { + mockConnections = [ + new Connection("Newtwork 1", { + method4: ConnectionMethod.AUTO, + method6: ConnectionMethod.AUTO, + state: ConnectionState.activating, + keep: true, + }), + ]; + }); + + it("does not render any hint", () => { + // @ts-expect-error: you need to specify the aria-label + installerRender(<WiredConnectionsList />); + expect(screen.queryByText("Configured for installation only")).toBeNull; + }); + }); + + describe("and the connection is not selected to be kept", () => { + beforeEach(() => { + mockConnections = [ + new Connection("Newtwork 1", { + method4: ConnectionMethod.AUTO, + method6: ConnectionMethod.AUTO, + state: ConnectionState.activating, + keep: false, + }), + ]; + }); + + it("renders an installation only hint", () => { + // @ts-expect-error: you need to specify the aria-label + installerRender(<WiredConnectionsList />); + screen.getByText("Configured for installation only"); + }); + }); +}); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/components/network/WiredConnectionsList.tsx new/agama/src/components/network/WiredConnectionsList.tsx --- old/agama/src/components/network/WiredConnectionsList.tsx 2025-05-27 09:03:23.000000000 +0200 +++ new/agama/src/components/network/WiredConnectionsList.tsx 2025-06-03 17:34:25.000000000 +0200 @@ -33,7 +33,7 @@ Flex, } from "@patternfly/react-core"; import a11yStyles from "@patternfly/react-styles/css/utilities/Accessibility/accessibility"; -import { EmptyState } from "~/components/core"; +import { Annotation, EmptyState } from "~/components/core"; import { Connection } from "~/types/network"; import { useConnections, useNetworkDevices } from "~/queries/network"; import { NETWORK as PATHS } from "~/routes/paths"; @@ -66,6 +66,9 @@ <Content className={a11yStyles.screenReader}>{_("IP addresses")}</Content> {addresses.map(formatIp).join(", ")} </Content> + {!connection.keep && ( + <Annotation>{_("Configured for installation only")}</Annotation> + )} </Flex> </DataListCell>, ]} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/queries/network.ts new/agama/src/queries/network.ts --- old/agama/src/queries/network.ts 2025-05-27 09:03:23.000000000 +0200 +++ new/agama/src/queries/network.ts 2025-06-03 17:34:25.000000000 +0200 @@ -42,6 +42,8 @@ fetchConnections, fetchDevices, fetchState, + keep, + unkeep, updateConnection, } from "~/api/network"; @@ -123,6 +125,7 @@ }; return useMutation(query); }; + /** * Hook that builds a mutation to update a network connection * @@ -142,6 +145,54 @@ }; /** + * Hook that provides a mutation for toggling the "keep" state of a network + * connection. + * + * This hook uses optimistic updates to immediately reflect the change in the UI + * before the mutation completes. If the mutation fails, it will rollback to the + * previous state. + */ +const useConnectionKeepMutation = () => { + const queryClient = useQueryClient(); + const query = { + mutationFn: (connection: Connection) => { + const method = connection.keep ? unkeep : keep; + return method(connection.id); + }, + onMutate: async (connection: Connection) => { + // Get the current list of cached connections + const previousConnections: Connection[] = queryClient.getQueryData([ + "network", + "connections", + ]); + + // Optimistically toggle the 'keep' status of the matching connection + const updatedConnections = previousConnections.map((cachedConnection) => { + if (connection.id !== cachedConnection.id) return cachedConnection; + + const { id, ...nextConnection } = cachedConnection; + return new Connection(id, { ...nextConnection, keep: !cachedConnection.keep }); + }); + + // Update the cached data with the optimistically updated connections + queryClient.setQueryData(["network", "connections"], updatedConnections); + + // Return the previous state for potential rollback + return { previousConnections }; + }, + + /** + * Called if the mutation fails for whatever reason. Rolls back the cache to + * the previous state. + */ + onError: (_, connection: Connection, context: { previousConnections: Connection[] }) => { + queryClient.setQueryData(["network", "connections"], context.previousConnections); + }, + }; + + return useMutation(query); +}; +/** * Hook that builds a mutation to remove a network connection * * It does not require to call `useMutation`. @@ -316,6 +367,7 @@ useAddConnectionMutation, useConnections, useConnectionMutation, + useConnectionKeepMutation, useRemoveConnectionMutation, useConnection, useNetworkDevices, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/types/network.ts new/agama/src/types/network.ts --- old/agama/src/types/network.ts 2025-05-27 09:03:23.000000000 +0200 +++ new/agama/src/types/network.ts 2025-06-03 17:34:25.000000000 +0200 @@ -20,7 +20,7 @@ * find current contact information at www.suse.com. */ -import { isObject } from "~/utils"; +import { isEmpty, isObject } from "~/utils"; import { buildAddress, buildAddresses, @@ -232,6 +232,7 @@ wireless?: Wireless; status: ConnectionStatus; state: ConnectionState; + keep: boolean; }; type WirelessOptions = { @@ -268,6 +269,7 @@ method6?: ConnectionMethod; wireless?: Wireless; state?: ConnectionState; + keep?: boolean; }; class Connection { @@ -282,6 +284,7 @@ method4: ConnectionMethod = ConnectionMethod.AUTO; method6: ConnectionMethod = ConnectionMethod.AUTO; wireless?: Wireless; + keep: boolean; constructor(id: string, options?: ConnectionOptions) { this.id = id; @@ -289,7 +292,7 @@ if (!isObject(options)) return; for (const [key, value] of Object.entries(options)) { - if (value) this[key] = value; + if (!isEmpty(value)) this[key] = value; } } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/utils.test.ts new/agama/src/utils.test.ts --- old/agama/src/utils.test.ts 2025-05-27 09:03:23.000000000 +0200 +++ new/agama/src/utils.test.ts 2025-06-03 17:34:25.000000000 +0200 @@ -178,6 +178,14 @@ expect(isEmpty(() => {})).toBe(false); }); + it("returns false when called with `true` (boolean)", () => { + expect(isEmpty(true)).toBe(false); + }); + + it("returns false when called with `false` (boolean)", () => { + expect(isEmpty(false)).toBe(false); + }); + it("returns false when called with a number", () => { expect(isEmpty(1)).toBe(false); }); diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/agama/src/utils.ts new/agama/src/utils.ts --- old/agama/src/utils.ts 2025-05-27 09:03:23.000000000 +0200 +++ new/agama/src/utils.ts 2025-06-03 17:34:25.000000000 +0200 @@ -62,6 +62,10 @@ return true; } + if (typeof value === "boolean") { + return false; + } + if (typeof value === "function") { return false; } ++++++ agama.obsinfo ++++++ --- /var/tmp/diff_new_pack.fSdpeV/_old 2025-06-03 19:11:43.520375523 +0200 +++ /var/tmp/diff_new_pack.fSdpeV/_new 2025-06-03 19:11:43.524375688 +0200 @@ -1,5 +1,5 @@ name: agama -version: 15+4.dc6580496 -mtime: 1748329403 -commit: dc6580496dea8904e55a8525ade52ac27f60cab1 +version: 15+54.8e10affb2 +mtime: 1748964865 +commit: 8e10affb228ff198651e5338d8ad91b481a86be0