mirror of
https://github.com/rzuasti/oott.git
synced 2026-07-08 19:21:54 +02:00
Add owner and device type filters to device list
Owner filter uses a server-side substring match (LIKE '%VALUE%'); device type filter matches the stored value exactly, sending an empty string for unknown/unclassified devices. Both filters combine with the existing registration status chip via AND logic. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
bf74cbb38e
commit
e7d075ec29
@@ -54,8 +54,8 @@ pub fn list_devices(
|
|||||||
};
|
};
|
||||||
if let Some(owner) = owner {
|
if let Some(owner) = owner {
|
||||||
debug!("Adding filter owner={}", owner);
|
debug!("Adding filter owner={}", owner);
|
||||||
sql_statement.push_str("AND owner=? ");
|
sql_statement.push_str("AND owner LIKE ? ");
|
||||||
params.push(owner.into());
|
params.push(format!("%{}%", owner).into());
|
||||||
};
|
};
|
||||||
if let Some(device_type) = device_type {
|
if let Some(device_type) = device_type {
|
||||||
debug!("Adding filter device_type={}", device_type);
|
debug!("Adding filter device_type={}", device_type);
|
||||||
@@ -241,6 +241,27 @@ mod tests {
|
|||||||
"Vendor 2".to_string(),
|
"Vendor 2".to_string(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Filter by owner substring - "oh" matches "John" but not "Sarah"
|
||||||
|
let devices: Vec<Device> =
|
||||||
|
list_devices(None, None, None, Some("oh".to_string()), None, None).unwrap();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
devices.len() >= 1,
|
||||||
|
"There should be at least one device matching owner 'oh'"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
devices
|
||||||
|
.iter()
|
||||||
|
.any(|item| item.mac_address == "bb:bb:bb:bb:bb:bb"),
|
||||||
|
"Device bb:bb:bb:bb:bb:bb (John) should be present"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!devices
|
||||||
|
.iter()
|
||||||
|
.any(|item| item.mac_address == "cc:cc:cc:cc:cc:cc"),
|
||||||
|
"Device cc:cc:cc:cc:cc:cc (Sarah) should not be present"
|
||||||
|
);
|
||||||
|
|
||||||
// List devices seen in a time period - 2026-02-03 13:14:15
|
// List devices seen in a time period - 2026-02-03 13:14:15
|
||||||
let devices = list_devices(
|
let devices = list_devices(
|
||||||
None,
|
None,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
@@ -22,18 +24,29 @@ class _DeviceListState extends State<DeviceList> {
|
|||||||
List<Device> _devices = [];
|
List<Device> _devices = [];
|
||||||
bool _isLoading = true;
|
bool _isLoading = true;
|
||||||
String? _error;
|
String? _error;
|
||||||
|
final TextEditingController _ownerController = TextEditingController();
|
||||||
|
DeviceType? _typeFilter;
|
||||||
|
Timer? _ownerDebounce;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_ownerController.addListener(_onOwnerChanged);
|
||||||
_loadDevices();
|
_loadDevices();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_ownerDebounce?.cancel();
|
||||||
|
_ownerController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onOwnerChanged() {
|
||||||
|
_ownerDebounce?.cancel();
|
||||||
|
_ownerDebounce = Timer(const Duration(milliseconds: 500), _loadDevices);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _loadDevices() async {
|
Future<void> _loadDevices() async {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isLoading = _devices.isEmpty;
|
_isLoading = _devices.isEmpty;
|
||||||
@@ -46,6 +59,8 @@ class _DeviceListState extends State<DeviceList> {
|
|||||||
|
|
||||||
final devices = await BackendAPI.instance.listDevices(
|
final devices = await BackendAPI.instance.listDevices(
|
||||||
isRegistered: isRegistered,
|
isRegistered: isRegistered,
|
||||||
|
owner: _ownerController.text.isEmpty ? null : _ownerController.text,
|
||||||
|
deviceType: _typeFilter,
|
||||||
);
|
);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -81,9 +96,14 @@ class _DeviceListState extends State<DeviceList> {
|
|||||||
appBar: AppBar(title: const Text('Devices')),
|
appBar: AppBar(title: const Text('Devices')),
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Padding(
|
||||||
height: 50,
|
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
|
||||||
alignment: Alignment.centerRight,
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
child: Wrap(
|
child: Wrap(
|
||||||
spacing: 8.0,
|
spacing: 8.0,
|
||||||
children: [
|
children: [
|
||||||
@@ -114,6 +134,60 @@ class _DeviceListState extends State<DeviceList> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
flex: 2,
|
||||||
|
child: TextField(
|
||||||
|
controller: _ownerController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Owner',
|
||||||
|
prefixIcon: Icon(Icons.person_outline),
|
||||||
|
isDense: true,
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: DropdownButtonFormField<DeviceType?>(
|
||||||
|
initialValue: _typeFilter,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Type',
|
||||||
|
isDense: true,
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
items: [
|
||||||
|
const DropdownMenuItem(
|
||||||
|
value: null,
|
||||||
|
child: Text('All'),
|
||||||
|
),
|
||||||
|
...DeviceType.values.map(
|
||||||
|
(t) => DropdownMenuItem(
|
||||||
|
value: t,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(t.icon, size: 16),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(t.label),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() => _typeFilter = value);
|
||||||
|
_loadDevices();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: _isLoading
|
child: _isLoading
|
||||||
? const Center(child: CircularProgressIndicator())
|
? const Center(child: CircularProgressIndicator())
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:encrypter/encrypter/xor.dart';
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:frontend/utils/pref_utils.dart';
|
import 'package:frontend/utils/pref_utils.dart';
|
||||||
import '../model/device.dart';
|
import '../model/device.dart';
|
||||||
|
import '../model/device_type.dart';
|
||||||
import '../model/notification.dart';
|
import '../model/notification.dart';
|
||||||
|
|
||||||
class BackendAPI {
|
class BackendAPI {
|
||||||
@@ -90,11 +91,20 @@ class BackendAPI {
|
|||||||
return Device.fromJson(response.data as Map<String, dynamic>);
|
return Device.fromJson(response.data as Map<String, dynamic>);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Device>> listDevices({bool? isRegistered}) async {
|
Future<List<Device>> listDevices({
|
||||||
|
bool? isRegistered,
|
||||||
|
String? owner,
|
||||||
|
DeviceType? deviceType,
|
||||||
|
}) async {
|
||||||
debugPrint('About to call /devices');
|
debugPrint('About to call /devices');
|
||||||
|
|
||||||
final params = <String, dynamic>{};
|
final params = <String, dynamic>{};
|
||||||
if (isRegistered != null) params['is_registered'] = isRegistered;
|
if (isRegistered != null) params['is_registered'] = isRegistered;
|
||||||
|
if (owner != null && owner.isNotEmpty) params['owner'] = owner;
|
||||||
|
if (deviceType != null) {
|
||||||
|
params['device_type'] =
|
||||||
|
deviceType == DeviceType.unknown ? '' : deviceType.name;
|
||||||
|
}
|
||||||
|
|
||||||
final response = await _dio.get('/devices', queryParameters: params);
|
final response = await _dio.get('/devices', queryParameters: params);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user