Skip to content

Commit 1558c99

Browse files
authored
refactor(web): enhance network page (#3247)
## Problem Beyond the usual enhancements, there was some pending work to make the network pages more useful for users. The following issues were identified as high priority: * Associated devices to connection: it is difficult to identify which devices belong to each connection. Improving the connection list to clearly show associated devices would make it easier for users to manage their networks. * Contextual actions for connections: Users can only click to view a connection and then edit it. Would be better providing a contextual menu with actions such as view details, edit, connect, or delete allows users to perform common tasks more efficiently. * Creating new connections: Users can only edit existing connections, which limits a lot the network configuration. Adding the ability to create new connections will address this gap and improve overall usability. Related to, - https://trello.com/c/jvgwGFIl/5581-8-bsc1247441-agama-ui-does-not-allow-to-add-or-remove-network-connections (protected link) - https://bugzilla.suse.com/show_bug.cgi?id=1247441 (protected link) - https://jira.suse.com/browse/PED-15913 (protected link) ## Solution * Backend enhancements: Added a backend improvement to notify the system about added or removed connections, as well as changes to devices, ensuring the internal state stays in sync (see PR #3244 ). * Connections table: Brought back the connections table, enhanced to give users more control and visibility over their connections. * Manage connections: Users can now add and remove connections directly from the UI. * IP settings improvements: Added the dnsSearchList field to the connection IPSettingsForm to support additional DNS configuration options (related to https://jira.suse.com/browse/PED-15913).
2 parents f25544d + 76140a6 commit 1558c99

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+2421
-1030
lines changed

rust/agama-lib/share/profile.schema.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -365,13 +365,21 @@
365365
"type": "string"
366366
}
367367
},
368-
"dnsSearchlist": {
368+
"dnsSearchList": {
369369
"type": "array",
370370
"items": {
371371
"description": "DNS search domains",
372372
"type": "string"
373373
}
374374
},
375+
"dnsSearchlist": {
376+
"type": "array",
377+
"items": {
378+
"description": "DNS search domains",
379+
"type": "string"
380+
},
381+
"deprecated": true
382+
},
375383
"ignoreAutoDns": {
376384
"description": "Whether DNS options provided via DHCP are used or not",
377385
"type": "boolean"

rust/agama-network/src/action.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ pub enum Action {
5454
GetConnections(Responder<Vec<Connection>>),
5555
/// Gets all scanned access points
5656
GetAccessPoints(Responder<Vec<AccessPoint>>),
57+
/// Adds a new access point.
58+
AddAccessPoint(Box<AccessPoint>),
59+
/// Removes an access point by its hardware address.
60+
RemoveAccessPoint(String),
5761
/// Adds a new device.
5862
AddDevice(Box<Device>),
5963
/// Updates a device by its `name`.
@@ -66,7 +70,7 @@ pub enum Action {
6670
GetDevices(Responder<Vec<Device>>),
6771
GetGeneralState(Responder<GeneralState>),
6872
/// Connection state changed
69-
ChangeConnectionState(String, ConnectionState),
73+
ChangeConnectionState(Uuid, ConnectionState),
7074
/// Persists existing connections if none exist and the network copy is not disabled.
7175
ProposeDefault(Responder<Result<(), NetworkStateError>>),
7276
// Copies persistent connections to the target system
@@ -77,8 +81,8 @@ pub enum Action {
7781
UpdateGeneralState(GeneralState),
7882
/// Forces a wireless networks scan refresh
7983
RefreshScan(Responder<Result<(), NetworkAdapterError>>),
80-
/// Remove the connection with the given ID.
81-
RemoveConnection(String),
84+
/// Remove the connection with the given UUID.
85+
RemoveConnection(Uuid),
8286
/// Apply the current configuration.
8387
Apply(Responder<Result<(), NetworkAdapterError>>),
8488
}

rust/agama-network/src/error.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ pub enum NetworkStateError {
4040
CannotUpdateConnection(String),
4141
#[error("Unknown device '{0}'")]
4242
UnknownDevice(String),
43+
#[error("Unknown access point '{0}'")]
44+
UnknownAccessPoint(String),
4345
#[error("Invalid connection UUID: '{0}'")]
4446
InvalidUuid(String),
4547
#[error("Invalid IP address: '{0}'")]

rust/agama-network/src/model.rs

Lines changed: 106 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -296,12 +296,10 @@ impl NetworkState {
296296

297297
/// Updates a connection with a new one.
298298
///
299-
/// It uses the `id` to decide which connection to update.
300-
///
301-
/// Additionally, it registers the connection to be removed when the changes are applied.
299+
/// It uses the `uuid` to decide which connection to update.
302300
pub fn update_connection(&mut self, conn: Connection) -> Result<(), NetworkStateError> {
303-
let Some(old_conn) = self.get_connection_mut(&conn.id) else {
304-
return Err(NetworkStateError::UnknownConnection(conn.id.clone()));
301+
let Some(old_conn) = self.get_connection_by_uuid_mut(conn.uuid) else {
302+
return Err(NetworkStateError::UnknownConnection(conn.uuid.to_string()));
305303
};
306304
*old_conn = conn;
307305

@@ -311,9 +309,9 @@ impl NetworkState {
311309
/// Removes a connection from the state.
312310
///
313311
/// Additionally, it registers the connection to be removed when the changes are applied.
314-
pub fn remove_connection(&mut self, id: &str) -> Result<(), NetworkStateError> {
315-
let Some(position) = self.connections.iter().position(|d| d.id == id) else {
316-
return Err(NetworkStateError::UnknownConnection(id.to_string()));
312+
pub fn remove_connection(&mut self, uuid: Uuid) -> Result<(), NetworkStateError> {
313+
let Some(position) = self.connections.iter().position(|d| d.uuid == uuid) else {
314+
return Err(NetworkStateError::UnknownConnection(uuid.to_string()));
317315
};
318316

319317
self.connections.remove(position);
@@ -343,6 +341,34 @@ impl NetworkState {
343341
Ok(())
344342
}
345343

344+
pub fn add_access_point(&mut self, ap: AccessPoint) -> Result<(), NetworkStateError> {
345+
if let Some(position) = self
346+
.access_points
347+
.iter()
348+
.position(|a| a.hw_address == ap.hw_address)
349+
{
350+
self.access_points.remove(position);
351+
}
352+
self.access_points.push(ap);
353+
354+
Ok(())
355+
}
356+
357+
pub fn remove_access_point(&mut self, hw_address: &str) -> Result<(), NetworkStateError> {
358+
let Some(position) = self
359+
.access_points
360+
.iter()
361+
.position(|a| a.hw_address == hw_address)
362+
else {
363+
return Err(NetworkStateError::UnknownAccessPoint(
364+
hw_address.to_string(),
365+
));
366+
};
367+
368+
self.access_points.remove(position);
369+
Ok(())
370+
}
371+
346372
/// Sets a controller's ports.
347373
///
348374
/// If the connection is not a controller, returns an error.
@@ -417,22 +443,23 @@ mod tests {
417443
#[test]
418444
fn test_update_connection() {
419445
let mut state = NetworkState::default();
446+
let uuid = Uuid::new_v4();
420447
let conn0 = Connection {
421448
id: "eth0".to_string(),
422-
uuid: Uuid::new_v4(),
449+
uuid,
423450
..Default::default()
424451
};
425452
state.add_connection(conn0).unwrap();
426453

427-
let uuid = Uuid::new_v4();
428454
let conn1 = Connection {
429455
id: "eth0".to_string(),
430456
uuid,
457+
firewall_zone: Some("public".to_string()),
431458
..Default::default()
432459
};
433460
state.update_connection(conn1).unwrap();
434-
let found = state.get_connection("eth0").unwrap();
435-
assert_eq!(found.uuid, uuid);
461+
let found = state.get_connection_by_uuid(uuid).unwrap();
462+
assert_eq!(found.firewall_zone, Some("public".to_string()));
436463
}
437464

438465
#[test]
@@ -446,20 +473,77 @@ mod tests {
446473
#[test]
447474
fn test_remove_connection() {
448475
let mut state = NetworkState::default();
449-
let conn0 = Connection::new("eth0".to_string(), DeviceType::Ethernet);
476+
let uuid = Uuid::new_v4();
477+
let conn0 = Connection {
478+
id: "eth0".to_string(),
479+
uuid,
480+
..Default::default()
481+
};
450482
state.add_connection(conn0).unwrap();
451-
state.remove_connection("eth0".as_ref()).unwrap();
452-
let found = state.get_connection("eth0");
483+
state.remove_connection(uuid).unwrap();
484+
let found = state.get_connection_by_uuid(uuid);
453485
assert!(found.is_none());
454486
}
455487

456488
#[test]
457489
fn test_remove_unknown_connection() {
458490
let mut state = NetworkState::default();
459-
let error = state.remove_connection("unknown".as_ref()).unwrap_err();
491+
let uuid = Uuid::new_v4();
492+
let error = state.remove_connection(uuid).unwrap_err();
460493
assert!(matches!(error, NetworkStateError::UnknownConnection(_)));
461494
}
462495

496+
#[test]
497+
fn test_remove_device() {
498+
let mut state = NetworkState::default();
499+
let device = Device {
500+
name: "eth0".to_string(),
501+
..Default::default()
502+
};
503+
state.add_device(device).unwrap();
504+
state.remove_device("eth0").unwrap();
505+
assert!(state.get_device("eth0").is_none());
506+
}
507+
508+
#[test]
509+
fn test_add_access_point() {
510+
let mut state = NetworkState::default();
511+
let ap = AccessPoint {
512+
hw_address: "AA:BB:CC:DD:EE:FF".to_string(),
513+
ssid: SSID(b"test".to_vec()),
514+
..Default::default()
515+
};
516+
state.add_access_point(ap.clone()).unwrap();
517+
assert_eq!(state.access_points.len(), 1);
518+
assert_eq!(state.access_points[0].hw_address, "AA:BB:CC:DD:EE:FF");
519+
520+
// Adding same AP should replace it (in our implementation we remove and push)
521+
let mut ap2 = ap.clone();
522+
ap2.strength = 80;
523+
state.add_access_point(ap2).unwrap();
524+
assert_eq!(state.access_points.len(), 1);
525+
assert_eq!(state.access_points[0].strength, 80);
526+
}
527+
528+
#[test]
529+
fn test_remove_access_point() {
530+
let mut state = NetworkState::default();
531+
let ap = AccessPoint {
532+
hw_address: "AA:BB:CC:DD:EE:FF".to_string(),
533+
..Default::default()
534+
};
535+
state.add_access_point(ap).unwrap();
536+
state.remove_access_point("AA:BB:CC:DD:EE:FF").unwrap();
537+
assert_eq!(state.access_points.len(), 0);
538+
}
539+
540+
#[test]
541+
fn test_remove_unknown_access_point() {
542+
let mut state = NetworkState::default();
543+
let error = state.remove_access_point("unknown").unwrap_err();
544+
assert!(matches!(error, NetworkStateError::UnknownAccessPoint(_)));
545+
}
546+
463547
#[test]
464548
fn test_is_loopback() {
465549
let conn = Connection::new("eth0".to_string(), DeviceType::Ethernet);
@@ -1726,7 +1810,7 @@ pub struct TunConfig {
17261810
#[serde(rename_all = "camelCase")]
17271811
pub enum NetworkChange {
17281812
ConnectionAdded(Connection),
1729-
ConnectionRemoved(String),
1813+
ConnectionRemoved(Uuid),
17301814
/// A new device has been added.
17311815
DeviceAdded(Device),
17321816
/// A device has been removed.
@@ -1737,9 +1821,13 @@ pub enum NetworkChange {
17371821
DeviceUpdated(String, Device),
17381822
/// A connection state has changed.
17391823
ConnectionStateChanged {
1740-
id: String,
1824+
uuid: Uuid,
17411825
state: ConnectionState,
17421826
},
1827+
/// A new access point has been added.
1828+
AccessPointAdded(AccessPoint),
1829+
/// An access point has been removed.
1830+
AccessPointRemoved(String),
17431831
}
17441832

17451833
#[derive(Default, Debug, PartialEq, Clone, Deserialize, Serialize, utoipa::ToSchema)]

rust/agama-network/src/nm/builder.rs

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,12 @@ use crate::{
2525
nm::{
2626
dbus::connection_from_dbus,
2727
model::NmDeviceType,
28-
proxies::{ConnectionProxy, DeviceProxy, IP4ConfigProxy, IP6ConfigProxy},
28+
proxies::{AccessPointProxy, ConnectionProxy, DeviceProxy, IP4ConfigProxy, IP6ConfigProxy},
29+
},
30+
types::{
31+
AccessPoint, ConnectionFlags, Device, DeviceState, DeviceType, IpConfig, IpRoute,
32+
MacAddress, SSID,
2933
},
30-
types::{ConnectionFlags, Device, DeviceState, DeviceType, IpConfig, IpRoute, MacAddress},
3134
};
3235
use cidr::IpInet;
3336
use std::{collections::HashMap, net::IpAddr, str::FromStr};
@@ -44,6 +47,38 @@ pub struct DeviceFromProxyBuilder<'a> {
4447
proxy: &'a DeviceProxy<'a>,
4548
}
4649

50+
/// Builder to create an [AccessPoint] from its corresponding NetworkManager D-Bus representation.
51+
pub struct AccessPointFromProxyBuilder<'a> {
52+
device_name: String,
53+
proxy: &'a AccessPointProxy<'a>,
54+
}
55+
56+
impl<'a> AccessPointFromProxyBuilder<'a> {
57+
pub fn new(device_name: String, proxy: &'a AccessPointProxy<'a>) -> Self {
58+
Self { device_name, proxy }
59+
}
60+
61+
/// Creates an [AccessPoint] starting on the [AccessPointProxy].
62+
pub async fn build(&self) -> Result<AccessPoint, NmError> {
63+
let ssid = SSID(self.proxy.ssid().await?);
64+
let hw_address = self.proxy.hw_address().await?;
65+
let strength = self.proxy.strength().await?;
66+
let flags = self.proxy.flags().await?;
67+
let rsn_flags = self.proxy.rsn_flags().await?;
68+
let wpa_flags = self.proxy.wpa_flags().await?;
69+
70+
Ok(AccessPoint {
71+
device: self.device_name.clone(),
72+
ssid,
73+
hw_address,
74+
strength,
75+
flags,
76+
rsn_flags,
77+
wpa_flags,
78+
})
79+
}
80+
}
81+
4782
impl<'a> ConnectionFromProxyBuilder<'a> {
4883
pub fn new(_connection: &zbus::Connection, proxy: &'a ConnectionProxy<'a>) -> Self {
4984
Self { proxy }
@@ -139,6 +174,7 @@ impl<'a> DeviceFromProxyBuilder<'a> {
139174
) -> Result<IpConfig, NmError> {
140175
let address_data = ip4_proxy.address_data().await?;
141176
let nameserver_data = ip4_proxy.nameserver_data().await?;
177+
let mut dns_searchlist = ip4_proxy.searches().await?;
142178
let mut addresses: Vec<IpInet> = vec![];
143179
let mut nameservers: Vec<IpAddr> = vec![];
144180

@@ -160,6 +196,12 @@ impl<'a> DeviceFromProxyBuilder<'a> {
160196
nameservers.push(address)
161197
}
162198
}
199+
200+
for search in ip6_proxy.searches().await? {
201+
if !dns_searchlist.contains(&search) {
202+
dns_searchlist.push(search);
203+
}
204+
}
163205
// FIXME: Convert from Vec<u8> to [u8; 16] and take into account big vs little endian order,
164206
// in IP6Config there is no nameserver-data.
165207
//
@@ -188,6 +230,7 @@ impl<'a> DeviceFromProxyBuilder<'a> {
188230
let mut ip_config = IpConfig {
189231
addresses,
190232
nameservers,
233+
dns_searchlist,
191234
routes4,
192235
routes6,
193236
..Default::default()

rust/agama-network/src/nm/streams/common.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ pub enum NmChange {
3535
ActiveConnectionAdded(OwnedObjectPath),
3636
ActiveConnectionUpdated(OwnedObjectPath),
3737
ActiveConnectionRemoved(OwnedObjectPath),
38+
AccessPointAdded(OwnedObjectPath, OwnedObjectPath),
39+
AccessPointRemoved(OwnedObjectPath, OwnedObjectPath),
3840
}
3941

4042
pub async fn build_added_and_removed_stream(
@@ -63,3 +65,17 @@ pub async fn build_properties_changed_stream(
6365
let stream = MessageStream::for_match_rule(rule, connection, Some(1)).await?;
6466
Ok(stream)
6567
}
68+
69+
/// Returns a stream of wireless signals to be used by DeviceChangedStream.
70+
///
71+
/// It listens for AccessPointAdded and AccessPointRemoved signals.
72+
pub async fn build_wireless_signals_stream(
73+
connection: &zbus::Connection,
74+
) -> Result<MessageStream, NmError> {
75+
let rule = MatchRule::builder()
76+
.msg_type(MessageType::Signal)
77+
.interface("org.freedesktop.NetworkManager.Device.Wireless")?
78+
.build();
79+
let stream = MessageStream::for_match_rule(rule, connection, Some(1)).await?;
80+
Ok(stream)
81+
}

0 commit comments

Comments
 (0)