Skip to content
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
9d6053e
fix(rust): filter duplicated device state changes
imobachgs Apr 1, 2025
939bb90
refactor(web): rename WifiNetworksListPage to WifiNetworksList
imobachgs Apr 2, 2025
78ccf81
feat(web): reorganize wireless configuration
imobachgs Apr 2, 2025
c57c740
feat(web): begin modeling new connection details page
dgdavid Apr 4, 2025
2d2a2d8
fix(web): use camelCase for network GeneralState
imobachgs Apr 3, 2025
55fac2d
refactor(web): break useNetwork into smaller hooks
imobachgs Apr 3, 2025
ed32d17
fix(web): avoid fetching all network devices on each change
imobachgs Apr 3, 2025
8875de0
fix(rust): expose the password for wpa-psk
imobachgs Apr 4, 2025
ef12ab6
fix(rust): fix network state test
imobachgs Apr 4, 2025
7cf50f0
fix(web): drop the feature to connect to a hidden network
imobachgs Apr 4, 2025
badedfa
refactor(web): drop unused code related to Wi-Fi selection
imobachgs Apr 4, 2025
61a31d8
fix(rust): do not crash if cannot read secrets
imobachgs Apr 4, 2025
da542ed
fix(web): fix and rename useNetworkConfigChanges
imobachgs Apr 7, 2025
bc2136e
fix(web): display an error when cannot add/update a connection
imobachgs Apr 7, 2025
aedb70d
web: simplify list of visible Wi-Fi networks
dgdavid Apr 7, 2025
b7a117a
web: minor adjustments for connection details
dgdavid Apr 7, 2025
14ca2ec
fix(web): fix error when creating a connection
imobachgs Apr 7, 2025
2c13172
feat(rust): simplify network DeviceState values
imobachgs Apr 7, 2025
007294b
refactor(web): adapt network types to the new DeviceState
imobachgs Apr 7, 2025
06687d7
fix(web): correctly detect non-empty arrays
dgdavid Apr 7, 2025
62bfdf3
web: Wi-Fi network list layout improvements
dgdavid Apr 7, 2025
5485267
web: improve Wi-Fi networks interface
dgdavid Apr 8, 2025
90666ab
web: add new route and components for wired connections
dgdavid Apr 8, 2025
bcf1be1
web: adapt network page
dgdavid Apr 8, 2025
4985d2e
fix(web): render IP settings form in a single column
dgdavid Apr 8, 2025
7acb5ae
web: change hover color for clickable data list items
dgdavid Apr 8, 2025
b769cdd
fix(web): add missing component
dgdavid Apr 8, 2025
3e1e5e9
fix(web): adapt DeviceState to the new values
imobachgs Apr 8, 2025
280ae20
fix(web): drop the unused ConnectionState enum
imobachgs Apr 8, 2025
675e42e
fix(web): drop an unused mock
imobachgs Apr 8, 2025
eba6f5c
fix(Web): fix NetworkPage test
imobachgs Apr 8, 2025
e923262
fix(web): web the unused WifiSelectorPage component
imobachgs Apr 8, 2025
10425be
fix(web): drop an unused plainRender import
imobachgs Apr 8, 2025
dcd3a77
fix(web): update cancel action on connection edit
dgdavid Apr 8, 2025
472c07c
fix(web): ignore aria-label errors in the WifiNetworksList tests
imobachgs Apr 9, 2025
fbba1c3
web: connection details adjustments
dgdavid Apr 9, 2025
be5492f
web: add empty state for Wi-Fi network not found
dgdavid Apr 9, 2025
416867f
web: use empty state for wired connection not found
dgdavid Apr 9, 2025
7f611a9
web: attemp to improve wifi connection form behavior
dgdavid Apr 9, 2025
4e6020d
fix(web): drop reference to not used variant
dgdavid Apr 10, 2025
224bfe7
fix(web): update WifiConnectionForm unit test
dgdavid Apr 10, 2025
229ba5d
Merge branch 'master' into better-network
imobachgs Apr 10, 2025
e54db5f
refactor(rust): move DevicesChangedStream to its own module
imobachgs Apr 10, 2025
0060f53
refactor(rust): add a common module for NM streams
imobachgs Apr 10, 2025
e2c90dc
feat(rust): expose the connection state
imobachgs Apr 10, 2025
5bb1c60
feat(web): add network connections state
imobachgs Apr 10, 2025
37c7682
fix(web): add missing property to network connection type
dgdavid Apr 11, 2025
70db700
fix(web): avoid crashing because malformed connection
dgdavid Apr 11, 2025
487ba67
web: make WifiConnectionForm less fragile
dgdavid Apr 11, 2025
3920799
web: fix broken unit test
dgdavid Apr 11, 2025
3c9d77a
fix(web): fix connection device detection
dgdavid Apr 15, 2025
5a42d49
fix(web): minor adjustments for Wi-Fi networks list
dgdavid Apr 15, 2025
f0ef57a
web: show spinner in Wi-Fi network when connecting
dgdavid Apr 15, 2025
9e0c003
feat(rust): do not write wireless security settings when not used
imobachgs Apr 16, 2025
cf1625e
fix(web): use "none" as security for public networks
dgdavid Apr 16, 2025
df6c7ce
fix(web): clarify signal strength in Wi-Fi connection details
dgdavid Apr 16, 2025
4dfe2f5
fix(rust): add ConnectionState to the OpenAPI spec
imobachgs Apr 18, 2025
3301cf1
Fix sorting of access points in the original query
teclator Apr 1, 2025
97c9459
Document the sorting of access points
teclator Apr 1, 2025
e2d318d
docs: update changes files
imobachgs Apr 21, 2025
0fe86f4
Merge branch 'master' into better-network
imobachgs Apr 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 31 additions & 62 deletions rust/agama-lib/src/network/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,71 +78,40 @@ pub enum DeviceType {
Bridge = 6,
}

