diff --git a/web/package/cockpit-d-installer.changes b/web/package/cockpit-d-installer.changes index 02a45f4bdc..8739e73399 100644 --- a/web/package/cockpit-d-installer.changes +++ b/web/package/cockpit-d-installer.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Thu Dec 15 10:14:22 UTC 2022 - Knut Anderssen + +- Do not show the link to configure wifi networks when wireless is + not enabled (gh#yast/d-installer#323). + ------------------------------------------------------------------- Thu Dec 15 08:55:02 UTC 2022 - Imobach Gonzalez Sosa diff --git a/web/src/client/network.test.js b/web/src/client/network.test.js index 9fd51b511f..a5da6ba573 100644 --- a/web/src/client/network.test.js +++ b/web/src/client/network.test.js @@ -38,11 +38,15 @@ const conn = { addresses: [{ address: "192.168.122.1", prefix: 24 }] }; +const settings = { + wireless: true, + hostname: "localhost.localdomain" +}; + const adapter = { setUp: jest.fn(), activeConnections: jest.fn().mockReturnValue([active_conn]), connections: jest.fn().mockReturnValue([conn]), - hostname: jest.fn().mockReturnValue("localhost.localdomain"), subscribe: jest.fn(), getConnection: jest.fn(), addConnection: jest.fn(), @@ -50,7 +54,8 @@ const adapter = { deleteConnection: jest.fn(), accessPoints: jest.fn(), connectTo: jest.fn(), - addAndConnectTo: jest.fn() + addAndConnectTo: jest.fn(), + settings: jest.fn().mockReturnValue(settings), }; describe("NetworkClient", () => { @@ -69,10 +74,11 @@ describe("NetworkClient", () => { }); }); - describe("#hostname", () => { - it("returns the hostname from the adapter", () => { + describe("#settings", () => { + it("returns network general settings", () => { const client = new NetworkClient(adapter); - expect(client.hostname()).toEqual("localhost.localdomain"); + expect(client.settings().hostname).toEqual("localhost.localdomain"); + expect(client.settings().wireless).toEqual(true); }); }); }); diff --git a/web/src/client/network/index.js b/web/src/client/network/index.js index a30c2fc2a4..99641f2f9a 100644 --- a/web/src/client/network/index.js +++ b/web/src/client/network/index.js @@ -25,6 +25,7 @@ import { NetworkManagerAdapter } from "./network_manager"; import { ConnectionTypes, ConnectionState } from "./model"; /** + * @typedef {import("./model").NetworkSettings} NetworkSettings * @typedef {import("./model").Connection} Connection * @typedef {import("./model").ActiveConnection} ActiveConnection * @typedef {import("./model").IPAddress} IPAddress @@ -37,7 +38,8 @@ const NetworkEventTypes = Object.freeze({ ACTIVE_CONNECTION_REMOVED: "active_connection_removed", CONNECTION_ADDED: "connection_added", CONNECTION_UPDATED: "connection_updated", - CONNECTION_REMOVED: "connection_removed" + CONNECTION_REMOVED: "connection_removed", + SETTINGS_UPDATED: "settings_updated" }); /** @@ -52,7 +54,7 @@ const NetworkEventTypes = Object.freeze({ * @property {(connection: Connection) => Promise} addConnection * @property {(connection: Connection) => Promise} updateConnection * @property {(connection: Connection) => void} deleteConnection - * @property {() => string} hostname + * @property {() => NetworkSettings} settings * @property {() => void} setUp */ @@ -210,13 +212,11 @@ o * NetworkManagerAdapter. return conns.flatMap(c => c.addresses); } - /** - * Returns the computer's hostname - * - * @return {string} - */ - hostname() { - return this.adapter.hostname(); + /* + * Returns network general settings + */ + settings() { + return this.adapter.settings(); } } diff --git a/web/src/client/network/model.js b/web/src/client/network/model.js index b4688a8030..04925b5e0e 100644 --- a/web/src/client/network/model.js +++ b/web/src/client/network/model.js @@ -107,6 +107,11 @@ const SecurityProtocols = Object.freeze({ * @property {string[]} security */ +/** +* @typedef {object} NetworkSettings +* @property {boolean} wireless +* @property {string} hostname + /** * Returns an IPv4 configuration object * diff --git a/web/src/client/network/network_manager.js b/web/src/client/network/network_manager.js index becd385419..1d88fbdc51 100644 --- a/web/src/client/network/network_manager.js +++ b/web/src/client/network/network_manager.js @@ -28,6 +28,7 @@ import { NetworkEventTypes } from "./index"; import { createAccessPoint, createConnection, SecurityProtocols } from "./model"; /** + * @typedef {import("./model").NetworkSettings} NetworkSettings * @typedef {import("./model").Connection} Connection * @typedef {import("./model").ActiveConnection} ActiveConnection * @typedef {import("./model").IPAddress} IPAddress @@ -177,6 +178,7 @@ class NetworkManagerAdapter { accessPoints: {}, activeConnections: {}, ip4Configs: {}, + manager: null, settings: null, connections: {} }; @@ -196,6 +198,7 @@ class NetworkManagerAdapter { ACTIVE_CONNECTION_IFACE, ACTIVE_CONNECTION_NAMESPACE ), ip4Configs: await this.client.proxies(IP4CONFIG_IFACE, IP4CONFIG_NAMESPACE), + manager: await this.client.proxy(IFACE), settings: await this.client.proxy(SETTINGS_IFACE), connections: await this.client.proxies(CONNECTION_IFACE, SETTINGS_NAMESPACE) }; @@ -372,6 +375,8 @@ class NetworkManagerAdapter { async subscribeToEvents() { const activeConnectionProxies = this.proxies.activeConnections; const connectionProxies = this.proxies.connections; + const managerProxy = this.proxies.manager; + const settingsProxy = this.proxies.settings; /** @type {(eventType: string) => NetworkEventFn} */ const handleWrapperActiveConnection = (eventType) => (_event, proxy) => { @@ -392,6 +397,12 @@ class NetworkManagerAdapter { this.eventsHandler({ type: eventType, payload: connection }); }; + const handleWrapperSettings = (eventType) => () => { + const settings = this.settingsFromProxies(managerProxy, settingsProxy); + + this.eventsHandler({ type: eventType, payload: settings }); + }; + // FIXME: do not build a map (eventTypesMap), just inject the type here connectionProxies.addEventListener("added", handleWrapperConnection(NetworkEventTypes.CONNECTION_ADDED)); connectionProxies.addEventListener("changed", handleWrapperConnection(NetworkEventTypes.CONNECTION_UPDATED)); @@ -401,6 +412,9 @@ class NetworkManagerAdapter { activeConnectionProxies.addEventListener("added", handleWrapperActiveConnection(NetworkEventTypes.ACTIVE_CONNECTION_ADDED)); activeConnectionProxies.addEventListener("changed", handleWrapperActiveConnection(NetworkEventTypes.ACTIVE_CONNECTION_UPDATED)); activeConnectionProxies.addEventListener("removed", handleWrapperActiveConnection(NetworkEventTypes.ACTIVE_CONNECTION_REMOVED)); + + managerProxy.addEventListener("changed", handleWrapperSettings(NetworkEventTypes.SETTINGS_UPDATED)); + settingsProxy.addEventListener("changed", handleWrapperSettings(NetworkEventTypes.SETTINGS_UPDATED)); } /** @@ -486,17 +500,19 @@ class NetworkManagerAdapter { return { address: data.address.v, prefix: parseInt(data.prefix.v) }; } - /** - * Returns the computer's hostname - * - * @return {string} - * - * https://developer-old.gnome.org/NetworkManager/stable/gdbus-org.freedesktop.NetworkManager.Settings.html - */ - hostname() { - if (!this.proxies.settings) return ""; + settingsFromProxies(manager, settings) { + return { + wireless: !!(manager?.WirelessEnabled && manager?.WirelessHardwareEnabled), + hostname: settings?.Hostname || "" + }; + } - return this.proxies.settings.Hostname; + /* + * Returns NetworkManager general settings + * @return {NetworkSettings} + */ + settings() { + return this.settingsFromProxies(this.proxies.manager, this.proxies.settings); } } diff --git a/web/src/client/network/network_manager.test.js b/web/src/client/network/network_manager.test.js index 91fbf66b47..587c2ebb59 100644 --- a/web/src/client/network/network_manager.test.js +++ b/web/src/client/network/network_manager.test.js @@ -95,12 +95,15 @@ const connections = { } }; +// Reminder: by default, properties added using Object.defineProperties() are not enumerable. +// We use #defineProperties here, so it doesn't show up as a "connection" in these objects. +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperties#enumerable Object.defineProperties(activeConnections, { - addEventListener: { value: jest.fn(), enumerable: false } + addEventListener: { value: jest.fn() } }); Object.defineProperties(connections, { - addEventListener: { value: jest.fn(), enumerable: false } + addEventListener: { value: jest.fn() } }); const addressesData = { @@ -125,18 +128,26 @@ const addressesData = { }; const ActivateConnectionFn = jest.fn(); -const networkProxy = () => ({ + +const networkProxy = { wait: jest.fn(), ActivateConnection: ActivateConnectionFn, ActiveConnections: Object.keys(activeConnections), -}); + WirelessEnabled: false, + WifiHardwareEnabled: true +}; const AddConnectionFn = jest.fn(); -const networkSettingsProxy = () => ({ +const networkSettingsProxy = { wait: jest.fn(), Hostname: "testing-machine", GetConnectionByUuid: () => "/org/freedesktop/NetworkManager/Settings/1", - AddConnection: AddConnectionFn + AddConnection: AddConnectionFn, + addEventListener: () => ({ value: jest.fn(), enumerable: false }) +}; + +Object.defineProperties(networkProxy, { + addEventListener: { value: jest.fn(), enumerable: false } }); const connectionSettingsMock = { @@ -172,8 +183,8 @@ const connectionSettingsProxy = () => connectionSettingsMock; describe("NetworkManagerAdapter", () => { beforeEach(() => { dbusClient.proxy = jest.fn().mockImplementation(iface => { - if (iface === NM_IFACE) return networkProxy(); - if (iface === NM_SETTINGS_IFACE) return networkSettingsProxy(); + if (iface === NM_IFACE) return networkProxy; + if (iface === NM_SETTINGS_IFACE) return networkSettingsProxy; if (iface === NM_CONNECTION_IFACE) return connectionSettingsProxy(); }); @@ -345,11 +356,12 @@ describe("NetworkManagerAdapter", () => { }); }); - describe("#hostname", () => { + describe("#settings", () => { it("returns the Network Manager settings hostname", async() => { const client = new NetworkManagerAdapter(dbusClient); await client.setUp(); - expect(client.hostname()).toEqual("testing-machine"); + expect(client.settings().hostname).toEqual("testing-machine"); + expect(client.settings().wireless).toEqual(false); }); }); }); diff --git a/web/src/components/network/Network.jsx b/web/src/components/network/Network.jsx index 96c77d42d2..b930ab2059 100644 --- a/web/src/components/network/Network.jsx +++ b/web/src/components/network/Network.jsx @@ -31,10 +31,12 @@ export default function Network() { const [initialized, setInitialized] = useState(false); const [connections, setConnections] = useState([]); const [wifiSelectorOpen, setWifiSelectorOpen] = useState(false); + const [wireless, setWireless] = useState(false); useEffect(() => { if (!initialized) return; + setWireless(client.network.settings().wireless); setConnections(client.network.activeConnections()); }, [client.network, initialized]); @@ -59,6 +61,11 @@ export default function Network() { case NetworkEventTypes.ACTIVE_CONNECTION_REMOVED: { setConnections(conns => conns.filter(c => c.id !== payload.id)); + break; + } + + case NetworkEventTypes.SETTINGS_UPDATED: { + setWireless(payload.wireless); } } }); @@ -81,10 +88,11 @@ export default function Network() { - - - setWifiSelectorOpen(false)} /> - + { wireless && + + + setWifiSelectorOpen(false)} /> + } ); } diff --git a/web/src/components/network/Network.test.jsx b/web/src/components/network/Network.test.jsx index 8e7adf476f..7571077e3a 100644 --- a/web/src/components/network/Network.test.jsx +++ b/web/src/components/network/Network.test.jsx @@ -20,7 +20,7 @@ */ import React from "react"; -import { screen, within } from "@testing-library/react"; +import { screen, within, waitFor } from "@testing-library/react"; import { installerRender } from "@/test-utils"; import Network from "@components/network/Network"; import { createClient } from "@client"; @@ -29,6 +29,9 @@ jest.mock("@client"); jest.mock("@components/network/NetworkWiredStatus", () => () => "Wired Connections"); jest.mock("@components/network/NetworkWifiStatus", () => () => "WiFi Connections"); +const networkSettings = { wireless: false, hostname: "test" }; +let settingsFn = jest.fn().mockReturnValue(networkSettings); + beforeEach(() => { createClient.mockImplementation(() => { return { @@ -37,32 +40,55 @@ beforeEach(() => { activeConnections: () => [], connections: () => Promise.resolve([]), accessPoints: () => [], - onNetworkEvent: jest.fn() + onNetworkEvent: jest.fn(), + settings: settingsFn } }; }); }); describe("Network", () => { - it("shows a link to open the WiFi selector", async () => { - installerRender(); - await screen.findByRole("button", { name: "Connect to a Wi-Fi network" }); + describe("when it has not been initialized", () => { + it("renders nothing", async () => { + const { container } = installerRender(, { usingLayout: false }); + await waitFor(() => expect(container).toBeEmptyDOMElement()); + }); }); - it("renders a summary for wired and wifi connections", async () => { - installerRender(); + describe("when it has been initialized", () => { + it("renders a summary for wired and wifi connections", async () => { + installerRender(); - await screen.findByText("Wired Connections"); - await screen.findByText("WiFi Connections"); - }); + await screen.findByText("Wired Connections"); + await screen.findByText("WiFi Connections"); + }); + + describe("when Wireless is currently not enabled", () => { + it("does not show a link to open the WiFi selector", async () => { + installerRender(, { usingLayout: false }); + await waitFor(() => expect(screen.queryByRole("button", { name: "Connect to a Wi-Fi network" })).not.toBeInTheDocument()); + }); + }); + + describe("when Wireless is currently enabled", () => { + beforeEach(() => { + settingsFn = jest.fn().mockReturnValue({ ...networkSettings, wireless: true }); + }); + + it("shows a link to open the WiFi selector", async () => { + installerRender(); + await screen.findByRole("button", { name: "Connect to a Wi-Fi network" }); + }); - describe("when the user clicks on connect to a Wi-Fi", () => { - it("opens the WiFi selector dialog", async () => { - const { user } = installerRender(); - const link = await screen.findByRole("button", { name: "Connect to a Wi-Fi network" }); - await user.click(link); - const wifiDialog = await screen.findByRole("dialog"); - within(wifiDialog).getByText("Connect to a Wi-Fi network"); + describe("when the user clicks on connect to a Wi-Fi", () => { + it("opens the WiFi selector dialog", async () => { + const { user } = installerRender(); + const link = await screen.findByRole("button", { name: "Connect to a Wi-Fi network" }); + await user.click(link); + const wifiDialog = await screen.findByRole("dialog"); + within(wifiDialog).getByText("Connect to a Wi-Fi network"); + }); + }); }); }); }); diff --git a/web/src/components/network/TargetIpsPopup.jsx b/web/src/components/network/TargetIpsPopup.jsx index 0d0c68e99c..b3f30d04e5 100644 --- a/web/src/components/network/TargetIpsPopup.jsx +++ b/web/src/components/network/TargetIpsPopup.jsx @@ -45,7 +45,7 @@ export default function TargetIpsPopup() { const refreshState = () => { setAddresses(client.network.addresses()); - setHostname(client.network.hostname()); + setHostname(client.network.settings().hostname); }; refreshState(); diff --git a/web/src/components/network/TargetIpsPopup.test.jsx b/web/src/components/network/TargetIpsPopup.test.jsx index d87fbf8aa1..95c6d4c375 100644 --- a/web/src/components/network/TargetIpsPopup.test.jsx +++ b/web/src/components/network/TargetIpsPopup.test.jsx @@ -34,7 +34,8 @@ const addresses = [ { address: "5.6.7.8", prefix: 16 }, ]; const addressFn = jest.fn().mockReturnValue(addresses); -const hostnameFn = jest.fn().mockReturnValue("example.net"); +const networkSettings = { wireless: false, hostname: "example.net" }; +const settingsFn = jest.fn().mockReturnValue(networkSettings); describe("TargetIpsPopup", () => { let callbacks; @@ -46,7 +47,7 @@ describe("TargetIpsPopup", () => { network: { onNetworkEvent: onNetworkEventFn, addresses: addressFn, - hostname: hostnameFn, + settings: settingsFn, setUp: jest.fn().mockResolvedValue() } }; @@ -77,7 +78,7 @@ describe("TargetIpsPopup", () => { await screen.findByRole("button", { name: /1.2.3.4\/24 \(example.net\)/i }); addressFn.mockReturnValue([{ address: "5.6.7.8", prefix: 24 }]); - hostnameFn.mockReturnValue("localhost.localdomain"); + settingsFn.mockReturnValue({ wireless: false, hostname: "localhost.localdomain" }); act(() => { callbacks.forEach(cb => cb()); }); diff --git a/web/src/components/network/WifiSelector.jsx b/web/src/components/network/WifiSelector.jsx index aa0e99787d..b873902d32 100644 --- a/web/src/components/network/WifiSelector.jsx +++ b/web/src/components/network/WifiSelector.jsx @@ -84,7 +84,7 @@ function WifiSelector({ isOpen = false, onClose }) { setNetworks(data); setActiveNetwork(networksFromValues(data).find(d => d.connection)); }); - }, [client.network, connections, activeConnections]); + }, [client.network, connections, activeConnections, isOpen]); useEffect(() => { return client.network.onNetworkEvent(({ type, payload }) => {