mirror of
https://github.com/cockpit-project/cockpit.git
synced 2026-11-03 21:49:50 -09:00
Merge 45b80afd97 into e2c433478c
This commit is contained in:
commit
ba8b7d35ce
9 changed files with 545 additions and 141 deletions
|
|
@ -18,21 +18,21 @@ import {
|
|||
is_managed,
|
||||
is_wireless,
|
||||
} from "../interfaces.js";
|
||||
import { NetworkInterfacePage } from "../network-interface.jsx";
|
||||
import { EmptyStatePanel } from "cockpit-components-empty-state.jsx";
|
||||
import { AnacondaNetworkInterface } from "./anaconda-network-interface.jsx";
|
||||
import { NMInterface } from "../utils.js";
|
||||
|
||||
const _ = cockpit.gettext;
|
||||
|
||||
interface AnacondaInterfaceRowProps {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
iface: any;
|
||||
iface: NMInterface;
|
||||
setSelectedIfaceName: React.Dispatch<React.SetStateAction<string | null>>;
|
||||
}
|
||||
|
||||
const AnacondaInterfaceRow = ({ iface, setSelectedIfaceName }: AnacondaInterfaceRowProps) => {
|
||||
const dev = iface.Device;
|
||||
// Show WiFi name in the list
|
||||
const ssid = dev.ActiveAccessPoint ? dev.ActiveAccessPoint.Ssid : null;
|
||||
const ssid = dev?.ActiveAccessPoint ? dev?.ActiveAccessPoint.Ssid : null;
|
||||
const ifaceDisplayName = ssid ? `${iface.Name} (${ssid})` : iface.Name;
|
||||
const connectionStatus = dev.ActiveConnection ? _("Connected") : _("Disconnected");
|
||||
|
||||
|
|
@ -55,13 +55,11 @@ const AnacondaInterfaceRow = ({ iface, setSelectedIfaceName }: AnacondaInterface
|
|||
};
|
||||
|
||||
interface AnacondaNetworkPageProps {
|
||||
privileged: boolean;
|
||||
operationInProgress: boolean;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
interfaces: any[];
|
||||
interfaces: NMInterface[];
|
||||
}
|
||||
|
||||
export const AnacondaNetworkPage = ({ privileged, operationInProgress, interfaces }: AnacondaNetworkPageProps) => {
|
||||
export const AnacondaNetworkPage = ({ operationInProgress, interfaces }: AnacondaNetworkPageProps) => {
|
||||
const [selectedIfaceName, setSelectedIfaceName] = useState<string | null>(null);
|
||||
const selectedIface = interfaces.find(iface => iface.Name === selectedIfaceName) ?? null;
|
||||
|
||||
|
|
@ -99,7 +97,7 @@ export const AnacondaNetworkPage = ({ privileged, operationInProgress, interface
|
|||
|
||||
return (
|
||||
<Page data-test-wait={operationInProgress} id="networking" className="pf-m-no-sidebar anaconda">
|
||||
<Split hasGutter id="networking-interfaces">
|
||||
<Split hasGutter id="networking-interfaces" isWrappable>
|
||||
<SplitItem>
|
||||
<SimpleList>
|
||||
{managedWireless.length !== 0 &&
|
||||
|
|
@ -115,13 +113,7 @@ export const AnacondaNetworkPage = ({ privileged, operationInProgress, interface
|
|||
</SplitItem>
|
||||
<SplitItem isFilled>
|
||||
{selectedIface
|
||||
? <NetworkInterfacePage
|
||||
privileged={privileged}
|
||||
operationInProgress={operationInProgress}
|
||||
usage_monitor={undefined}
|
||||
plot_state={undefined}
|
||||
interfaces={interfaces}
|
||||
iface={selectedIface} />
|
||||
? <AnacondaNetworkInterface iface={selectedIface} />
|
||||
: <EmptyStatePanel
|
||||
title={_("No interface selected")}
|
||||
paragraph={_("Select a network interface to configure the connection.")}
|
||||
|
|
|
|||
279
pkg/networkmanager/anaconda/anaconda-network-interface.tsx
Normal file
279
pkg/networkmanager/anaconda/anaconda-network-interface.tsx
Normal file
|
|
@ -0,0 +1,279 @@
|
|||
/*
|
||||
* Copyright (C) 2026 Red Hat, Inc.
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
*/
|
||||
|
||||
import cockpit from "cockpit";
|
||||
import React, { useContext, useState } from 'react';
|
||||
|
||||
import {
|
||||
DescriptionList,
|
||||
DescriptionListDescription,
|
||||
DescriptionListGroup,
|
||||
DescriptionListTerm,
|
||||
} from "@patternfly/react-core/dist/esm/components/DescriptionList/index.js";
|
||||
import { ExpandableSection } from "@patternfly/react-core/dist/esm/components/ExpandableSection/index.js";
|
||||
import { Switch } from "@patternfly/react-core/dist/esm/components/Switch/index.js";
|
||||
import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
|
||||
import { ModelContext } from "../model-context";
|
||||
import { useEvent } from "hooks";
|
||||
import { active_connection_gateway, active_connection_ipv4, active_connection_ipv6, choice_title, connectInterface, createGhostConnectionSettings, disconnectInterface, is_managed, is_wireless, toggleConnectionAutoconnect, } from "../interfaces";
|
||||
import { get_ip_method_choices } from "../ip-settings";
|
||||
import { NetworkAction } from "../dialogs-common.jsx";
|
||||
import { apSecurityFlagsToString, NMConnectionSettings, NMDevice, NMInterface } from "../utils";
|
||||
import { Card, CardBody, CardHeader, CardTitle } from "@patternfly/react-core/dist/esm/components/Card/index.js";
|
||||
import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
|
||||
|
||||
const _ = cockpit.gettext;
|
||||
|
||||
interface IpConfig {
|
||||
addresses: string[];
|
||||
gateway: string;
|
||||
dns_data: string[];
|
||||
}
|
||||
|
||||
type IPTopic = "ipv4" | "ipv6";
|
||||
|
||||
interface IPConfigExpandableProps {
|
||||
toggleText: string;
|
||||
onToggle: any;
|
||||
isExpanded: boolean;
|
||||
iface: NMInterface;
|
||||
settings: NMConnectionSettings;
|
||||
topic: IPTopic;
|
||||
}
|
||||
|
||||
// Get IP config, use what is available on ActiveConnection if the device is up.
|
||||
// If it is down only show what we can get from the Connection settings.
|
||||
const getIpConfig = (dev: NMDevice, settings: NMConnectionSettings, topic: IPTopic) => {
|
||||
const ipSettings: IpConfig = { addresses: [], gateway: "", dns_data: [] };
|
||||
const hasActiveCon = dev.ActiveConnection;
|
||||
const params = settings[topic];
|
||||
|
||||
if (!params) {
|
||||
return ipSettings;
|
||||
}
|
||||
|
||||
// IP address
|
||||
if (hasActiveCon) {
|
||||
ipSettings.addresses = (topic === "ipv4")
|
||||
? active_connection_ipv4(dev)
|
||||
: active_connection_ipv6(dev, false);
|
||||
} else {
|
||||
ipSettings.addresses = params.address_data?.map((a) => a.address + "/" + a.prefix);
|
||||
}
|
||||
// show "choice title" if we get nothing
|
||||
if (ipSettings.addresses.length === 0 && params.method != "manual") {
|
||||
ipSettings.addresses = [choice_title(get_ip_method_choices(topic), params.method, _("Unknown configuration"))];
|
||||
}
|
||||
|
||||
// Gateway
|
||||
if (hasActiveCon) {
|
||||
ipSettings.gateway = active_connection_gateway(dev, topic);
|
||||
} else {
|
||||
ipSettings.gateway = params.gateway || _("Not configured");
|
||||
}
|
||||
|
||||
// DNS
|
||||
ipSettings.dns_data = params.dns_data;
|
||||
|
||||
return ipSettings;
|
||||
};
|
||||
|
||||
const IPConfigExpandable = ({
|
||||
toggleText,
|
||||
onToggle,
|
||||
isExpanded,
|
||||
iface,
|
||||
settings,
|
||||
topic,
|
||||
}: IPConfigExpandableProps) => {
|
||||
const ipSettings = getIpConfig(iface.Device, settings, topic);
|
||||
|
||||
const descriptionGroups = [
|
||||
{
|
||||
term: _("IP address"),
|
||||
description: ipSettings.addresses.join(", "),
|
||||
},
|
||||
{
|
||||
term: _("Default gateway"),
|
||||
description: ipSettings.gateway,
|
||||
},
|
||||
{
|
||||
term: _("DNS"),
|
||||
description: ipSettings.dns_data.join(", "),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Flex className="ip-expandable"
|
||||
alignItems={{ default: "alignItemsFlexStart" }}
|
||||
flexWrap={{ default: "nowrap" }}
|
||||
>
|
||||
<FlexItem flex={{ default: "flex_1" }}>
|
||||
<ExpandableSection
|
||||
toggleText={toggleText}
|
||||
onToggle={(_event, expanded) => onToggle(expanded)}
|
||||
isExpanded={isExpanded}
|
||||
isIndented
|
||||
>
|
||||
<DescriptionList aria-label={_("IP configuration")}>
|
||||
{descriptionGroups.filter(group => !!group.description).map(group => (
|
||||
<DescriptionListGroup key={group.term}>
|
||||
<DescriptionListTerm>{group.term}</DescriptionListTerm>
|
||||
<DescriptionListDescription>{group.description}</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
))}
|
||||
</DescriptionList>
|
||||
</ExpandableSection>
|
||||
</FlexItem>
|
||||
<FlexItem>
|
||||
<NetworkAction buttonText={_("Edit")}
|
||||
iface={iface}
|
||||
connectionSettings={settings}
|
||||
type={topic}
|
||||
buttonVariant="secondary"
|
||||
isInline={false}
|
||||
/>
|
||||
</FlexItem>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
interface DescriptionListWithAction {
|
||||
dlTerm: React.ReactNode;
|
||||
dlDescription: React.ReactNode;
|
||||
actionButton?: React.ReactNode;
|
||||
}
|
||||
|
||||
const DescriptionListWithAction = ({
|
||||
dlTerm,
|
||||
dlDescription,
|
||||
actionButton,
|
||||
}: DescriptionListWithAction) => {
|
||||
return (
|
||||
<Flex alignItems={{ default: "alignItemsFlexStart" }}
|
||||
justifyContent={{ default: "justifyContentSpaceBetween" }}
|
||||
className="description-list-with-action"
|
||||
>
|
||||
<FlexItem grow={{ default: "grow" }}>
|
||||
<DescriptionList>
|
||||
<DescriptionListGroup>
|
||||
<DescriptionListTerm>{dlTerm}</DescriptionListTerm>
|
||||
<DescriptionListDescription>{dlDescription}</DescriptionListDescription>
|
||||
</DescriptionListGroup>
|
||||
</DescriptionList>
|
||||
</FlexItem>
|
||||
{ actionButton &&
|
||||
<FlexItem>
|
||||
{actionButton}
|
||||
</FlexItem>
|
||||
}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
const connectionState = (dev: NMDevice, isManaged: boolean) => {
|
||||
if (!dev) {
|
||||
return _("Inactive");
|
||||
} else if (isManaged) {
|
||||
return dev.StateText;
|
||||
}
|
||||
|
||||
return dev.ActiveConnection ? _("Connected") : _("Disconnected");
|
||||
};
|
||||
|
||||
interface AnacondaNetworkInterfaceProps {
|
||||
iface: NMInterface;
|
||||
}
|
||||
|
||||
export const AnacondaNetworkInterface = ({
|
||||
iface,
|
||||
}: AnacondaNetworkInterfaceProps) => {
|
||||
const model = useContext(ModelContext);
|
||||
useEvent(model, "changed");
|
||||
console.log(iface);
|
||||
|
||||
const [isIpv4Expanded, setIsIpv4Expanded] = useState(true);
|
||||
const [isIpv6Expanded, setIsIpv6Expanded] = useState(true);
|
||||
|
||||
const devName = iface.Name;
|
||||
const dev = iface.Device;
|
||||
const connectionSettings = iface?.MainConnection?.Settings || (createGhostConnectionSettings(devName, dev) as NMConnectionSettings);
|
||||
const isManaged = iface && (!dev || is_managed(dev));
|
||||
|
||||
const toggleDevAction = (
|
||||
<Switch id="network-interface-enable-device"
|
||||
label={_("Enable device during installation")}
|
||||
isReversed
|
||||
isChecked={!!(dev?.ActiveConnection)}
|
||||
onChange={(_event, enable) => enable
|
||||
? connectInterface(model, iface, dev, connectionSettings)
|
||||
: disconnectInterface(model, iface, dev)}
|
||||
/>
|
||||
|
||||
);
|
||||
|
||||
const autoconnectSwitch = (
|
||||
<Switch id="network-interface-connect-automatically"
|
||||
label={_("Connect automatically on reboot")}
|
||||
isReversed
|
||||
isChecked={connectionSettings.connection.autoconnect}
|
||||
onChange={(_event, checked) => toggleConnectionAutoconnect(model, dev, iface.MainConnection, connectionSettings, checked)}
|
||||
/>
|
||||
);
|
||||
|
||||
const isWireless = is_wireless(iface);
|
||||
const activeAP = dev?.ActiveAccessPoint;
|
||||
const ssid = activeAP ? activeAP.Ssid : null;
|
||||
|
||||
const joinNetButton = (
|
||||
<Button variant="secondary">
|
||||
{_("Join another network")}
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<Card id="anaconda-network-interface">
|
||||
<CardHeader actions={{ actions: toggleDevAction }}>
|
||||
<CardTitle>{devName}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<DescriptionListWithAction dlTerm={_("Status")}
|
||||
dlDescription={connectionState(dev, isManaged)}
|
||||
actionButton={autoconnectSwitch}
|
||||
/>
|
||||
{ isWireless &&
|
||||
<DescriptionListWithAction dlTerm={_("Network joined")}
|
||||
dlDescription={ssid ?? _("Disconnected")}
|
||||
actionButton={joinNetButton}
|
||||
/>
|
||||
}
|
||||
{ isWireless && activeAP &&
|
||||
<DescriptionListWithAction dlTerm={_("Security type")}
|
||||
dlDescription={apSecurityFlagsToString(activeAP.Flags, activeAP.WpaFlags, activeAP.RsnFlags)}
|
||||
/>
|
||||
}
|
||||
<DescriptionListWithAction dlTerm={_("IP settings")}
|
||||
dlDescription={<>
|
||||
<IPConfigExpandable toggleText={_("IPv4")}
|
||||
onToggle={setIsIpv4Expanded}
|
||||
isExpanded={isIpv4Expanded}
|
||||
iface={iface}
|
||||
settings={connectionSettings}
|
||||
topic="ipv4"
|
||||
/>
|
||||
|
||||
<IPConfigExpandable toggleText={_("IPv6")}
|
||||
onToggle={setIsIpv6Expanded}
|
||||
isExpanded={isIpv6Expanded}
|
||||
iface={iface}
|
||||
settings={connectionSettings}
|
||||
topic="ipv6"
|
||||
/>
|
||||
</>}
|
||||
/>
|
||||
</CardBody>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
|
@ -7,17 +7,18 @@
|
|||
@include anaconda.embedded-page-shell;
|
||||
}
|
||||
|
||||
// Main networking overview only: omit graphs and journal noise in the installer
|
||||
#networking.anaconda #networking-graphs,
|
||||
#networking.anaconda .cockpit-log-panel {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@include anaconda.anaconda-body-overflow-hack;
|
||||
|
||||
// Remove padding above device name (header) in the detail view
|
||||
#networking.anaconda #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;
|
||||
#networking.anaconda {
|
||||
// Add padding between each description list
|
||||
.description-list-with-action:not(:first-of-type),
|
||||
#ip-settings {
|
||||
margin-block-start: var(--pf-t--global--spacer--lg);
|
||||
}
|
||||
|
||||
// Add padding between expandable sections so edit buttons don't touch when
|
||||
// sections are not expanded
|
||||
.ip-expandable:not(:first-of-type) {
|
||||
margin-block-start: var(--pf-t--global--spacer--md);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ export const NetworkModal = ({ dialogError, help, idPrefix, title, onSubmit, chi
|
|||
);
|
||||
};
|
||||
|
||||
export const NetworkAction = ({ buttonText, iface, connectionSettings, type }) => {
|
||||
export const NetworkAction = ({ buttonText, iface, connectionSettings, type, buttonVariant, isInline }) => {
|
||||
const Dialogs = useDialogs();
|
||||
const model = useContext(ModelContext);
|
||||
|
||||
|
|
@ -234,11 +234,14 @@ export const NetworkAction = ({ buttonText, iface, connectionSettings, type }) =
|
|||
.catch(err => console.error("NetworkAction Dialog failed:", err)); // not-covered: OS error
|
||||
}
|
||||
|
||||
const variant = buttonVariant ?? (!iface ? "secondary" : "link");
|
||||
const inline = isInline ?? !!iface;
|
||||
|
||||
return (
|
||||
<Button id={"networking-" + (!iface ? "add-" : "edit-") + type}
|
||||
isInline={!!iface}
|
||||
isInline={inline}
|
||||
onClick={syn_click(model, show)}
|
||||
variant={!iface ? "secondary" : "link"}>
|
||||
variant={variant}>
|
||||
{buttonText || _("edit")}
|
||||
</Button>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -980,7 +980,8 @@ export function NetworkManagerModel() {
|
|||
],
|
||||
|
||||
props: {
|
||||
AddressData: { conv: conv_Array(ip_address_from_nm), def: [] }
|
||||
AddressData: { conv: conv_Array(ip_address_from_nm), def: [] },
|
||||
Gateway: { def: "" },
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -990,7 +991,8 @@ export function NetworkManagerModel() {
|
|||
],
|
||||
|
||||
props: {
|
||||
AddressData: { conv: conv_Array(ip_address_from_nm), def: [] }
|
||||
AddressData: { conv: conv_Array(ip_address_from_nm), def: [] },
|
||||
Gateway: { def: "" },
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -1645,6 +1647,49 @@ export function array_join(elts, sep) {
|
|||
return result;
|
||||
}
|
||||
|
||||
export function active_connection_ipv4(dev) {
|
||||
const con = dev.ActiveConnection;
|
||||
const ip4config = con ? con.Ip4Config : dev.Ip4Config;
|
||||
const parts = [];
|
||||
if (ip4config) {
|
||||
ip4config.AddressData.forEach(function (a) {
|
||||
parts.push(a.address + "/" + a.prefix);
|
||||
});
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
export function active_connection_ipv6(dev, hide_link_local) {
|
||||
const con = dev.ActiveConnection;
|
||||
|
||||
function is_ipv6_link_local(addr) {
|
||||
return (addr.indexOf("fe8") === 0 ||
|
||||
addr.indexOf("fe9") === 0 ||
|
||||
addr.indexOf("fea") === 0 ||
|
||||
addr.indexOf("feb") === 0);
|
||||
}
|
||||
|
||||
const ip6config = con ? con.Ip6Config : dev.Ip6Config;
|
||||
const parts = [];
|
||||
if (ip6config) {
|
||||
ip6config.AddressData.forEach(function (a) {
|
||||
if (!(hide_link_local && is_ipv6_link_local(a.address)))
|
||||
parts.push(a.address + "/" + a.prefix);
|
||||
});
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
export function active_connection_gateway(dev, topic) {
|
||||
const con = dev.ActiveConnection;
|
||||
const ipProp = topic === "ipv4" ? "Ip4Config" : "Ip6Config";
|
||||
const ipconfig = con ? con[ipProp] : dev[ipProp];
|
||||
|
||||
return ipconfig?.Gateway || _("Not configured");
|
||||
}
|
||||
|
||||
export function render_active_connection(dev, with_link, hide_link_local) {
|
||||
const parts = [];
|
||||
|
||||
|
|
@ -1657,27 +1702,8 @@ export function render_active_connection(dev, with_link, hide_link_local) {
|
|||
return fmt_to_fragments(_("Part of $0"), with_link ? render_interface_link(con.Group.Interface) : con.Group.Interface);
|
||||
}
|
||||
|
||||
const ip4config = con ? con.Ip4Config : dev.Ip4Config;
|
||||
if (ip4config) {
|
||||
ip4config.AddressData.forEach(function (a) {
|
||||
parts.push(a.address + "/" + a.prefix);
|
||||
});
|
||||
}
|
||||
|
||||
function is_ipv6_link_local(addr) {
|
||||
return (addr.indexOf("fe8") === 0 ||
|
||||
addr.indexOf("fe9") === 0 ||
|
||||
addr.indexOf("fea") === 0 ||
|
||||
addr.indexOf("feb") === 0);
|
||||
}
|
||||
|
||||
const ip6config = con ? con.Ip6Config : dev.Ip6Config;
|
||||
if (ip6config) {
|
||||
ip6config.AddressData.forEach(function (a) {
|
||||
if (!(hide_link_local && is_ipv6_link_local(a.address)))
|
||||
parts.push(a.address + "/" + a.prefix);
|
||||
});
|
||||
}
|
||||
parts.push(...active_connection_ipv4(dev));
|
||||
parts.push(...active_connection_ipv6(dev, hide_link_local));
|
||||
|
||||
return parts.join(", ");
|
||||
}
|
||||
|
|
@ -1704,6 +1730,34 @@ export function complete_settings(settings, device) {
|
|||
}
|
||||
}
|
||||
|
||||
export function createGhostConnectionSettings(ifaceName, dev) {
|
||||
const settings = {
|
||||
connection: {
|
||||
interface_name: ifaceName,
|
||||
autoconnect: true,
|
||||
autoconnect_priority: 0,
|
||||
},
|
||||
ipv4: {
|
||||
method: "auto",
|
||||
address_data: [],
|
||||
dns_data: [],
|
||||
dns_search: [],
|
||||
route_data: [],
|
||||
gateway: "",
|
||||
},
|
||||
ipv6: {
|
||||
method: "auto",
|
||||
address_data: [],
|
||||
dns_data: [],
|
||||
dns_search: [],
|
||||
route_data: [],
|
||||
gateway: "",
|
||||
}
|
||||
};
|
||||
complete_settings(settings, dev);
|
||||
return settings;
|
||||
}
|
||||
|
||||
export function settings_applier(model, device, connection) {
|
||||
/* If we have a connection, we can just update it.
|
||||
* Otherwise if the settings has TYPE set, we can add
|
||||
|
|
@ -2125,6 +2179,51 @@ export function is_wireless(iface) {
|
|||
return iface.Device?.DeviceType === '802-11-wireless';
|
||||
}
|
||||
|
||||
export function connectInterface(model, iface, dev, ghostSettings) {
|
||||
if (!(iface.MainConnection || (dev && ghostSettings)))
|
||||
return;
|
||||
|
||||
function modify() {
|
||||
if (iface.MainConnection) {
|
||||
return iface.MainConnection.activate(dev, null).catch(show_unexpected_error);
|
||||
} else {
|
||||
return dev.activate_with_settings(ghostSettings, null).catch(show_unexpected_error);
|
||||
}
|
||||
}
|
||||
|
||||
const dev_name = iface.Name;
|
||||
with_checkpoint(model, modify,
|
||||
{
|
||||
devices: dev ? [dev] : [],
|
||||
fail_text: fmt_to_fragments(_("Switching on $0 will break the connection to the server, and will make the administration UI unavailable."), <b>{dev_name}</b>),
|
||||
anyway_text: cockpit.format(_("Switch on $0"), dev_name)
|
||||
});
|
||||
}
|
||||
|
||||
export function disconnectInterface(model, iface, dev) {
|
||||
if (!dev) {
|
||||
console.log("Trying to switch off without a device?");
|
||||
return;
|
||||
}
|
||||
|
||||
function modify() {
|
||||
return dev.disconnect().catch(show_unexpected_error);
|
||||
}
|
||||
|
||||
const dev_name = iface.Name;
|
||||
with_checkpoint(model, modify,
|
||||
{
|
||||
devices: [dev],
|
||||
fail_text: fmt_to_fragments(_("Switching off $0 will break the connection to the server, and will make the administration UI unavailable."), <b>{dev_name}</b>),
|
||||
anyway_text: cockpit.format(_("Switch off $0"), dev_name)
|
||||
});
|
||||
}
|
||||
|
||||
export function toggleConnectionAutoconnect(model, dev, con, settings, autoconnect) {
|
||||
settings.connection.autoconnect = autoconnect;
|
||||
return settings_applier(model, dev, con)(settings);
|
||||
}
|
||||
|
||||
export function init() {
|
||||
cockpit.translate();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
import React from 'react';
|
||||
|
||||
export const ModelContext = React.createContext(undefined);
|
||||
export const ModelContext = React.createContext(null);
|
||||
|
|
|
|||
|
|
@ -46,15 +46,17 @@ import * as utils from "./utils.js";
|
|||
import {
|
||||
array_join,
|
||||
choice_title,
|
||||
complete_settings,
|
||||
connectInterface,
|
||||
connection_settings,
|
||||
createGhostConnectionSettings,
|
||||
disconnectInterface,
|
||||
free_member_connection,
|
||||
is_managed,
|
||||
render_active_connection,
|
||||
settings_applier,
|
||||
show_error_dialog,
|
||||
show_unexpected_error,
|
||||
syn_click,
|
||||
toggleConnectionAutoconnect,
|
||||
with_checkpoint,
|
||||
} from './interfaces.js';
|
||||
import {
|
||||
|
|
@ -283,7 +285,7 @@ export const NetworkInterfacePage = ({
|
|||
if (iface.MainConnection) {
|
||||
connectionSettings = iface.MainConnection.Settings;
|
||||
} else {
|
||||
ghostSettings = createGhostConnectionSettings();
|
||||
ghostSettings = createGhostConnectionSettings(iface.Name, dev);
|
||||
connectionSettings = ghostSettings;
|
||||
}
|
||||
}
|
||||
|
|
@ -323,49 +325,6 @@ export const NetworkInterfacePage = ({
|
|||
}
|
||||
}
|
||||
|
||||
function connect() {
|
||||
if (!(iface.MainConnection || (dev && ghostSettings)))
|
||||
return;
|
||||
|
||||
function fail(error) {
|
||||
show_unexpected_error(error);
|
||||
}
|
||||
|
||||
function modify() {
|
||||
if (iface.MainConnection) {
|
||||
return iface.MainConnection.activate(dev, null).catch(fail);
|
||||
} else {
|
||||
return dev.activate_with_settings(ghostSettings, null).catch(fail);
|
||||
}
|
||||
}
|
||||
|
||||
with_checkpoint(model, modify,
|
||||
{
|
||||
devices: dev ? [dev] : [],
|
||||
fail_text: fmt_to_fragments(_("Switching on $0 will break the connection to the server, and will make the administration UI unavailable."), <b>{dev_name}</b>),
|
||||
anyway_text: cockpit.format(_("Switch on $0"), dev_name)
|
||||
});
|
||||
}
|
||||
|
||||
function disconnect() {
|
||||
if (!dev) {
|
||||
console.log("Trying to switch off without a device?");
|
||||
return;
|
||||
}
|
||||
|
||||
function modify () {
|
||||
return dev.disconnect()
|
||||
.catch(error => show_unexpected_error(error));
|
||||
}
|
||||
|
||||
with_checkpoint(model, modify,
|
||||
{
|
||||
devices: [dev],
|
||||
fail_text: fmt_to_fragments(_("Switching off $0 will break the connection to the server, and will make the administration UI unavailable."), <b>{dev_name}</b>),
|
||||
anyway_text: cockpit.format(_("Switch off $0"), dev_name)
|
||||
});
|
||||
}
|
||||
|
||||
function renderDesc() {
|
||||
let desc;
|
||||
let cs;
|
||||
|
|
@ -521,8 +480,7 @@ export const NetworkInterfacePage = ({
|
|||
<DescriptionListDescription data-label="General">
|
||||
<Checkbox id="autoreconnect" isDisabled={!privileged}
|
||||
onChange={(_event, checked) => {
|
||||
settings.connection.autoconnect = checked;
|
||||
settings_applier(model, dev, con)(settings);
|
||||
toggleConnectionAutoconnect(model, dev, con, settings, checked);
|
||||
}}
|
||||
isChecked={settings.connection.autoconnect}
|
||||
label={_("Connect automatically")} />
|
||||
|
|
@ -1170,30 +1128,6 @@ export const NetworkInterfacePage = ({
|
|||
);
|
||||
}
|
||||
|
||||
function createGhostConnectionSettings() {
|
||||
const settings = {
|
||||
connection: {
|
||||
interface_name: iface.Name
|
||||
},
|
||||
ipv4: {
|
||||
method: "auto",
|
||||
address_data: [],
|
||||
dns_data: [],
|
||||
dns_search: [],
|
||||
route_data: []
|
||||
},
|
||||
ipv6: {
|
||||
method: "auto",
|
||||
address_data: [],
|
||||
dns_data: [],
|
||||
dns_search: [],
|
||||
route_data: []
|
||||
}
|
||||
};
|
||||
complete_settings(settings, dev);
|
||||
return settings;
|
||||
}
|
||||
|
||||
/* Disable the On/Off button for interfaces that we don't know about at all,
|
||||
and for devices that NM declares to be unavailable. Neither can be activated.
|
||||
*/
|
||||
|
|
@ -1207,7 +1141,9 @@ export const NetworkInterfacePage = ({
|
|||
<Switch id="interface-switch"
|
||||
isChecked={!!(dev && dev.ActiveConnection)}
|
||||
isDisabled={!iface || (dev && dev.State === 20) || !privileged}
|
||||
onChange={(_event, enable) => enable ? connect() : disconnect()}
|
||||
onChange={(_event, enable) => enable
|
||||
? connectInterface(model, iface, dev, ghostSettings)
|
||||
: disconnectInterface(model, iface, dev)}
|
||||
aria-label={_("Enable or disable the device")} />
|
||||
</Privileged>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -103,8 +103,7 @@ const App = () => {
|
|||
return (
|
||||
<ModelContext.Provider value={model}>
|
||||
<WithDialogs key="networking-anaconda">
|
||||
<AnacondaNetworkPage privileged={superuser.allowed}
|
||||
operationInProgress={model.operationInProgress}
|
||||
<AnacondaNetworkPage operationInProgress={model.operationInProgress}
|
||||
interfaces={model.list_interfaces()} />
|
||||
</WithDialogs>
|
||||
</ModelContext.Provider>
|
||||
|
|
|
|||
|
|
@ -10,22 +10,62 @@ const _ = cockpit.gettext;
|
|||
|
||||
type IPAddress = ipaddr.IPv4 | ipaddr.IPv6;
|
||||
|
||||
type NetworkManagerSettings = {
|
||||
connection: {
|
||||
id: string;
|
||||
uuid?: string;
|
||||
interface_name: string | undefined;
|
||||
multi_connect?: number;
|
||||
type: string
|
||||
autoconnect_priority: number;
|
||||
}
|
||||
// Partial NetworkManager objects types
|
||||
|
||||
type NMIPMethod = "auto" | "manual" | "dhcp" | "link-local" | "ignore" | "shared" | "disabled";
|
||||
|
||||
type NMIPSettings = {
|
||||
method: NMIPMethod;
|
||||
address_data: {
|
||||
address: string;
|
||||
prefix: string;
|
||||
}[];
|
||||
dns_data: string[];
|
||||
dns_search: string[];
|
||||
route_data: unknown[];
|
||||
gateway: string;
|
||||
}
|
||||
|
||||
type NetworkManagerConnection = {
|
||||
Settings: NetworkManagerSettings;
|
||||
export type NMConnectionSettings = {
|
||||
connection: {
|
||||
interface_name: string | undefined;
|
||||
autoconnect: boolean;
|
||||
autoconnect_priority: number;
|
||||
// Optional because createGhostConnectionSettings() doesn't set default values
|
||||
id?: string;
|
||||
uuid?: string;
|
||||
multi_connect?: number;
|
||||
type?: string;
|
||||
};
|
||||
ipv4?: NMIPSettings;
|
||||
ipv6?: NMIPSettings;
|
||||
}
|
||||
|
||||
export type NMConnection = {
|
||||
Settings?: NMConnectionSettings;
|
||||
Unsaved: boolean;
|
||||
}
|
||||
|
||||
export type NMAccessPoint = {
|
||||
Ssid: string;
|
||||
Flags: number;
|
||||
WpaFlags: number;
|
||||
RsnFlags: number;
|
||||
}
|
||||
|
||||
export type NMDevice = {
|
||||
StateText: string;
|
||||
// this is type_ActiveConnection
|
||||
ActiveConnection: unknown;
|
||||
ActiveAccessPoint: NMAccessPoint | null | undefined;
|
||||
}
|
||||
|
||||
export type NMInterface = {
|
||||
Name: string;
|
||||
Device: NMDevice;
|
||||
MainConnection: NMConnection | null;
|
||||
}
|
||||
|
||||
/* NetworkManager specific data conversions and utility functions.
|
||||
*/
|
||||
|
||||
|
|
@ -333,7 +373,7 @@ export function list_interfaces() {
|
|||
// on top of this, remove the multiconnection settings and assign it directly
|
||||
// to interface. This is done for Anaconda in order to avoid creating unexpected
|
||||
// multiconnections when user modifies the connection during installation.
|
||||
export function isNonPersistentMultiCon(connection: NetworkManagerConnection): boolean {
|
||||
export function isNonPersistentMultiCon(connection: NMConnection): boolean {
|
||||
const settings = connection?.Settings;
|
||||
if (settings &&
|
||||
settings.connection.interface_name === undefined &&
|
||||
|
|
@ -347,7 +387,7 @@ export function isNonPersistentMultiCon(connection: NetworkManagerConnection): b
|
|||
return false;
|
||||
}
|
||||
|
||||
export function createNewConnSettings(settings: NetworkManagerSettings, iface_name: string) {
|
||||
export function createNewConnSettings(settings: NMConnectionSettings, iface_name: string) {
|
||||
const newSettings = {
|
||||
...settings,
|
||||
connection: {
|
||||
|
|
@ -364,6 +404,61 @@ export function createNewConnSettings(settings: NetworkManagerSettings, iface_na
|
|||
return newSettings;
|
||||
}
|
||||
|
||||
const NM80211ApFlags = {
|
||||
// access point has no special capabilities
|
||||
NONE: 0x00000000,
|
||||
// access point requires authentication and encryption (usually means WEP)
|
||||
PRIVACY: 0x00000001,
|
||||
} as const;
|
||||
|
||||
const NM80211ApSecurityFlags = {
|
||||
// the access point has no special security requirements
|
||||
NONE: 0x00000000,
|
||||
// 40/64-bit WEP is supported for pairwise/unicast encryption
|
||||
PAIR_WEP40: 0x00000001,
|
||||
// 104/128-bit WEP is supported for pairwise/unicast encryption
|
||||
PAIR_WEP104: 0x00000002,
|
||||
// TKIP is supported for pairwise/unicast encryption
|
||||
PAIR_TKIP: 0x00000004,
|
||||
// AES/CCMP is supported for pairwise/unicast encryption
|
||||
PAIR_CCMP: 0x00000008,
|
||||
// 40/64-bit WEP is supported for group/broadcast encryption
|
||||
GROUP_WEP40: 0x00000010,
|
||||
// 104/128-bit WEP is supported for group/broadcast encryption
|
||||
GROUP_WEP104: 0x00000020,
|
||||
// TKIP is supported for group/broadcast encryption
|
||||
GROUP_TKIP: 0x00000040,
|
||||
// AES/CCMP is supported for group/broadcast encryption
|
||||
GROUP_CCMP: 0x00000080,
|
||||
// WPA/RSN Pre-Shared Key encryption is supported
|
||||
KEY_MGMT_PSK: 0x00000100,
|
||||
// 802.1x authentication and key management is supported
|
||||
KEY_MGMT_802_1X: 0x00000200,
|
||||
} as const;
|
||||
|
||||
// See NetworkManager examples for access point flags conversion:
|
||||
// https://github.com/NetworkManager/NetworkManager/blob/1c71c568e7736ce2075fd0d53c387f2221bab832/examples/python/gi/show-wifi-networks.py#L64-L76
|
||||
export function apSecurityFlagsToString(flags: number, wpaFlags: number, rsnFlags: number): string {
|
||||
let str = "";
|
||||
if ((flags & NM80211ApFlags.PRIVACY) && wpaFlags === 0 && rsnFlags === 0) {
|
||||
str += " WEP";
|
||||
}
|
||||
if (wpaFlags !== 0) {
|
||||
str += " WPA1";
|
||||
}
|
||||
if (rsnFlags !== 0) {
|
||||
str += " WPA2";
|
||||
}
|
||||
if ((wpaFlags & NM80211ApSecurityFlags.KEY_MGMT_802_1X) || (rsnFlags & NM80211ApSecurityFlags.KEY_MGMT_802_1X)) {
|
||||
str += " 802.1X";
|
||||
}
|
||||
if (str === "") {
|
||||
str = _("Unsecured");
|
||||
}
|
||||
|
||||
return str.trimStart();
|
||||
}
|
||||
|
||||
export function debug(...args: Parameters<typeof console.debug>): void {
|
||||
if (window.debugging === "all" || window.debugging?.includes("networkmanager")) // not-covered: debugging
|
||||
console.debug("networkmanager:", ...args); // not-covered: debugging
|
||||
|
|
|
|||
Loading…
Reference in a new issue