// For now this mirrors NetworkManager, because it was less mental work than coming up with
// what exactly Agama needs. Expected to be adapted.
#[derive(Debug, Default, Clone, Copy, PartialEq, Serialize, Deserialize, utoipa::ToSchema)]
/// Network device state.
#[derive(
Default,
Serialize,
Deserialize,
Debug,
PartialEq,
Eq,
Clone,
Copy,
strum::Display,
strum::EnumString,
utoipa::ToSchema,
)]
#[strum(serialize_all = "camelCase")]
#[serde(rename_all = "camelCase")]
pub enum DeviceState {
#[default]
Unknown = 0,
Unmanaged = 10,
Unavailable = 20,
Disconnected = 30,
Prepare = 40,
Config = 50,
NeedAuth = 60,
IpConfig = 70,
IpCheck = 80,
Secondaries = 90,
Activated = 100,
Deactivating = 110,
Failed = 120,
}
#[derive(Debug, Error, PartialEq)]
#[error("Invalid state: {0}")]
pub struct InvalidDeviceState(String);

impl TryFrom<u8> for DeviceState {
type Error = InvalidDeviceState;

fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0 => Ok(DeviceState::Unknown),
10 => Ok(DeviceState::Unmanaged),
20 => Ok(DeviceState::Unavailable),
30 => Ok(DeviceState::Disconnected),
40 => Ok(DeviceState::Prepare),
50 => Ok(DeviceState::Config),
60 => Ok(DeviceState::NeedAuth),
70 => Ok(DeviceState::IpConfig),
80 => Ok(DeviceState::IpCheck),
90 => Ok(DeviceState::Secondaries),
100 => Ok(DeviceState::Activated),
110 => Ok(DeviceState::Deactivating),
120 => Ok(DeviceState::Failed),
_ => Err(InvalidDeviceState(value.to_string())),
}
}
}
impl fmt::Display for DeviceState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let name = match &self {
DeviceState::Unknown => "unknown",
DeviceState::Unmanaged => "unmanaged",
DeviceState::Unavailable => "unavailable",
DeviceState::Disconnected => "disconnected",
DeviceState::Prepare => "prepare",
DeviceState::Config => "config",
DeviceState::NeedAuth => "need_auth",
DeviceState::IpConfig => "ip_config",
DeviceState::IpCheck => "ip_check",
DeviceState::Secondaries => "secondaries",
DeviceState::Activated => "activated",
DeviceState::Deactivating => "deactivating",
DeviceState::Failed => "failed",
};
write!(f, "{}", name)
}
/// The device's state is unknown.
Unknown,
/// The device is recognized but not managed by Agama.
Unmanaged,
/// The device is detected but it cannot be used (wireless switched off, missing firmware, etc.).
Unavailable,
/// The device is connecting to the network.
Connecting,
/// The device is successfully connected to the network.
Connected,
/// The device is disconnecting from the network.
Disconnecting,
/// The device is disconnected from the network.
Disconnected,
/// The device failed to connect to a network.
Failed,
}

