diff --git a/rust/data.json b/rust/data.json deleted file mode 100644 index a6b2691850..0000000000 --- a/rust/data.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "network": { - "connections": [ - { - "id": "eth0", - "method4": "auto", - "method6": "auto", - "addresses": [ - "192.168.0.101/24" - ], - "interface": "eth0", - "status": "up" - }, - { - "id": "Wired connection 1", - "method4": "auto", - "method6": "auto", - "interface": "enp5s0f4u2c2", - "status": "up" - }, - { - "id": "Sarambeque", - "method4": "auto", - "method6": "auto", - "wireless": { - "password": "vikingo.pass", - "security": "wpa-psk", - "ssid": "Sarambeque", - "mode": "infrastructure" - }, - "status": "up" - } - ] - } -} - diff --git a/web/src/client/network/index.js b/web/src/client/network/index.js index 103d177482..3b6fbeab98 100644 --- a/web/src/client/network/index.js +++ b/web/src/client/network/index.js @@ -20,34 +20,12 @@ */ // @ts-check +// +// @import { AccessPoint, Device } from "~/types/network.ts"; -import { - ConnectionState, - ConnectionTypes, - createAccessPoint, - createConnection, - DeviceState, - NetworkState, - securityFromFlags, -} from "./model"; -import { formatIp, ipPrefixFor } from "./utils"; +import { Connection, ConnectionState } from "~/types/network"; +import { formatIp } from "~/utils/network"; -const DeviceType = Object.freeze({ - LOOPBACK: 0, - ETHERNET: 1, - WIRELESS: 2, - DUMMY: 3, - BOND: 4, -}); - -/** - * @typedef {import("./model").NetworkSettings} NetworkSettings - * @typedef {import("./model").Connection} Connection - * @typedef {import("./model").Connection} Device - * @typedef {import("./model").Connection} Route - * @typedef {import("./model").IPAddress} IPAddress - * @typedef {import("./model").AccessPoint} AccessPoint - */ const NetworkEventTypes = Object.freeze({ DEVICE_ADDED: "deviceAdded", @@ -99,130 +77,6 @@ class NetworkClient { this.client = client; } - /** - * Returns the devices running configuration - * - * @return {Promise} - */ - async devices() { - const response = await this.client.get("/network/devices"); - if (!response.ok) { - return []; - } - - const devices = await response.json(); - return devices.map(this.fromApiDevice); - } - - /** - * Returns the device settings - * - * @param {object} device - device settings from the API server - * @return {Device} - */ - fromApiDevice(device) { - const nameservers = device?.ipConfig?.nameservers || []; - const { ipConfig = {}, ...dev } = device; - const routes4 = (ipConfig.routes4 || []).map((route) => { - const [ip, netmask] = route.destination.split("/"); - const destination = - netmask !== undefined ? { address: ip, prefix: ipPrefixFor(netmask) } : { address: ip }; - - return { ...route, destination }; - }); - - const routes6 = (ipConfig.routes6 || []).map((route) => { - const [ip, netmask] = route.destination.split("/"); - const destination = - netmask !== undefined ? { address: ip, prefix: ipPrefixFor(netmask) } : { address: ip }; - - return { ...route, destination }; - }); - - const addresses = (ipConfig.addresses || []).map((address) => { - const [ip, netmask] = address.split("/"); - if (netmask !== undefined) { - return { address: ip, prefix: ipPrefixFor(netmask) }; - } else { - return { address: ip }; - } - }); - - return { ...dev, ...ipConfig, addresses, nameservers, routes4, routes6 }; - } - - /** - * Returns the connection settings - * - * @return {Promise} - */ - async connections() { - const response = await this.client.get("/network/connections"); - if (!response.ok) { - console.error("Failed to get list of connections", response); - return []; - } - - const connections = await response.json(); - return connections.map(this.fromApiConnection); - } - - fromApiConnection(connection) { - const nameservers = connection.nameservers || []; - const addresses = (connection.addresses || []).map((address) => { - const [ip, netmask] = address.split("/"); - if (netmask !== undefined) { - return { address: ip, prefix: ipPrefixFor(netmask) }; - } else { - return { address: ip }; - } - }); - - return { ...connection, addresses, nameservers }; - } - - connectionType(connection) { - if (connection.wireless) return ConnectionTypes.WIFI; - if (connection.bond) return ConnectionTypes.BOND; - if (connection.vlan) return ConnectionTypes.VLAN; - if (connection.iface === "lo") return ConnectionTypes.LOOPBACK; - - return ConnectionTypes.ETHERNET; - } - - toApiConnection(connection) { - const addresses = (connection.addresses || []).map((addr) => formatIp(addr)); - const { iface, gateway4, gateway6, ...conn } = connection; - - if (gateway4?.trim() !== "") conn.gateway4 = gateway4; - if (gateway6?.trim() !== "") conn.gateway6 = gateway6; - - return { ...conn, addresses, interface: iface }; - } - - /** - * Returns the list of available wireless access points (AP) - * - * @return {Promise} - */ - async accessPoints() { - const response = await this.client.get("/network/wifi"); - if (!response.ok) { - console.error("Failed to get list of APs", response); - return []; - } - const access_points = await response.json(); - - return access_points.map((ap) => { - return createAccessPoint({ - ssid: ap.ssid, - hwAddress: ap.hw_address, - strength: ap.strength, - security: securityFromFlags(ap.flags, ap.wpaFlags, ap.rsnFlags), - }); - }); - } - /** * Connects to given Wireless network * @@ -241,60 +95,6 @@ class NetworkClient { return this.client.get(`/network/connections/${connection.id}/disconnect`); } - networkStateFor(state) { - switch (state) { - case DeviceState.CONFIG: - case DeviceState.IPCHECK: - // TRANSLATORS: Wifi network status - return NetworkState.CONNECTING; - case DeviceState.ACTIVATED: - // TRANSLATORS: Wifi network status - return NetworkState.CONNECTED; - case DeviceState.DEACTIVATING: - case DeviceState.FAILED: - case DeviceState.DISCONNECTED: - // TRANSLATORS: Wifi network status - return NetworkState.DISCONNECTED; - default: - return ""; - } - } - - loadNetworks(devices, connections, accessPoints) { - const knownSsids = []; - - return accessPoints - .sort((a, b) => b.strength - a.strength) - .reduce( - (networks, ap) => { - // Do not include networks without SSID - if (!ap.ssid || ap.ssid === "") return networks; - // Do not include "duplicates" - if (knownSsids.includes(ap.ssid)) return networks; - - const network = { - ...ap, - settings: connections.find((c) => c.wireless?.ssid === ap.ssid), - device: devices.find((c) => c.connection === ap.ssid), - }; - - // Group networks - if (network.device) { - networks.connected.push(network); - } else if (network.settings) { - networks.configured.push(network); - } else { - networks.others.push(network); - } - - knownSsids.push(network.ssid); - - return networks; - }, - { connected: [], configured: [], others: [] }, - ); - } - /** * Apply network changes */ @@ -302,52 +102,14 @@ class NetworkClient { return this.client.put("/network/system/apply", {}); } - /** - * Add the connection for the given Wireless network and activate it - * - * @param {string} ssid - Network id - * @param {object} options - connection options - */ - async addAndConnectTo(ssid, options) { - // duplicated code (see network manager adapter) - const wireless = { ssid, mode: "infrastructure" }; - if (options.security) wireless.security = options.security; - if (options.password) wireless.password = options.password; - if (options.hidden) wireless.hidden = options.hidden; - if (options.mode) wireless.mode = options.mode; - - const connection = createConnection({ - id: ssid, - wireless, - }); - - const conn = await this.addConnection(connection); - await this.apply(); - - // the connection is automatically activated when written - return conn; - } + toApiConnection(connection) { + const addresses = (connection.addresses || []).map((addr) => formatIp(addr)); + const { iface, gateway4, gateway6, ...conn } = connection; - /** - * Adds a new connection - * - * If a connection with the given ID already exists, it updates such a - * connection. - * - * @param {Connection} connection - Connection to add - * @return {Promise} the added connection - */ - async addConnection(connection) { - const response = await this.client.post( - "/network/connections", - this.toApiConnection(connection), - ); - if (!response.ok) { - console.error("Failed to post list of connections", response); - return null; - } + if (gateway4?.trim() !== "") conn.gateway4 = gateway4; + if (gateway6?.trim() !== "") conn.gateway6 = gateway6; - return response.json(); + return { ...conn, addresses, interface: iface }; } /** @@ -439,4 +201,4 @@ class NetworkClient { } } -export { ConnectionState, ConnectionTypes, NetworkClient, NetworkEventTypes }; +export { ConnectionState, NetworkClient, NetworkEventTypes }; diff --git a/web/src/client/network/model.js b/web/src/client/network/model.js index d28fcc849f..ef9d69deab 100644 --- a/web/src/client/network/model.js +++ b/web/src/client/network/model.js @@ -19,74 +19,9 @@ * find current contact information at www.suse.com. */ -// @ts-check - -/** - * Enum for the active connection state values - * - * @readonly - * @enum { number } - * https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMActiveConnectionState - */ -const ConnectionState = Object.freeze({ - UNKNOWN: 0, - ACTIVATING: 1, - ACTIVATED: 2, - DEACTIVATING: 3, - DEACTIVATED: 4, -}); - -const DeviceState = Object.freeze({ - UNKNOWN: "unknown", - UNMANAGED: "unmanaged", - UNAVAILABLE: "unavailable", - DISCONNECTED: "disconnected", - CONFIG: "config", - IPCHECK: "ipCheck", - NEEDAUTH: "needAuth", - ACTIVATED: "activated", - DEACTIVATING: "deactivating", - FAILED: "failed", -}); - -const NetworkState = Object.freeze({ - DISCONNECTED: "disconnected", - CONNECTING: "connecting", - CONNECTED: "connected" -}); +import { Connection, ConnectionState } from "~/types/network"; -/** - * Returns a human readable connection state - * - * @property {number} state - * @return {string} - */ -const connectionHumanState = (state) => { - const stateIndex = Object.values(ConnectionState).indexOf(state); - const stateKey = Object.keys(ConnectionState)[stateIndex]; - return stateKey.toLowerCase(); -}; - -/** - * @typedef {keyof ConnectionTypes} ConnectionType - */ - -const ConnectionTypes = Object.freeze({ - ETHERNET: "ethernet", - WIFI: "wireless", - LOOPBACK: "loopback", - BOND: "bond", - BRIDGE: "bridge", - VLAN: "vlan", - UNKNOWN: "unknown", -}); - -const SecurityProtocols = Object.freeze({ - WEP: "WEP", - WPA: "WPA1", - RSN: "WPA2", - _8021X: "802.1X", -}); +// @ts-check // security protocols // const AgamaSecurityProtocols = Object.freeze({ @@ -99,94 +34,6 @@ const SecurityProtocols = Object.freeze({ // WPA3Only: "wpa-eap-suite-b-192" // }); -const ApFlags = Object.freeze({ - NONE: 0x00000000, - PRIVACY: 0x00000001, - WPS: 0x00000002, - WPS_PBC: 0x00000004, - WPS_PIN: 0x00000008, -}); - -const ApSecurityFlags = Object.freeze({ - NONE: 0x00000000, - PAIR_WEP40: 0x00000001, - PAIR_WEP104: 0x00000002, - PAIR_TKIP: 0x00000004, - PAIR_CCMP: 0x00000008, - GROUP_WEP40: 0x00000010, - GROUP_WEP104: 0x00000020, - GROUP_TKIP: 0x00000040, - GROUP_CCMP: 0x00000080, - KEY_MGMT_PSK: 0x00000100, - KEY_MGMT_8021_X: 0x00000200, -}); - -/** - * @typedef {object} IPAddress - * @property {string} address - like "129.168.1.2" - * @property {number|string} prefix - like "16" - */ - -/** - * @typedef {object} Device - * @property {string} name - * @property {ConnectionType} type - * @property {IPAddress[]} addresses - * @property {string[]} nameservers - * @property {string} gateway4 - * @property {string} gateway6 - * @property {string} method4 - * @property {string} method6 - * @property {Route[]} routes4 - * @property {Route[]} routes6 - * @property {string} macAddress - * @property {string} [connection] - * @property {string} DeviceState - */ - -/** - * @typedef {object} Route - * @property {IPAddress} destination - * @property {string} next_hop - * @property {number} metric - */ - -/** - * @typedef {object} Connection - * @property {string} id - * @property {string} iface - * @property {IPAddress[]} addresses - * @property {string[]} nameservers - * @property {string} gateway4 - * @property {string} gateway6 - * @property {string} method4 - * @property {string} method6 - * @property {Wireless} [wireless] - */ - -/** - * @typedef {object} Wireless - * @property {string} password - * @property {string} ssid - * @property {string} security - * @property {boolean} hidden - */ - -/** - * @typedef {object} AccessPoint - * @property {string} ssid - * @property {number} strength - * @property {string} hwAddress - * @property {string[]} security - */ - -/** -* @typedef {object} NetworkSettings -* @property {boolean} connectivity -* @property {boolean} wireless_enabled -* @property {boolean} networking_enabled -* @property {string} hostname - /** * Returns a connection object * @@ -274,41 +121,8 @@ const createAccessPoint = ({ ssid, hwAddress, strength, security }) => ({ security: security || [], }); -/** - * @param {number} flags - AP flags - * @param {number} wpa_flags - AP WPA1 flags - * @param {number} rsn_flags - AP WPA2 flags - * @return {string[]} security protocols supported - */ -const securityFromFlags = (flags, wpa_flags, rsn_flags) => { - const security = []; - - if (flags & ApFlags.PRIVACY && wpa_flags === 0 && rsn_flags === 0) { - security.push(SecurityProtocols.WEP); - } - - if (wpa_flags > 0) { - security.push(SecurityProtocols.WPA); - } - if (rsn_flags > 0) { - security.push(SecurityProtocols.RSN); - } - if (wpa_flags & ApSecurityFlags.KEY_MGMT_8021_X || rsn_flags & ApSecurityFlags.KEY_MGMT_8021_X) { - security.push(SecurityProtocols._8021X); - } - - return security; -}; - export { - connectionHumanState, - ConnectionState, - ConnectionTypes, createAccessPoint, createConnection, createDevice, - DeviceState, - NetworkState, - securityFromFlags, - SecurityProtocols, }; diff --git a/web/src/components/network/ConnectionsTable.test.jsx b/web/src/components/network/ConnectionsTable.test.jsx index c9e360f4ec..0ed886fa21 100644 --- a/web/src/components/network/ConnectionsTable.test.jsx +++ b/web/src/components/network/ConnectionsTable.test.jsx @@ -23,7 +23,7 @@ import React from "react"; import { screen, waitFor, within } from "@testing-library/react"; import { plainRender } from "~/test-utils"; -import { ConnectionTypes } from "~/client/network"; +import { ConnectionType } from "~/types/network"; import ConnectionsTable from "~/components/network/ConnectionsTable"; jest.mock("~/client"); @@ -31,14 +31,14 @@ jest.mock("~/client"); const firstConnection = { id: "WiFi 1", iface: "wlan0", - type: ConnectionTypes.WIFI, + type: ConnectionType.WIFI, addresses: [{ address: "192.168.69.200", prefix: 24 }], }; const firstDevice = { name: "wlan0", connection: "WiFi 1", - type: ConnectionTypes.WIFI, + type: ConnectionType.WIFI, addresses: [{ address: "192.168.69.200", prefix: 24 }], macAddress: "AA:11:22:33:44::FF", }; @@ -46,14 +46,14 @@ const firstDevice = { const secondConnection = { id: "WiFi 2", iface: "wlan1", - type: ConnectionTypes.WIFI, + type: ConnectionType.WIFI, addresses: [{ address: "192.168.69.201", prefix: 24 }], }; const secondDevice = { name: "wlan1", connection: "WiFi 2", - type: ConnectionTypes.WIFI, + type: ConnectionType.WIFI, addresses: [{ address: "192.168.69.201", prefix: 24 }], macAddress: "AA:11:22:33:44::AA", }; diff --git a/web/src/components/network/ConnectionsTable.jsx b/web/src/components/network/ConnectionsTable.tsx similarity index 85% rename from web/src/components/network/ConnectionsTable.jsx rename to web/src/components/network/ConnectionsTable.tsx index 4dd892758e..3f5b3d8a52 100644 --- a/web/src/components/network/ConnectionsTable.jsx +++ b/web/src/components/network/ConnectionsTable.tsx @@ -24,32 +24,23 @@ import { useNavigate, generatePath } from "react-router-dom"; import { Table, Thead, Tr, Th, Tbody, Td } from "@patternfly/react-table"; import { RowActions } from "~/components/core"; import { Icon } from "~/components/layout"; -import { formatIp } from "~/client/network/utils"; import { PATHS } from "~/routes/network"; import { sprintf } from "sprintf-js"; import { _ } from "~/i18n"; - -/** - * @typedef {import("~/client/network/model").Device} Device - * @typedef {import("~/client/network/model").Connection} Connection - */ +import { Connection, Device } from "~/types/network"; +import { formatIp } from "~/utils/network"; /** * * Displays given connections in a table - * @component - * - * @param {object} props - * @param {Connection[]} props.connections - Connections to be shown - * @param {Device[]} props.devices - Connections to be shown - * @param {function} [props.onForget] - function to be called for forgetting a connection + */ -export default function ConnectionsTable({ connections, devices, onForget }) { +const ConnectionsTable = ({ connections, devices, onForget }: { connections: Connection[], devices: Device[], onForget: Function }): React.ReactNode => { const navigate = useNavigate(); if (connections.length === 0) return null; const connectionDevice = ({ id }) => devices.find(({ connection }) => id === connection); - const connectionAddresses = (connection) => { + const connectionAddresses = (connection: Connection) => { const device = connectionDevice(connection); const addresses = device ? device.addresses : connection.addresses; @@ -107,3 +98,5 @@ export default function ConnectionsTable({ connections, devices, onForget }) { ); } + +export default ConnectionsTable; diff --git a/web/src/components/network/IpAddressInput.jsx b/web/src/components/network/IpAddressInput.jsx index 5a7c783ef9..b6e7e7ac21 100644 --- a/web/src/components/network/IpAddressInput.jsx +++ b/web/src/components/network/IpAddressInput.jsx @@ -20,7 +20,7 @@ */ import React, { useState } from "react"; -import { isValidIp } from "~/client/network/utils"; +import { isValidIp } from "~/utils/network"; import { TextInput, ValidatedOptions } from "@patternfly/react-core"; import { _ } from "~/i18n"; diff --git a/web/src/components/network/IpPrefixInput.jsx b/web/src/components/network/IpPrefixInput.jsx index d2f5362250..3f8f64963f 100644 --- a/web/src/components/network/IpPrefixInput.jsx +++ b/web/src/components/network/IpPrefixInput.jsx @@ -20,7 +20,7 @@ */ import React, { useState } from "react"; -import { isValidIpPrefix } from "~/client/network/utils"; +import { isValidIpPrefix } from "~/utils/network"; import { TextInput, ValidatedOptions } from "@patternfly/react-core"; import { _ } from "~/i18n"; diff --git a/web/src/components/network/IpSettingsForm.jsx b/web/src/components/network/IpSettingsForm.jsx index 4b71787bc8..a3a2eae68e 100644 --- a/web/src/components/network/IpSettingsForm.jsx +++ b/web/src/components/network/IpSettingsForm.jsx @@ -20,7 +20,7 @@ */ import React, { useState } from "react"; -import { useLoaderData, useNavigate } from "react-router-dom"; +import { useNavigate, useParams } from "react-router-dom"; import { HelperText, HelperTextItem, @@ -40,6 +40,7 @@ import { Page } from "~/components/core"; import { AddressesDataList, DnsDataList } from "~/components/network"; import { _ } from "~/i18n"; import { sprintf } from "sprintf-js"; +import { useConnection, useConnectionMutation } from "~/queries/network"; const METHODS = { MANUAL: "manual", @@ -50,7 +51,9 @@ const usingDHCP = (method) => method === METHODS.AUTO; export default function IpSettingsForm() { const client = useInstallerClient(); - const connection = useLoaderData(); + const { name } = useParams(); + const connection = useConnection(name); + const setConnection = useConnectionMutation(); const navigate = useNavigate(); const [addresses, setAddresses] = useState(connection.addresses); const [nameservers, setNameservers] = useState( @@ -98,7 +101,6 @@ export default function IpSettingsForm() { setErrors({}); const nextErrors = {}; - if (!usingDHCP(method) && sanitizedAddresses.length === 0) { // TRANSLATORS: error message nextErrors.method = _("At least one address must be provided for selected mode"); @@ -125,12 +127,7 @@ export default function IpSettingsForm() { gateway4: gateway, nameservers: sanitizedNameservers.map((s) => s.address), }; - - client.network - .updateConnection(updatedConnection) - .then(navigate(-1)) - // TODO: better error reporting. By now, it sets an error for the whole connection. - .catch(({ message }) => setErrors({ object: message })); + setConnection.mutateAsync(updatedConnection).catch((error) => setErrors(error)).then(navigate(-1)); }; const renderError = (field) => { diff --git a/web/src/components/network/NetworkPage.jsx b/web/src/components/network/NetworkPage.jsx index dd1216d346..e09b71e895 100644 --- a/web/src/components/network/NetworkPage.jsx +++ b/web/src/components/network/NetworkPage.jsx @@ -19,21 +19,19 @@ * find current contact information at www.suse.com. */ -// @ts-check - import React from "react"; import { CardBody, Grid, GridItem } from "@patternfly/react-core"; import { ButtonLink, CardField, EmptyState, Page } from "~/components/core"; import { ConnectionsTable } from "~/components/network"; -import { formatIp } from "~/client/network/utils"; +import { _ } from "~/i18n"; +import { formatIp } from "~/utils/network"; +import { sprintf } from "sprintf-js"; import { useNetwork, useNetworkConfigChanges } from "~/queries/network"; import { PATHS } from "~/routes/network"; -import { sprintf } from "sprintf-js"; import { _ } from "~/i18n"; /** * Page component holding Network settings - * @component */ export default function NetworkPage() { const { connections, devices, settings } = useNetwork(); diff --git a/web/src/components/network/NetworkPage.test.jsx b/web/src/components/network/NetworkPage.test.jsx index f3c4c6e6bf..3c30df50a8 100644 --- a/web/src/components/network/NetworkPage.test.jsx +++ b/web/src/components/network/NetworkPage.test.jsx @@ -23,8 +23,8 @@ import React from "react"; import { screen, within } from "@testing-library/react"; import { installerRender } from "~/test-utils"; import NetworkPage from "~/components/network/NetworkPage"; -import { ConnectionTypes } from "~/client/network"; import { createClient } from "~/client"; +import { ConnectionType } from "~/types/network"; jest.mock("~/client"); @@ -58,7 +58,7 @@ const wiFiConnection = { const ethernetDevice = { name: "eth0", connection: "eth0", - type: ConnectionTypes.ETHERNET, + type: ConnectionType.ETHERNET, addresses: [{ address: "192.168.122.20", prefix: 24 }], macAddress: "00:11:22:33:44::55", }; @@ -66,7 +66,7 @@ const ethernetDevice = { const wifiDevice = { name: "wlan0", connection: "AgamaNetwork", - type: ConnectionTypes.WIFI, + type: ConnectionType.WIFI, addresses: [{ address: "192.168.69.200", prefix: 24 }], macAddress: "AA:11:22:33:44::FF", }; diff --git a/web/src/components/network/WifiConnectionForm.jsx b/web/src/components/network/WifiConnectionForm.jsx index 275a083303..b555eae158 100644 --- a/web/src/components/network/WifiConnectionForm.jsx +++ b/web/src/components/network/WifiConnectionForm.jsx @@ -31,10 +31,9 @@ import { TextInput, } from "@patternfly/react-core"; import { PasswordInput } from "~/components/core"; -import { useInstallerClient } from "~/context/installer"; -import { useNetworkConfigChanges } from "~/queries/network"; +import { useAddConnectionMutation, useNetworkConfigChanges } from "~/queries/network"; import { _ } from "~/i18n"; -import { useQueryClient } from "@tanstack/react-query"; +import { Connection, Wireless } from "~/types/network"; /* * FIXME: it should be moved to the SecurityProtocols enum that already exists or to a class based @@ -58,8 +57,7 @@ const securityFrom = (supported) => { }; export default function WifiConnectionForm({ network, onCancel, onSubmitCallback }) { - const { network: client } = useInstallerClient(); - const queryClient = useQueryClient(); + const addClient = useAddConnectionMutation(); const [error, setError] = useState(false); const [isConnecting, setIsConnecting] = useState(false); const [ssid, setSsid] = useState(network?.ssid || ""); @@ -77,13 +75,9 @@ export default function WifiConnectionForm({ network, onCancel, onSubmitCallback if (typeof onSubmitCallback === "function") { onSubmitCallback({ ssid, password, hidden, security: [security] }); } - - client - .addAndConnectTo(ssid, { security, password, hidden }) - .catch(() => setError(true)) - .finally( - () => setIsConnecting(false) && queryClient.invalidateQueries({ queryKey: ["network"] }), - ); + const wireless = new Wireless(ssid, { security, password, hidden }); + const connection = new Connection(ssid, { wireless }); + addClient.mutate(connection); }; return ( diff --git a/web/src/components/network/WifiNetworksListPage.jsx b/web/src/components/network/WifiNetworksListPage.jsx index 12e362a298..1862b90bc2 100644 --- a/web/src/components/network/WifiNetworksListPage.jsx +++ b/web/src/components/network/WifiNetworksListPage.jsx @@ -48,12 +48,11 @@ import { useQueryClient } from "@tanstack/react-query"; import { Icon } from "~/components/layout"; import { WifiConnectionForm } from "~/components/network"; import { ButtonLink } from "~/components/core"; -import { DeviceState } from "~/client/network/model"; -import { formatIp } from "~/client/network/utils"; -import { useInstallerClient } from "~/context/installer"; -import { useSelectedWifi, useSelectedWifiChange } from "~/queries/network"; import { PATHS } from "~/routes/network"; +import { DeviceState } from "~/types/network"; import { _ } from "~/i18n"; +import { formatIp } from "~/utils/network"; +import { useRemoveConnectionMutation, useSelectedWifi, useSelectedWifiChange } from "~/queries/network"; const HIDDEN_NETWORK = Object.freeze({ hidden: true }); @@ -89,12 +88,15 @@ const ConnectionData = ({ network }) => { }; const WifiDrawerPanelBody = ({ network, onCancel }) => { - const client = useInstallerClient(); - const queryClient = useQueryClient(); + const setConnection = useRemoveConnectionMutation(); const { data } = useSelectedWifi(); const forgetNetwork = async () => { +<<<<<<< HEAD await client.network.deleteConnection(network.settings.id); queryClient.invalidateQueries({ queryKey: ["network", "connections"] }); +======= + setConnection.mutate(network.settings.id); +>>>>>>> 65493afcb (Getting rid of network client) }; if (!network) return; @@ -108,7 +110,7 @@ const WifiDrawerPanelBody = ({ network, onCancel }) => { if (network.settings && !network.device) { return ( - await client.network.connectTo(network.settings)}> + await network.connectTo(network.settings)}> {_("Connect")} @@ -132,7 +134,7 @@ const WifiDrawerPanelBody = ({ network, onCancel }) => { - await client.network.disconnect(network.settings)}> + await network.disconnect(network.settings)}> {_("Disconnect")} @@ -141,7 +143,7 @@ const WifiDrawerPanelBody = ({ network, onCancel }) => { diff --git a/web/src/components/storage/iscsi/DiscoverForm.jsx b/web/src/components/storage/iscsi/DiscoverForm.jsx index 788d9dc075..4adadb4c1c 100644 --- a/web/src/components/storage/iscsi/DiscoverForm.jsx +++ b/web/src/components/storage/iscsi/DiscoverForm.jsx @@ -25,7 +25,7 @@ import { Alert, Form, FormGroup, TextInput } from "@patternfly/react-core"; import { FormValidationError, Popup } from "~/components/core"; import { AuthFields } from "~/components/storage/iscsi"; import { useLocalStorage } from "~/utils"; -import { isValidIp } from "~/client/network/utils"; +import { isValidIp } from "~/utils/network"; import { _ } from "~/i18n"; const defaultData = { diff --git a/web/src/queries/network.js b/web/src/queries/network.ts similarity index 60% rename from web/src/queries/network.js rename to web/src/queries/network.ts index f476b72a89..7e4234c2ff 100644 --- a/web/src/queries/network.js +++ b/web/src/queries/network.ts @@ -22,15 +22,15 @@ import React from "react"; import { useQueryClient, useMutation, useSuspenseQuery, useSuspenseQueries } from "@tanstack/react-query"; import { useInstallerClient } from "~/context/installer"; -import { DeviceState, createAccessPoint, securityFromFlags } from "~/client/network/model"; -import { ipPrefixFor } from "~/client/network/utils"; +import { createAccessPoint } from "~/client/network/model"; +import { _ } from "~/i18n"; +import { AccessPoint, Connection, Device, DeviceState } from "~/types/network"; +import { formatIp, ipPrefixFor, securityFromFlags } from "~/utils/network"; + /** * Returns the device settings - * - * @param {object} device - device settings from the API server - * @return {Device} */ -const fromApiDevice = (device) => { +const fromApiDevice = (device: object): Device => { const nameservers = (device?.ipConfig?.nameservers || []); const { ipConfig = {}, ...dev } = device; const routes4 = (ipConfig.routes4 || []).map((route) => { @@ -59,7 +59,7 @@ const fromApiDevice = (device) => { return { ...dev, ...ipConfig, addresses, nameservers, routes4, routes6 }; }; -const fromApiConnection = (connection) => { +const fromApiConnection = (connection: object): Connection => { const nameservers = (connection.nameservers || []); const addresses = (connection.addresses || []).map((address) => { const [ip, netmask] = address.split("/"); @@ -73,6 +73,51 @@ const fromApiConnection = (connection) => { return { ...connection, addresses, nameservers }; }; +const toApiConnection = (connection: Connection): object => { + const addresses = (connection.addresses || []).map((addr) => formatIp(addr)); + const { iface, gateway4, gateway6, ...conn } = connection; + + if (gateway4?.trim() !== "") conn.gateway4 = gateway4; + if (gateway6?.trim() !== "") conn.gateway6 = gateway6; + + return { ...conn, addresses, interface: iface }; +}; + +const loadNetworks = (devices: Device[], connections: Connection[], accessPoints: AccessPoint[]) => { + const knownSsids = []; + + return accessPoints + .sort((a, b) => b.strength - a.strength) + .reduce( + (networks, ap) => { + // Do not include networks without SSID + if (!ap.ssid || ap.ssid === "") return networks; + // Do not include "duplicates" + if (knownSsids.includes(ap.ssid)) return networks; + + const network = { + ...ap, + settings: connections.find((c) => c.wireless?.ssid === ap.ssid), + device: devices.find((c) => c.connection === ap.ssid), + }; + + // Group networks + if (network.device) { + networks.connected.push(network); + } else if (network.settings) { + networks.configured.push(network); + } else { + networks.others.push(network); + } + + knownSsids.push(network.ssid); + + return networks; + }, + { connected: [], configured: [], others: [] }, + ); +}; + /** * Returns a query for retrieving the network configuration */ @@ -97,6 +142,19 @@ const devicesQuery = () => ({ staleTime: Infinity }); +/** + * Returns a query for retrieving the list of known connections + */ +const connectionQuery = (name) => ({ + queryKey: ["network", "connections", name], + queryFn: async () => { + const response = await fetch(`/api/network/connections/${name}`); + const connection = await response.json(); + return fromApiConnection(connection); + }, + staleTime: Infinity +}); + /** * Returns a query for retrieving the list of known connections */ @@ -131,21 +189,87 @@ const accessPointsQuery = () => ({ staleTime: Infinity }); +/** + * Hook that builds a mutation to add a new network connection + * + * It does not require to call `useMutation`. + */ +const useAddConnectionMutation = () => { + const queryClient = useQueryClient(); + const query = { + mutationFn: (newConnection) => + fetch("/api/network/connections", { + method: "POST", + body: JSON.stringify(toApiConnection(newConnection)), + headers: { + "Content-Type": "application/json", + } + }).then((response) => { + if (response.ok) { + return fetch(`/api/network/system/apply`, { method: "PUT" }); + } else { + throw new Error(_("Please, try again")); + } + }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["network"] }) + queryClient.invalidateQueries({ queryKey: ["network"] }) + } + }; + return useMutation(query); +}; /** * Hook that builds a mutation to update a network connections * * It does not require to call `useMutation`. */ const useConnectionMutation = () => { + const queryClient = useQueryClient(); const query = { mutationFn: (newConnection) => - fetch(`/api/network/connection/${newConnection.id}`, { + fetch(`/api/network/connections/${newConnection.id + }`, { method: "PUT", - body: JSON.stringify(newConnection), + body: JSON.stringify(toApiConnection(newConnection)), headers: { "Content-Type": "application/json", - }, - }) + } + }).then((response) => { + if (response.ok) { + return fetch(`/api/network/system/apply`, { method: "PUT" }); + } else { + throw new Error(_("Please, try again")); + } + }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["network", "connections"] }) + queryClient.invalidateQueries({ queryKey: ["network", "devices"] }) + } + }; + return useMutation(query); +}; + +/** + * Hook that builds a mutation to remove a network connection + * + * It does not require to call `useMutation`. + */ +const useRemoveConnectionMutation = () => { + const queryClient = useQueryClient(); + const query = { + mutationFn: (name) => + fetch(`/api/network/connections/${name}`, { method: "DELETE" }) + .then((response) => { + if (response.ok) { + return fetch(`/api/network/system/apply`, { method: "PUT" }); + } else { + throw new Error(_("Please, try again")); + } + }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["network", "connections"] }) + queryClient.invalidateQueries({ queryKey: ["network", "devices"] }) + } }; return useMutation(query); }; @@ -225,6 +349,11 @@ const useNetworkConfigChanges = () => { }, [client, queryClient, changeSelected]); }; +const useConnection = (name) => { + const { data } = useSuspenseQuery(connectionQuery(name)); + return data; +} + const useNetwork = () => { const [ { data: state }, @@ -239,8 +368,7 @@ const useNetwork = () => { accessPointsQuery() ] }); - const client = useInstallerClient(); - const networks = client.network.loadNetworks(devices, connections, accessPoints); + const networks = loadNetworks(devices, connections, accessPoints); return { connections, settings: state, devices, accessPoints, networks }; }; @@ -249,10 +377,14 @@ const useNetwork = () => { export { stateQuery, devicesQuery, + connectionQuery, connectionsQuery, accessPointsQuery, selectedWiFiNetworkQuery, + useAddConnectionMutation, useConnectionMutation, + useRemoveConnectionMutation, + useConnection, useNetwork, useSelectedWifi, useSelectedWifiChange, diff --git a/web/src/types/network.ts b/web/src/types/network.ts new file mode 100644 index 0000000000..060668d3f8 --- /dev/null +++ b/web/src/types/network.ts @@ -0,0 +1,180 @@ +enum ApFlags { + NONE = 0x00000000, + PRIVACY = 0x00000001, + WPS = 0x00000002, + WPS_PBC = 0x00000004, + WPS_PIN = 0x00000008, +}; + +enum ApSecurityFlags { + NONE = 0x00000000, + PAIR_WEP40 = 0x00000001, + PAIR_WEP104 = 0x00000002, + PAIR_TKIP = 0x00000004, + PAIR_CCMP = 0x00000008, + GROUP_WEP40 = 0x00000010, + GROUP_WEP104 = 0x00000020, + GROUP_TKIP = 0x00000040, + GROUP_CCMP = 0x00000080, + KEY_MGMT_PSK = 0x00000100, + KEY_MGMT_8021_X = 0x00000200, +}; + +enum ConnectionType { + ETHERNET = "ethernet", + WIFI = "wireless", + LOOPBACK = "loopback", + BOND = "bond", + BRIDGE = "bridge", + VLAN = "vlan", + UNKNOWN = "unknown", +}; + +/** + * Enum for the active connection state values + * + * @readonly + * @enum { number } + * https://networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMActiveConnectionState + */ +enum ConnectionState { + UNKNOWN = 0, + ACTIVATING = 1, + ACTIVATED = 2, + DEACTIVATING = 3, + DEACTIVATED = 4, +}; + +enum DeviceState { + UNKNOWN = "unknown", + UNMANAGED = "unmanaged", + UNAVAILABLE = "unavailable", + DISCONNECTED = "disconnected", + CONFIG = "config", + IPCHECK = "ipCheck", + NEEDAUTH = "needAuth", + ACTIVATED = "activated", + DEACTIVATING = "deactivating", + FAILED = "failed", +}; + +enum DeviceType { + LOOPBACK = 0, + ETHERNET = 1, + WIRELESS = 2, + DUMMY = 3, + BOND = 4, +}; + +enum NetworkState { + DISCONNECTED = "disconnected", + CONNECTING = "connecting", + CONNECTED = "connected" +}; + +enum SecurityProtocols { + WEP = "WEP", + WPA = "WPA1", + RSN = "WPA2", + _8021X = "802.1X", +}; + +type IPAddress = { + address: string; + prefix: number | string; +}; + +type Route = { + destination: IPAddress; + nextHop: string; + metric: number; +}; + +type AccessPoint = { + ssid: string, + strength: number, + hwAddress: string, + security: string[] +} + +type Device = { + name: string; + type: ConnectionType; + addresses: IPAddress[]; + nameservers: string; + gateway4: string; + gateway6: string; + method4: string; + method6: string; + routes4: Route[]; + routes6: Route[]; + macAddress: string; + state: DeviceState; + connection?: string; +}; + +type ConnectionApi = { + id: string; + iface: string; + addresses: IPAddress[]; + nameservers: string[]; + gateway4: string; + gateway6: string; + method4: string; + method6: string; + wireless?: Wireless; +} + +class Wireless { + password?: string; + security?: string; + hidden?: boolean = false; + mode: string = "infrastructure"; + + constructor(password?: string, security?: string, hidden?: boolean, mode?: string) { + if (security) this.security = security; + if (password) this.password = password; + if (hidden !== undefined) this.hidden = hidden; + if (mode) this.mode = mode; + } +} + +class Connection { + id: string; + iface: string; + addresses: IPAddress[] = []; + nameservers: string[] = []; + gateway4: string = ""; + gateway6: string = ""; + method4: string = "auto"; + method6: string = "auto"; + wireless?: Wireless; + + constructor(id: string, iface?: string, options?: Connection) { + this.id = id; + if (iface !== undefined) { + this.iface = iface; + } + + if (options !== undefined) { + if (options.addresses) this.addresses = options.addresses; + if (options.nameservers) this.nameservers = options.nameservers; + if (options.gateway4) this.gateway4 = options.gateway4; + if (options.gateway6) this.gateway6 = options.gateway6; + if (options.method4) this.method4 = options.method4; + if (options.method6) this.method6 = options.method6; + if (options.wireless) this.wireless = options.wireless; + } + } +} + + +type NetworkGeneralState = { + connectivity: boolean, + hostname: string, + networking_enabled: boolean, + wireless_enabled: boolean, +} + +export { ApFlags, ApSecurityFlags, Connection, ConnectionType, ConnectionState, DeviceState, NetworkState, DeviceType, Wireless, SecurityProtocols }; +export type { AccessPoint, Device, IPAddress, NetworkGeneralState }; diff --git a/web/src/client/network/utils.test.js b/web/src/utils/network.test.ts similarity index 99% rename from web/src/client/network/utils.test.js rename to web/src/utils/network.test.ts index be79d3b627..6d3b58228d 100644 --- a/web/src/client/network/utils.test.js +++ b/web/src/utils/network.test.ts @@ -28,7 +28,7 @@ import { stringToIPInt, formatIp, ipPrefixFor, -} from "./utils"; +} from "./network"; describe("#isValidIp", () => { it("returns true when the IP is valid", () => { diff --git a/web/src/client/network/utils.js b/web/src/utils/network.ts similarity index 63% rename from web/src/client/network/utils.js rename to web/src/utils/network.ts index b1dc153f1f..cc30afb738 100644 --- a/web/src/client/network/utils.js +++ b/web/src/utils/network.ts @@ -19,13 +19,20 @@ * find current contact information at www.suse.com. */ -// @ts-check - import ipaddr from "ipaddr.js"; +import { ApFlags, ApSecurityFlags, ConnectionState, IPAddress, SecurityProtocols } from "~/types/network"; /** - * @typedef {import("./model").IPAddress} IPAddress + * Returns a human readable connection state + * + * @property {number} state + * @return {string} */ +const connectionHumanState = (state: number): string => { + const stateIndex = Object.values(ConnectionState).indexOf(state); + const stateKey = Object.keys(ConnectionState)[stateIndex]; + return stateKey.toLowerCase(); +}; /** * Check if an IP is valid @@ -35,7 +42,7 @@ import ipaddr from "ipaddr.js"; * @param {string} value - An IP Address * @return {boolean} true if given IP is valid; false otherwise. */ -const isValidIp = (value) => ipaddr.IPv4.isValidFourPartDecimal(value); +const isValidIp = (value: string): boolean => ipaddr.IPv4.isValidFourPartDecimal(value); /** * Check if a value is a valid netmask or network prefix @@ -45,7 +52,7 @@ const isValidIp = (value) => ipaddr.IPv4.isValidFourPartDecimal(value); * @param {string} value - An netmask or a network prefix * @return {boolean} true if given IP is valid; false otherwise. */ -const isValidIpPrefix = (value) => { +const isValidIpPrefix = (value: string): boolean => { if (value.match(/^\d+$/)) { return parseInt(value) <= 32; } else { @@ -61,7 +68,7 @@ const isValidIpPrefix = (value) => { * @param {string} value - An netmask or a network prefix * @return {number} prefix for the given netmask or prefix */ -const ipPrefixFor = (value) => { +const ipPrefixFor = (value: string): number => { if (value.match(/^\d+$/)) { return parseInt(value); } else { @@ -77,7 +84,7 @@ const ipPrefixFor = (value) => { * @param {number} address - An IP Address as network byte-order * @return {string|null} the address given as a string */ -const intToIPString = (address) => { +const intToIPString = (address: number): string | null => { const ip = ipaddr.parse(address.toString()); if ("octets" in ip) { return ip.octets.reverse().join("."); @@ -93,7 +100,7 @@ const intToIPString = (address) => { * @param {string} text - string representing an IPv4 address * @return {number} IP address as network byte-order */ -const stringToIPInt = (text) => { +const stringToIPInt = (text: string): number => { if (text === "") return 0; const parts = text.split("."); @@ -114,7 +121,7 @@ const stringToIPInt = (text) => { * @param {IPAddress} addr * @return {string} */ -const formatIp = (addr) => { +const formatIp = (addr: IPAddress): string => { if (addr.prefix === undefined) { return `${addr.address}`; } else { @@ -122,4 +129,33 @@ const formatIp = (addr) => { } }; -export { isValidIp, isValidIpPrefix, intToIPString, stringToIPInt, formatIp, ipPrefixFor }; +/** + * @param {number} flags - AP flags + * @param {number} wpa_flags - AP WPA1 flags + * @param {number} rsn_flags - AP WPA2 flags + * @return {string[]} security protocols supported + */ + +const securityFromFlags = (flags: number, wpa_flags: number, rsn_flags: number): string[] => { + const security = []; + + if (flags & ApFlags.PRIVACY && wpa_flags === 0 && rsn_flags === 0) { + security.push(SecurityProtocols.WEP); + } + + if (wpa_flags > 0) { + security.push(SecurityProtocols.WPA); + } + if (rsn_flags > 0) { + security.push(SecurityProtocols.RSN); + } + if (wpa_flags & ApSecurityFlags.KEY_MGMT_8021_X || rsn_flags & ApSecurityFlags.KEY_MGMT_8021_X) { + security.push(SecurityProtocols._8021X); + } + + return security; +}; + + + +export { connectionHumanState, isValidIp, isValidIpPrefix, intToIPString, stringToIPInt, formatIp, ipPrefixFor, securityFromFlags };