Add search to HTTP logs (#2852)

This commit is contained in:
r3econ
2026-03-30 11:10:14 +02:00
committed by GitHub
parent c96f947f0b
commit 634e94e69e
3 changed files with 64 additions and 14 deletions
+19 -7
View File
@@ -9,22 +9,30 @@ part 'http_log_paginator.freezed.dart';
const _pageSize = 20;
/// A provider for [HttpLogPaginator].
final httpLogPaginatorProvider = AsyncNotifierProvider.autoDispose<HttpLogPaginator, HttpLogState>(
HttpLogPaginator.new,
name: 'HttpLogPaginatorProvider',
);
///
/// The family argument is the optional search query string.
final httpLogPaginatorProvider = AsyncNotifierProvider.autoDispose
.family<HttpLogPaginator, HttpLogState, String?>(
HttpLogPaginator.new,
name: 'HttpLogPaginatorProvider',
);
/// A Riverpod controller for managing HTTP logs.
///
/// The `HttpLogController` class is responsible for fetching and managing
/// paginated HTTP log entries from the storage. It uses a throttler to limit
/// the rate of fetching new pages.
class HttpLogPaginator extends AsyncNotifier<HttpLogState> {
HttpLogPaginator(this._searchQuery);
final String? _searchQuery;
@override
Future<HttpLogState> build() async {
final storage = await ref.read(httpLogStorageProvider.future);
return HttpLogState(
data: IList.new([await AsyncValue.guard(() => storage.page(limit: _pageSize))]),
data: IList.new([
await AsyncValue.guard(() => storage.page(limit: _pageSize, searchQuery: _searchQuery)),
]),
);
}
@@ -36,7 +44,11 @@ class HttpLogPaginator extends AsyncNotifier<HttpLogState> {
if (state.hasValue && state.requireValue.hasMore) {
final storage = await ref.read(httpLogStorageProvider.future);
final asyncPage = await AsyncValue.guard(
() => storage.page(limit: _pageSize, cursor: state.requireValue.nextPage),
() => storage.page(
limit: _pageSize,
cursor: state.requireValue.nextPage,
searchQuery: _searchQuery,
),
);
state = AsyncValue.data(
state.requireValue.copyWith(data: state.requireValue.data.add(asyncPage)),
+18 -2
View File
@@ -22,12 +22,28 @@ class HttpLogStorage {
final Database _db;
/// Retrieves a paginated list of [HttpLogEntry] entries from the database.
Future<HttpLog> page({int? cursor, int limit = 100}) async {
///
/// [searchQuery] filters entries whose request method, URL, or error message contain the query.
Future<HttpLog> page({int? cursor, String? searchQuery, int limit = 100}) async {
final whereParts = <String>[];
final args = <dynamic>[];
if (cursor != null) {
whereParts.add('id <= ?');
args.add(cursor);
}
if (searchQuery != null && searchQuery.isNotEmpty) {
whereParts.add('(requestMethod LIKE ? OR requestUrl LIKE ? OR errorMessage LIKE ?)');
final pattern = '%$searchQuery%';
args.addAll([pattern, pattern, pattern]);
}
final res = await _db.query(
kHttpLogStorageTable,
limit: limit + 1,
orderBy: 'id DESC',
where: cursor != null ? 'id <= $cursor' : null,
where: whereParts.isNotEmpty ? whereParts.join(' AND ') : null,
whereArgs: args.isNotEmpty ? args : null,
);
return HttpLog(
items: res.take(limit).map(HttpLogEntry.fromJson).toIList(),
+27 -5
View File
@@ -8,6 +8,7 @@ import 'package:lichess_mobile/src/styles/styles.dart';
import 'package:lichess_mobile/src/utils/navigation.dart';
import 'package:lichess_mobile/src/widgets/adaptive_action_sheet.dart';
import 'package:lichess_mobile/src/widgets/haptic_refresh_indicator.dart';
import 'package:lichess_mobile/src/widgets/platform_search_bar.dart';
class HttpLogScreen extends ConsumerStatefulWidget {
const HttpLogScreen({super.key});
@@ -23,6 +24,8 @@ class HttpLogScreen extends ConsumerStatefulWidget {
class _HttpLogScreenState extends ConsumerState<HttpLogScreen> {
final ScrollController _scrollController = ScrollController();
final GlobalKey<RefreshIndicatorState> _refreshIndicatorKey = GlobalKey<RefreshIndicatorState>();
final TextEditingController _searchController = TextEditingController();
String? _searchQuery;
@override
void initState() {
@@ -34,26 +37,27 @@ class _HttpLogScreenState extends ConsumerState<HttpLogScreen> {
void dispose() {
_scrollController.removeListener(_scrollListener);
_scrollController.dispose();
_searchController.dispose();
super.dispose();
}
void _scrollListener() {
if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 300) {
final currentState = ref.read(httpLogPaginatorProvider);
final currentState = ref.read(httpLogPaginatorProvider(_searchQuery));
if (currentState.hasValue && !currentState.isLoading && currentState.requireValue.hasMore) {
ref.read(httpLogPaginatorProvider.notifier).next();
ref.read(httpLogPaginatorProvider(_searchQuery).notifier).next();
}
}
}
Future<void> _onRefresh() async {
await Future<void>.delayed(const Duration(milliseconds: 300));
return ref.read(httpLogPaginatorProvider.notifier).refresh();
return ref.read(httpLogPaginatorProvider(_searchQuery).notifier).refresh();
}
@override
Widget build(BuildContext context) {
final asyncState = ref.watch(httpLogPaginatorProvider);
final asyncState = ref.watch(httpLogPaginatorProvider(_searchQuery));
return Scaffold(
appBar: AppBar(
title: const Text('HTTP logs'),
@@ -68,11 +72,29 @@ class _HttpLogScreenState extends ConsumerState<HttpLogScreen> {
context,
// TODO localize
title: const Text('Delete all logs'),
onConfirm: () => ref.read(httpLogPaginatorProvider.notifier).deleteAll(),
onConfirm: () =>
ref.read(httpLogPaginatorProvider(_searchQuery).notifier).deleteAll(),
);
},
),
],
bottom: PreferredSize(
preferredSize: const Size.fromHeight(60.0),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
child: PlatformSearchBar(
controller: _searchController,
hintText: 'Search logs...',
onChanged: (value) => setState(() {
_searchQuery = value.isEmpty ? null : value;
}),
onClear: () => setState(() {
_searchQuery = null;
_searchController.clear();
}),
),
),
),
),
body: _HttpLogList(
scrollController: _scrollController,