#[derive(Debug, Default, Clone, Copy, PartialEq, Serialize, Deserialize, utoipa::ToSchema)]
Expand Down
4 changes: 2 additions & 2 deletions rust/agama-server/src/network/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,7 @@ mod tests {
/// Network state
#[serde_as]
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct GeneralState {
pub hostname: String,
pub connectivity: bool,
Expand All @@ -479,7 +480,7 @@ pub struct AccessPoint {
/// Network device
#[serde_as]
#[skip_serializing_none]
#[derive(Default, Debug, Clone, Serialize, utoipa::ToSchema)]
#[derive(Default, Debug, Clone, PartialEq, Serialize, utoipa::ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct Device {
pub name: String,
Expand All @@ -491,7 +492,6 @@ pub struct Device {
// Connection.id
pub connection: Option<String>,
pub state: DeviceState,
pub state_reason: u8,
}

/// Represents a known network connection.
Expand Down
53 changes: 44 additions & 9 deletions rust/agama-server/src/network/nm/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ use anyhow::Context;
use cidr::IpInet;
use std::{collections::HashMap, net::IpAddr, str::FromStr};

use super::model::NmDeviceState;

/// Builder to create a [Device] from its corresponding NetworkManager D-Bus representation.
pub struct DeviceFromProxyBuilder<'a> {
connection: zbus::Connection,
Expand All @@ -57,21 +59,14 @@ impl<'a> DeviceFromProxyBuilder<'a> {
.try_into()
.context("Unsupported device type: {device_type}")?;

let state = self.proxy.state().await? as u8;
let (_, state_reason) = self.proxy.state_reason().await?;
let state: DeviceState = state
.try_into()
.context("Unsupported device state: {state}")?;

let mut device = Device {
name: self.proxy.interface().await?,
state: self.device_state_from_proxy().await?,
type_,
state,
state_reason: state_reason as u8,
..Default::default()
};

if state == DeviceState::Activated {
if device.state == DeviceState::Connected {
device.ip_config = self.build_ip_config().await?;
}

Expand Down Expand Up @@ -249,4 +244,44 @@ impl<'a> DeviceFromProxyBuilder<'a> {

Some(id.to_string())
}

/// Map the combination of state + reason to the Agama set of states.
///
/// See https://www.networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMDeviceState
/// and https://www.networkmanager.dev/docs/api/latest/nm-dbus-types.html#NMDeviceStateReason
/// for further information.
async fn device_state_from_proxy(&self) -> Result<DeviceState, ServiceError> {
const USER_REQUESTED: u32 = 39;

let (state, reason) = self.proxy.state_reason().await?;
let state: NmDeviceState = (state as u8)
.try_into()
.context("Unsupported device state: {state}")?;

let device_state = match state {
NmDeviceState::Unknown => DeviceState::Unknown,
NmDeviceState::Unmanaged => DeviceState::Unmanaged,
NmDeviceState::Unavailable => DeviceState::Unavailable,
NmDeviceState::Prepare
| NmDeviceState::IpConfig
| NmDeviceState::NeedAuth
| NmDeviceState::Config
| NmDeviceState::Secondaries
| NmDeviceState::IpCheck => DeviceState::Connecting,
NmDeviceState::Activated => DeviceState::Connected,
NmDeviceState::Deactivating => DeviceState::Disconnecting,
NmDeviceState::Disconnected => {
// If the connection failed, NetworkManager sets the state to "disconnected".
// Let's consider it a problem unless it was requested by the user.
if reason == USER_REQUESTED {
DeviceState::Disconnected
} else {
DeviceState::Failed
}
}
NmDeviceState::Failed => DeviceState::Failed,
};

Ok(device_state)
}
}
33 changes: 31 additions & 2 deletions rust/agama-server/src/network/nm/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ use super::proxies::{
AccessPointProxy, ActiveConnectionProxy, ConnectionProxy, DeviceProxy, NetworkManagerProxy,
SettingsProxy, WirelessProxy,
};
use crate::network::model::{AccessPoint, Connection, Device, GeneralState};
use crate::network::model::{
AccessPoint, Connection, ConnectionConfig, Device, GeneralState, SecurityProtocol,
};
use agama_lib::dbus::get_optional_property;
use agama_lib::error::ServiceError;
use agama_lib::network::types::{DeviceType, SSID};
use log;
Expand Down Expand Up @@ -200,11 +203,11 @@ impl<'a> NetworkManagerClient<'a> {
}

let settings = proxy.get_settings().await?;

let controller = controller_from_dbus(&settings)?;

match connection_from_dbus(settings) {
Ok(mut connection) => {
Self::add_secrets(&mut connection.config, &proxy).await?;
if let Some(controller) = controller {
controlled_by.insert(connection.uuid, controller.to_string());
}
Expand Down Expand Up @@ -366,4 +369,30 @@ impl<'a> NetworkManagerClient<'a> {

Ok(None)
}

/// Ancillary function to add secrets to a connection.
///
/// TODO: add support for more security protocols.
pub async fn add_secrets(
config: &mut ConnectionConfig,
proxy: &ConnectionProxy<'_>,
) -> Result<(), ServiceError> {
let ConnectionConfig::Wireless(ref mut wireless) = config else {
return Ok(());
};

if wireless.security == SecurityProtocol::WPA2 {
match proxy.get_secrets("802-11-wireless-security").await {
Ok(secrets) => {
if let Some(secret) = secrets.get("802-11-wireless-security") {
wireless.password = get_optional_property(&secret, "psk")?;
}
}
Err(error) => {
tracing::error!("Could not read connection secrets: {:?}", error);
}
}
}
Ok(())
}
}
46 changes: 46 additions & 0 deletions rust/agama-server/src/network/nm/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,52 @@ impl TryFrom<NmDeviceType> for DeviceType {
}
}

/// Device state
#[derive(Default, Debug, PartialEq, Copy, Clone)]
pub enum NmDeviceState {
#[default]
Unknown = 0,
Unmanaged = 10,
Unavailable = 20,
Disconnected = 30,
Prepare = 40,
Config = 50,
NeedAuth = 60,
IpConfig = 70,
IpCheck = 80,
Secondaries = 90,
Activated = 100,
Deactivating = 110,
Failed = 120,
}

#[derive(Debug, thiserror::Error, PartialEq)]
#[error("Invalid state: {0}")]
pub struct InvalidNmDeviceState(String);

impl TryFrom<u8> for NmDeviceState {
type Error = InvalidNmDeviceState;

fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0 => Ok(NmDeviceState::Unknown),
10 => Ok(NmDeviceState::Unmanaged),
20 => Ok(NmDeviceState::Unavailable),
30 => Ok(NmDeviceState::Disconnected),
40 => Ok(NmDeviceState::Prepare),
50 => Ok(NmDeviceState::Config),
60 => Ok(NmDeviceState::NeedAuth),
70 => Ok(NmDeviceState::IpConfig),
80 => Ok(NmDeviceState::IpCheck),
90 => Ok(NmDeviceState::Secondaries),
100 => Ok(NmDeviceState::Activated),
110 => Ok(NmDeviceState::Deactivating),
120 => Ok(NmDeviceState::Failed),
_ => Err(InvalidNmDeviceState(value.to_string())),
}
}
}

/// Key management
///
/// Using the newtype pattern around an String is enough. For proper support, we might replace this
Expand Down
5 changes: 5 additions & 0 deletions rust/agama-server/src/network/system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,11 @@ impl<T: Adapter> NetworkSystemServer<T> {
return Ok(Some(NetworkChange::DeviceAdded(*device)));
}
Action::UpdateDevice(name, device) => {
if let Some(old_device) = self.state.get_device(&name) {
if old_device == device.as_ref() {
return Ok(None);
}
}
self.state.update_device(&name, *device.clone())?;
return Ok(Some(NetworkChange::DeviceUpdated(name, *device)));
}
Expand Down
2 changes: 1 addition & 1 deletion rust/agama-server/tests/network_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ async fn test_network_state() -> Result<(), Box<dyn Error>> {
let response = network_service.oneshot(request).await?;
assert_eq!(response.status(), StatusCode::OK);
let body = body_to_string(response.into_body()).await;
assert!(body.contains(r#""wireless_enabled":false"#));
assert!(body.contains(r#""wirelessEnabled":false"#));
Ok(())
}

Expand Down
11 changes: 11 additions & 0 deletions web/src/assets/styles/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,13 @@ label.pf-m-disabled + .pf-v6-c-check__description {
justify-self: flex-start;
}

// Fix select toggle icon visibility by creating more space at the end :/
.pf-v6-c-form-control > select {
--pf-v6-c-form-control--PaddingInlineEnd: calc(
var(--pf-v6-c-form-control__select--PaddingInlineEnd) * 3
);
}

.pf-v6-c-check {
row-gap: var(--pf-t--global--spacer--xs);
align-content: baseline;
Expand All @@ -401,6 +408,10 @@ label.pf-m-disabled + .pf-v6-c-check__description {
--pf-v6-c-list--m-inline--ColumnGap: var(--pf-t--global--spacer--md);
}

.pf-v6-c-data-list li.pf-m-clickable {
--pf-v6-c-data-list__item--hover--BackgroundColor: var(--agm-t--action--background--color--hover);
}

// Nested content inside forms should respect parent grid gap
.pf-v6-c-form [class*="pf-v6-u-mx"] {
display: grid;
Expand Down
8 changes: 6 additions & 2 deletions web/src/components/layout/Icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,10 @@ import Lock from "@icons/lock.svg?component";
import ManageAccounts from "@icons/manage_accounts.svg?component";
import Menu from "@icons/menu.svg?component";
import MoreVert from "@icons/more_vert.svg?component";
import NetworkWifi from "@icons/network_wifi.svg?component";
import NetworkWifi1Bar from "@icons/network_wifi_1_bar.svg?component";
import NetworkWifi3Bar from "@icons/network_wifi_3_bar.svg?component";
import SettingsEthernet from "@icons/settings_ethernet.svg?component";
import SignalCellularAlt from "@icons/signal_cellular_alt.svg?component";
import Warning from "@icons/warning.svg?component";
import Visibility from "@icons/visibility.svg?component";
import VisibilityOff from "@icons/visibility_off.svg?component";
Expand All @@ -71,8 +73,10 @@ const icons = {
manage_accounts: ManageAccounts,
menu: Menu,
more_vert: MoreVert,
network_wifi: NetworkWifi,
network_wifi_1_bar: NetworkWifi1Bar,
network_wifi_3_bar: NetworkWifi3Bar,
settings_ethernet: SettingsEthernet,
signal_cellular_alt: SignalCellularAlt,
visibility: Visibility,
visibility_off: VisibilityOff,
warning: Warning,
Expand Down
Loading