mirror of
https://github.com/rzuasti/oott.git
synced 2026-07-08 19:21:54 +02:00
Add edit dialog for registered devices
Extend PUT /api/devices/{mac} to accept an optional name and surface an
Edit action from both the device detail screen and the per-row overflow
menu, letting users modify owner, device type, vendor and name without
forgetting and re-registering.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
3a579f0321
commit
0f6296759d
@@ -11,17 +11,19 @@
|
||||
- [x] Add date filter to device event list method (from)
|
||||
- [x] Add a data set (json) to store maps from vendors -> device type; modify the device creation so that it uses it automatically
|
||||
- [x] Separate the devices "update" method into update and seen (one for API edit one for scanners)
|
||||
- [ ] Add the scanner that triggered the event to the device_events table
|
||||
- [ ] Improve notifications layout/text
|
||||
|
||||
## Frontend
|
||||
|
||||
- [ ] Rework the devices list into a list when the screen is wide enough
|
||||
- [ ] Display the device "name" in the list and details
|
||||
- [ ] When registering a device ask for the name
|
||||
- [ ] Add a device edit feature for registered devices
|
||||
- [ ] Review how we are using DIO and improve error handling (500 error page?)
|
||||
- [x] Rework the devices list into a list when the screen is wide enough
|
||||
- [x] Display the device "name" in the list and details
|
||||
- [x] When registering a device ask for the name
|
||||
- [x] Add a device edit feature for registered devices
|
||||
- [x] Add an mDNS/Bonjour scanner
|
||||
- [x] Extract the select_interface method and interface logic from ARP scanner to a centralized utility file
|
||||
- [x] Figure out if we can univocally identify devices that mask their MAC address (like apple)
|
||||
- [ ] Add the scanner that triggered the event to the device_events table
|
||||
|
||||
## Frontend
|
||||
|
||||
|
||||
@@ -275,12 +275,13 @@ pub fn update(
|
||||
owner: String,
|
||||
device_type: String,
|
||||
vendor: String,
|
||||
name: Option<String>,
|
||||
) -> Result<(), DbError> {
|
||||
let conn = db::get_db_connection();
|
||||
|
||||
match conn.execute(
|
||||
"UPDATE devices SET owner=?1, device_type=?2, vendor=?3 WHERE mac_address=?4",
|
||||
params![owner, device_type, vendor, mac_address],
|
||||
"UPDATE devices SET owner=?1, device_type=?2, vendor=?3, name=?4 WHERE mac_address=?5",
|
||||
params![owner, device_type, vendor, name, mac_address],
|
||||
) {
|
||||
Ok(_) => {
|
||||
debug!("Device updated in database: {mac_address}");
|
||||
@@ -842,6 +843,7 @@ mod tests {
|
||||
"Bob".to_string(),
|
||||
"Laptop".to_string(),
|
||||
"New Vendor".to_string(),
|
||||
Some("kitchen-pc".to_string()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -849,11 +851,24 @@ mod tests {
|
||||
assert_eq!(device.owner, "Bob".to_string());
|
||||
assert_eq!(device.device_type, "Laptop".to_string());
|
||||
assert_eq!(device.vendor, "New Vendor".to_string());
|
||||
assert_eq!(device.name, Some("kitchen-pc".to_string()));
|
||||
// Sighting and registration fields are untouched
|
||||
assert_eq!(device.ipv4_address, "192.168.250.1".to_string());
|
||||
assert_eq!(device.last_seen, last_seen);
|
||||
assert_eq!(device.name, Some("host.local".to_string()));
|
||||
assert!(device.is_registered);
|
||||
|
||||
// Passing None clears the name (edit dialog is an explicit user action — "what you see
|
||||
// in the dialog is what gets saved", unlike register() which preserves on None).
|
||||
update(
|
||||
"mm:mm:mm:mm:mm:01".to_string(),
|
||||
"Bob".to_string(),
|
||||
"Laptop".to_string(),
|
||||
"New Vendor".to_string(),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let device = read("mm:mm:mm:mm:mm:01".to_string()).unwrap();
|
||||
assert_eq!(device.name, None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -166,8 +166,8 @@ pub async fn update(
|
||||
Json(payload): Json<UpdateDevicePayload>,
|
||||
) -> impl IntoResponse {
|
||||
debug!(
|
||||
"Device update received: mac_address={}, owner={}, device_type={}, vendor={}",
|
||||
mac_address, payload.owner, payload.device_type, payload.vendor
|
||||
"Device update received: mac_address={}, owner={}, device_type={}, vendor={}, name={:?}",
|
||||
mac_address, payload.owner, payload.device_type, payload.vendor, payload.name
|
||||
);
|
||||
|
||||
let device = match db::devices::read(mac_address.clone()) {
|
||||
@@ -192,6 +192,7 @@ pub async fn update(
|
||||
payload.owner,
|
||||
payload.device_type,
|
||||
payload.vendor,
|
||||
payload.name,
|
||||
) {
|
||||
Ok(_) => (axum::http::StatusCode::OK, "Device updated"),
|
||||
Err(err) => {
|
||||
@@ -283,4 +284,5 @@ pub struct UpdateDevicePayload {
|
||||
owner: String,
|
||||
device_type: String,
|
||||
vendor: String,
|
||||
name: Option<String>,
|
||||
}
|
||||
|
||||
@@ -46,6 +46,121 @@ Future<void> confirmForgetDevice(
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> showEditDeviceDialog(
|
||||
BuildContext context,
|
||||
Device device,
|
||||
VoidCallback onRefresh,
|
||||
) async {
|
||||
final formKey = GlobalKey<FormState>();
|
||||
String owner = device.owner;
|
||||
String name = device.name ?? '';
|
||||
String vendor = device.vendor;
|
||||
DeviceType deviceType = device.deviceType;
|
||||
|
||||
final saved = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => StatefulBuilder(
|
||||
builder: (context, setDialogState) {
|
||||
void save() {
|
||||
if (formKey.currentState?.validate() ?? false) {
|
||||
formKey.currentState?.save();
|
||||
Navigator.of(context).pop(true);
|
||||
}
|
||||
}
|
||||
|
||||
return AlertDialog(
|
||||
title: const Text('Edit Device'),
|
||||
content: Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextFormField(
|
||||
initialValue: owner,
|
||||
decoration: const InputDecoration(labelText: 'Owner'),
|
||||
autofocus: true,
|
||||
onFieldSubmitted: (_) => save(),
|
||||
validator: (value) => value == null || value.trim().isEmpty
|
||||
? 'Owner is required'
|
||||
: null,
|
||||
onSaved: (value) => owner = value?.trim() ?? '',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
initialValue: name,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Name (optional)',
|
||||
),
|
||||
onFieldSubmitted: (_) => save(),
|
||||
onSaved: (value) => name = value?.trim() ?? '',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
initialValue: vendor,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Vendor (optional)',
|
||||
),
|
||||
onFieldSubmitted: (_) => save(),
|
||||
onSaved: (value) => vendor = value?.trim() ?? '',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
DropdownButtonFormField<DeviceType>(
|
||||
initialValue: deviceType,
|
||||
decoration: const InputDecoration(labelText: 'Device Type'),
|
||||
items: DeviceType.values
|
||||
.map(
|
||||
(t) => DropdownMenuItem(
|
||||
value: t,
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(t.icon, size: 16),
|
||||
const SizedBox(width: 4),
|
||||
Text(t.label),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (value) =>
|
||||
setDialogState(() => deviceType = value ?? deviceType),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: save,
|
||||
child: const Text('Save'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
if (saved != true || !context.mounted) return;
|
||||
|
||||
try {
|
||||
await BackendAPI.instance.updateDevice(
|
||||
device.macAddress,
|
||||
owner,
|
||||
deviceType.apiName,
|
||||
vendor,
|
||||
name: name.isEmpty ? null : name,
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
UISnackbars.showSuccess(context, 'Device updated');
|
||||
onRefresh();
|
||||
} catch (e) {
|
||||
if (!context.mounted) return;
|
||||
UISnackbars.showError(context, 'Failed to update device: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> showRegisterDeviceDialog(
|
||||
BuildContext context,
|
||||
Device device,
|
||||
|
||||
@@ -199,7 +199,16 @@ class _DeviceActions extends StatelessWidget {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
if (device.isRegistered) {
|
||||
return OutlinedButton.icon(
|
||||
return Wrap(
|
||||
spacing: 12,
|
||||
runSpacing: 12,
|
||||
children: [
|
||||
FilledButton.icon(
|
||||
onPressed: () => showEditDeviceDialog(context, device, onAction),
|
||||
icon: const Icon(Icons.edit),
|
||||
label: const Text('Edit'),
|
||||
),
|
||||
OutlinedButton.icon(
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: colorScheme.error,
|
||||
side: BorderSide(color: colorScheme.error),
|
||||
@@ -207,6 +216,8 @@ class _DeviceActions extends StatelessWidget {
|
||||
onPressed: () => confirmForgetDevice(context, device, onAction),
|
||||
icon: const Icon(Icons.link_off),
|
||||
label: const Text('Forget Device'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
return FilledButton.icon(
|
||||
|
||||
@@ -333,6 +333,8 @@ class _DeviceActionsMenu extends StatelessWidget {
|
||||
onSelected: (value) async {
|
||||
if (value == 'details') {
|
||||
context.push('/devices/${device.macAddress}');
|
||||
} else if (value == 'edit') {
|
||||
await showEditDeviceDialog(context, device, onRefresh);
|
||||
} else if (value == 'forget') {
|
||||
await confirmForgetDevice(context, device, onRefresh);
|
||||
} else if (value == 'register') {
|
||||
@@ -341,6 +343,8 @@ class _DeviceActionsMenu extends StatelessWidget {
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
const PopupMenuItem(value: 'details', child: Text('View details')),
|
||||
if (device.isRegistered)
|
||||
const PopupMenuItem(value: 'edit', child: Text('Edit')),
|
||||
if (device.isRegistered)
|
||||
const PopupMenuItem(value: 'forget', child: Text('Forget')),
|
||||
if (!device.isRegistered)
|
||||
|
||||
@@ -150,6 +150,25 @@ class BackendAPI {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> updateDevice(
|
||||
String macAddress,
|
||||
String owner,
|
||||
String deviceType,
|
||||
String vendor, {
|
||||
String? name,
|
||||
}) async {
|
||||
debugPrint('About to call PUT /devices/$macAddress');
|
||||
await _dio.put(
|
||||
'/devices/$macAddress',
|
||||
data: {
|
||||
'owner': owner,
|
||||
'device_type': deviceType,
|
||||
'vendor': vendor,
|
||||
'name': name,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> forgetDevice(String macAddress) async {
|
||||
debugPrint('About to call DELETE /devices/$macAddress');
|
||||
await _dio.delete('/devices/$macAddress');
|
||||
|
||||
Reference in New Issue
Block a user