From d3f6d261660829dd87444ad10fceba08ee9e6298 Mon Sep 17 00:00:00 2001 From: Freya Gustavsson Date: Tue, 21 Apr 2026 14:38:58 +0200 Subject: [PATCH] WIP! Anaconda Signed-off-by: Freya Gustavsson --- pkg/lib/anaconda/_anaconda.scss | 6 +- pkg/networkmanager/anaconda-main.css | 5 + pkg/networkmanager/anaconda-main.tsx | 122 ++++++++++++++++++ pkg/networkmanager/interfaces.js | 18 +++ pkg/networkmanager/network-interface.jsx | 157 ++++++++++++++--------- pkg/networkmanager/network-main.jsx | 16 +-- pkg/networkmanager/networking.scss | 7 + pkg/networkmanager/networkmanager.jsx | 23 +++- pkg/packagekit/updates.jsx | 9 +- test/verify/check-networkmanager-wifi | 1 + 10 files changed, 283 insertions(+), 81 deletions(-) create mode 100644 pkg/networkmanager/anaconda-main.css create mode 100644 pkg/networkmanager/anaconda-main.tsx diff --git a/pkg/lib/anaconda/_anaconda.scss b/pkg/lib/anaconda/_anaconda.scss index 06b99354fced..0e4bb87689fe 100644 --- a/pkg/lib/anaconda/_anaconda.scss +++ b/pkg/lib/anaconda/_anaconda.scss @@ -30,9 +30,9 @@ --pf-v6-c-page--BackgroundColor: var(--pf-t--global--background--color--primary--default); } - .pf-v6-c-card { - --pf-v6-c-card--BorderColor: transparent; - } + // .pf-v6-c-card { + // --pf-v6-c-card--BorderColor: transparent; + // } // Approximates PageSection padding={{ default: "noPadding" }} isFilled={false} (PF .pf-m-no-padding / .pf-m-no-fill) .pf-v6-c-page__main-section { diff --git a/pkg/networkmanager/anaconda-main.css b/pkg/networkmanager/anaconda-main.css new file mode 100644 index 000000000000..b32fa5fa577a --- /dev/null +++ b/pkg/networkmanager/anaconda-main.css @@ -0,0 +1,5 @@ +#network-interface { + .pf-v6-c-card.pf-m-plain .pf-v6-c-card__header, .pf-v6-c-card.pf-m-plain > .pf-v6-c-card__title { + padding-block-start: 0; + } +} diff --git a/pkg/networkmanager/anaconda-main.tsx b/pkg/networkmanager/anaconda-main.tsx new file mode 100644 index 000000000000..a8876d3cb111 --- /dev/null +++ b/pkg/networkmanager/anaconda-main.tsx @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2026 Red Hat, Inc. + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +import cockpit from "cockpit"; +import React, { useState } from 'react'; + +import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js"; +import { Page, } from "@patternfly/react-core/dist/esm/components/Page/index.js"; + +import { + has_group, + is_loopback, + is_managed, + is_wireless, + render_active_connection, +} from './interfaces.js'; +import { Content, ContentVariants, SimpleList, SimpleListGroup, SimpleListItem, Split, SplitItem } from "@patternfly/react-core"; +import { NetworkInterfacePage } from "./network-interface.jsx"; +import "./anaconda-main.css"; +import { EmptyStatePanel } from "cockpit-components-empty-state.js"; + +const _ = cockpit.gettext; + +interface AnacondaNetworkPageProps { + privileged: boolean; + operationInProgress: boolean; + usage_monitor: any; + interfaces: any[]; + iface?: any; +} + +interface AnacondaActiveNetwork { + isWireless?: boolean; + iface: any; +} + +export const AnacondaNetworkPage = ({ privileged, operationInProgress, usage_monitor, interfaces }: AnacondaNetworkPageProps) => { + const [active, setActive] = useState(); + + const managedWired: React.ReactNode[] = []; + const managedWireless: React.ReactNode[] = []; + + interfaces.forEach(iface => { + // Skip loopback + if (is_loopback(iface)) + return; + + // Skip members + else if (has_group(iface)) + return; + + const dev = iface.Device; + + const activeConnection = render_active_connection(dev, false, true); + const isWireless = is_wireless(iface); + + let connectionStatus; + if (activeConnection) { + connectionStatus = _("Connected") + } else { + connectionStatus = _("Disconnected") + } + + const row = ( + {setActive({isWireless, iface})}}> + + {iface.Name} + + {connectionStatus} + + + + ) + + if (!dev || is_managed(dev)) { + isWireless ? managedWireless.push(row) : managedWired.push(row); + } + }); + + return ( + + {_("Networks")} + + + + {managedWireless.length !== 0 && ( + {...managedWireless} + )} + {managedWired.length !== 0 && ( + {...managedWired} + )} + {(managedWireless.length === 0 && managedWired.length === 0) && ( + {_("No networks found")} + )} + + + + {active?.iface ? + : + + } + + + + + ); +}; diff --git a/pkg/networkmanager/interfaces.js b/pkg/networkmanager/interfaces.js index 73f641f4128e..72045084dc3e 100644 --- a/pkg/networkmanager/interfaces.js +++ b/pkg/networkmanager/interfaces.js @@ -2105,6 +2105,24 @@ export function apply_group_member(choices, model, apply_group, group_connection }); } +export function is_loopback(iface) { + return iface.Name == "lo" || (iface.Device && iface.Device.DeviceType == "loopback"); +} + +export function has_group(iface) { + return ( + (iface.Device && + iface.Device.ActiveConnection && + iface.Device.ActiveConnection.Group && + iface.Device.ActiveConnection.Group.Members.length > 0) || + (iface.MainConnection && iface.MainConnection.Groups.length > 0) + ); +} + +export function is_wireless(iface) { + return iface.Device?.DeviceType === '802-11-wireless'; +} + export function init() { cockpit.translate(); } diff --git a/pkg/networkmanager/network-interface.jsx b/pkg/networkmanager/network-interface.jsx index 43aa0dcb1996..ee8c2a1cad41 100644 --- a/pkg/networkmanager/network-interface.jsx +++ b/pkg/networkmanager/network-interface.jsx @@ -21,7 +21,7 @@ import { SearchInput } from "@patternfly/react-core/dist/esm/components/SearchIn import { Spinner } from "@patternfly/react-core/dist/esm/components/Spinner/index.js"; import { Switch } from "@patternfly/react-core/dist/esm/components/Switch/index.js"; import { Tooltip } from "@patternfly/react-core/dist/esm/components/Tooltip/index.js"; -import { SortByDirection } from '@patternfly/react-table'; +import { ActionsColumn, SortByDirection } from '@patternfly/react-table'; import { ConnectedIcon, DisconnectedIcon, @@ -225,6 +225,8 @@ export const NetworkInterfacePage = ({ const [prevAPCount, setPrevAPCount] = useState(0); const [networkSearch, setNetworkSearch] = useState(""); + const anaconda = in_anaconda_mode(); + const dev_name = iface.Name; const dev = iface.Device; const isManaged = iface && (!dev || is_managed(dev)); @@ -432,7 +434,14 @@ export const NetworkInterfacePage = ({ mac_desc = mac; } - return mac_desc; + return ( + + {_("MAC")} + + { mac_desc } + + + ); } function renderCarrierStatusRow() { @@ -980,47 +989,49 @@ export const NetworkInterfacePage = ({ ); } else { actionColumn = ( - <> - - - - {ap.Connection && ( - <> - {" "} - forgetNetwork(ap)} - aria-label={_("Forget")}> - {_("Forget")} - - ]} /> - - )} - + + + ); } + let rowActions = <>; + if (ap.Connection) { + rowActions = forgetNetwork(ap), + isDanger: true, + "aria-label": _("Forget") + } + ]} + isDisabled={!privileged} + />; + } + + const networkColumns = [ + { title: nameColumn, sortKey: ap.Ssid, header: true }, + { title: <>{securityIcon} {ap.Mode}, sortKey: ap.Mode }, + { title: signalColumn, sortKey: String(ap.Strength).padStart(3, '0') }, + ]; + if (!anaconda) { + networkColumns.push({ title: cockpit.format_bits_per_sec(ap.MaxBitrate * 1000) }); + } + networkColumns.push({ title: {actionColumn}, props: { hasAction: true } }); + networkColumns.push({ title: rowActions, props: { isActionCell: true } }); + return { - columns: [ - { title: nameColumn, sortKey: ap.Ssid, header: true }, - { title: <>{securityIcon} {ap.Mode}, sortKey: ap.Mode }, - { title: signalColumn, sortKey: String(ap.Strength).padStart(3, '0') }, - { title: cockpit.format_bits_per_sec(ap.MaxBitrate * 1000) }, - { title: actionColumn }, - ], + columns: networkColumns, props: { key: ap.HwAddress, "data-ssid": ap.Ssid, "data-known": !!ap.Connection } }; }); @@ -1028,18 +1039,38 @@ export const NetworkInterfacePage = ({ // Add aggregated hidden access points row at the bottom if (dev.hiddenAPCount > 0) { const hiddenLabel = cockpit.ngettext("$0 hidden network", "$0 hidden networks", dev.hiddenAPCount); + + const networkHiddenColumns = [ + { title: cockpit.format(hiddenLabel, dev.hiddenAPCount), sortKey: "zzz-hidden", header: true }, + { title: "" }, + { title: "" }, + { title: "" }, + { title: "" }, + ]; + + if (!anaconda) { + networkHiddenColumns.push({ title: "" }); + } + networkHiddenColumns.push({ title: "" }); + + rows.push({ - columns: [ - { title: cockpit.format(hiddenLabel, dev.hiddenAPCount), sortKey: "zzz-hidden", header: true }, - { title: "" }, - { title: "" }, - { title: "" }, - { title: "" }, - ], + columns: networkHiddenColumns, props: { key: "hidden-networks", "data-hidden": true } }); } + const listingColumns = [ + { title: _("Network"), header: true, sortable: true }, + { title: _("Mode") }, + { title: _("Signal"), sortable: true }, + ]; + + if (!anaconda) { + listingColumns.push({ title: _("Rate") }); + } + listingColumns.push({ title: "", props: { screenReaderText: _("Actions") } }); + return ( @@ -1112,7 +1137,7 @@ export const NetworkInterfacePage = ({ }; const cs = con && connection_settings(con); - if (!con || (cs.type != "bond" && cs.type != "team" && cs.type != "bridge")) { + if (plot_state && (!con || (cs.type != "bond" && cs.type != "team" && cs.type != "bridge"))) { plot_state.plot_instances('rx', rx_plot_data, [dev_name], true); plot_state.plot_instances('tx', tx_plot_data, [dev_name], true); return null; @@ -1120,7 +1145,7 @@ export const NetworkInterfacePage = ({ const plot_ifaces = []; - con.Members.forEach(member_con => { + con && con.Members.forEach(member_con => { member_con.Interfaces.forEach(iface => { if (iface.MainConnection != member_con) return; @@ -1140,8 +1165,10 @@ export const NetworkInterfacePage = ({ }); }); - plot_state.plot_instances('rx', rx_plot_data, plot_ifaces, true); - plot_state.plot_instances('tx', tx_plot_data, plot_ifaces, true); + if (plot_state) { + plot_state.plot_instances('rx', rx_plot_data, plot_ifaces, true); + plot_state.plot_instances('tx', tx_plot_data, plot_ifaces, true); + } const sorted_members = Object.keys(members).sort() .map(name => members[name]); @@ -1208,12 +1235,11 @@ export const NetworkInterfacePage = ({ const settingsRows = renderConnectionSettingsRows(iface.MainConnection, connectionSettings) .map((component, idx) => {component}); - const anaconda = in_anaconda_mode(); - return ( + { !anaconda ? ( @@ -1224,9 +1250,12 @@ export const NetworkInterfacePage = ({ - - - + ) : <> } + { plot_state && + + + + } @@ -1246,12 +1275,12 @@ export const NetworkInterfacePage = ({ {dev_name} {renderDesc()} - {renderMac()} {renderActiveStatusRow()} + {renderMac()} {renderCarrierStatusRow()} {settingsRows} @@ -1264,7 +1293,7 @@ export const NetworkInterfacePage = ({ } {renderWiFiNetworks()} - {renderConnectionMembers(iface.MainConnection)} + { !anaconda && renderConnectionMembers(iface.MainConnection)} diff --git a/pkg/networkmanager/network-main.jsx b/pkg/networkmanager/network-main.jsx index d2870f684464..1b75ec3f49bf 100644 --- a/pkg/networkmanager/network-main.jsx +++ b/pkg/networkmanager/network-main.jsx @@ -25,6 +25,8 @@ import { in_anaconda_mode } from "utils"; import firewall from './firewall-client.js'; import { device_state_text, + has_group, + is_loopback, is_managed, render_active_connection, } from './interfaces.js'; @@ -39,23 +41,17 @@ export const NetworkPage = ({ privileged, operationInProgress, usage_monitor, pl const unmanaged = []; const plot_ifaces = []; let hasDetails = false; + const anaconda_mode = JSON.parse(window.sessionStorage.getItem("cockpit_anaconda")); + interfaces.forEach(iface => { - function hasGroup(iface) { - return ((iface.Device && - iface.Device.ActiveConnection && - iface.Device.ActiveConnection.Group && - iface.Device.ActiveConnection.Group.Members.length > 0) || - (iface.MainConnection && - iface.MainConnection.Groups.length > 0)); - } // Skip loopback - if (iface.Name == "lo" || (iface.Device && iface.Device.DeviceType == 'loopback')) + if (is_loopback(iface)) return; // Skip members - if (hasGroup(iface)) + else if (has_group(iface)) return; const dev = iface.Device; diff --git a/pkg/networkmanager/networking.scss b/pkg/networkmanager/networking.scss index bf340a51558c..823e383d271c 100644 --- a/pkg/networkmanager/networking.scss +++ b/pkg/networkmanager/networking.scss @@ -323,3 +323,10 @@ th { } /* End Firewall specific CSS */ + +#networking.anaconda .pf-v6-c-page__main-container { + border-radius: 0; + margin: 0; + border: 0; + max-block-size: 100%; +} diff --git a/pkg/networkmanager/networkmanager.jsx b/pkg/networkmanager/networkmanager.jsx index 56bf87476aa2..4dee79f4c645 100644 --- a/pkg/networkmanager/networkmanager.jsx +++ b/pkg/networkmanager/networkmanager.jsx @@ -25,6 +25,7 @@ import { PlotState } from 'plot'; import { useObject, useEvent, usePageLocation } from "hooks"; import { WithDialogs } from "dialogs.jsx"; +import { AnacondaNetworkPage } from './anaconda-main'; const _ = cockpit.gettext; @@ -99,11 +100,29 @@ const App = () => { const interfaces = model.list_interfaces(); + const anaconda_mode = JSON.parse(window.sessionStorage.getItem("cockpit_anaconda")); + + if (anaconda_mode) { + const iface = path.length == 1 ? interfaces.find(iface => iface.Name == path[0]) : undefined; + + return ( + + + + + + ); + } + /* At this point NM is running and the model is ready */ if (path.length == 0) { return ( - + { if (iface) { return ( - + = 24 * 3600 || seconds < 0) + if (seconds >= 60 * 5 || seconds < 0) { + console.log("refreshing") this.handleRefresh(); - else if (always_load) + } + else if (always_load) { + console.log("always refresh") this.loadUpdates(); + } } catch (exc) { this.handleLoadError(exc); } diff --git a/test/verify/check-networkmanager-wifi b/test/verify/check-networkmanager-wifi index d104e1b10914..8322e5a638fb 100755 --- a/test/verify/check-networkmanager-wifi +++ b/test/verify/check-networkmanager-wifi @@ -108,6 +108,7 @@ wpa_passphrase=hidden99""") # detects our three visible wifis (plus one hidden) b.wait_in_text("table[aria-label='Available networks'] tbody:first-of-type", "ZE WIFI!") b.wait_text("tr[data-ssid='ZE WIFI!'] th", "ZE WIFI!") + testlib.sit() self.assertEqual(b.get_pf_progress_value("tr[data-ssid='ZE WIFI!'] [data-label='Signal']"), 100) # sadly, mac80211_hwsim's data rate reporting is flaky, so just ensure at least one of our networks is correct b.wait_in_text("table[aria-label='Available networks']", "54 Mbps")