mirror of
https://github.com/lifegpc/eh_downloader_flutter.git
synced 2026-06-06 05:49:03 +08:00
feat: Add server logs page with advanced datatable and log level parsing
This commit is contained in:
@@ -15,6 +15,33 @@ enum LogLevel {
|
||||
warn,
|
||||
@JsonValue(6)
|
||||
error;
|
||||
|
||||
static LogLevel? tryParse(String level) {
|
||||
int? levnum = int.tryParse(level);
|
||||
if (levnum != null && levnum <= 6 && levnum >= 1) {
|
||||
return LogLevel.values[levnum - 1];
|
||||
}
|
||||
switch (level) {
|
||||
case 'trace':
|
||||
return LogLevel.trace;
|
||||
case 'debug':
|
||||
return LogLevel.debug;
|
||||
case 'log':
|
||||
return LogLevel.log;
|
||||
case 'info':
|
||||
return LogLevel.info;
|
||||
case 'warn':
|
||||
return LogLevel.warn;
|
||||
case 'error':
|
||||
return LogLevel.error;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
int toInt() {
|
||||
return index + 1;
|
||||
}
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
|
||||
@@ -41,7 +41,8 @@ class AuthInfo {
|
||||
: _user?.permissions.has(UserPermission.manageTasks);
|
||||
bool? get canShareGallery =>
|
||||
_user?.permissions.has(UserPermission.shareGallery);
|
||||
bool? get canQueryLog => _user?.permissions.has(UserPermission.queryLog);
|
||||
bool? get canQueryLog =>
|
||||
noUser == true ? true : _user?.permissions.has(UserPermission.queryLog);
|
||||
MeilisearchInfo? get meilisearch => _status?.meilisearch;
|
||||
MeiliSearchClient? _meiliSearchClient;
|
||||
MeiliSearchClient? get meiliSearchClient => _meiliSearchClient;
|
||||
|
||||
@@ -359,5 +359,10 @@
|
||||
"deleteShareConfirm": "Do you want to delete shared token?",
|
||||
"failedDeleteShare": "Failed to delete shared token: ",
|
||||
"queryLog": "Query log",
|
||||
"loggingStack": "Enable logging stack for all log levels"
|
||||
"loggingStack": "Enable logging stack for all log levels",
|
||||
"serverLogs": "Server logs",
|
||||
"time": "Time",
|
||||
"message": "Message",
|
||||
"type": "Type",
|
||||
"level": "Level"
|
||||
}
|
||||
|
||||
@@ -359,5 +359,10 @@
|
||||
"deleteShareConfirm": "是否删除分享令牌?",
|
||||
"failedDeleteShare": "删除分享令牌失败:",
|
||||
"queryLog": "查询日志",
|
||||
"loggingStack": "为所有日志级别启用堆栈记录"
|
||||
"loggingStack": "为所有日志级别启用堆栈记录",
|
||||
"serverLogs": "服务器日志",
|
||||
"time": "时间",
|
||||
"message": "消息",
|
||||
"type": "类型",
|
||||
"level": "级别"
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:go_router/go_router.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
import 'api/client.dart';
|
||||
import 'api/log.dart';
|
||||
import 'dialog/dialog_page.dart';
|
||||
import 'dialog/download_zip_page.dart';
|
||||
import 'dialog/edit_user_page.dart';
|
||||
@@ -25,6 +26,7 @@ import 'pages/galleries.dart';
|
||||
import 'pages/gallery.dart';
|
||||
import 'pages/home.dart';
|
||||
import 'pages/login.dart';
|
||||
import 'pages/logs.dart';
|
||||
import 'pages/sessions.dart';
|
||||
import 'pages/settings.dart';
|
||||
import 'pages/settings/cache.dart';
|
||||
@@ -361,6 +363,43 @@ final _router = GoRouter(
|
||||
path: SessionsPage.routeName,
|
||||
builder: (context, state) => SessionsPage(key: state.pageKey),
|
||||
),
|
||||
GoRoute(
|
||||
name: LogsPage.routeName,
|
||||
path: LogsPage.routeName,
|
||||
builder: (context, state) {
|
||||
int? page;
|
||||
String? type;
|
||||
LogLevel? minLevel;
|
||||
List<LogLevel>? allowedLevel;
|
||||
int? size;
|
||||
if (state.uri.queryParameters.containsKey("page")) {
|
||||
page = int.tryParse(state.uri.queryParameters["page"]!);
|
||||
}
|
||||
if (state.uri.queryParameters.containsKey("type")) {
|
||||
type = state.uri.queryParameters["type"]!;
|
||||
}
|
||||
if (state.uri.queryParameters.containsKey("min_level")) {
|
||||
minLevel =
|
||||
LogLevel.tryParse(state.uri.queryParameters["min_level"]!);
|
||||
}
|
||||
if (state.uri.queryParameters.containsKey("allowed_level")) {
|
||||
allowedLevel = state.uri.queryParameters["allowed_level"]!
|
||||
.split(",")
|
||||
.map((e) => LogLevel.tryParse(e))
|
||||
.nonNulls
|
||||
.toList();
|
||||
}
|
||||
if (state.uri.queryParameters.containsKey("size")) {
|
||||
size = int.tryParse(state.uri.queryParameters["size"]!);
|
||||
}
|
||||
return LogsPage(
|
||||
key: state.pageKey,
|
||||
page: page,
|
||||
type: type,
|
||||
minLevel: minLevel,
|
||||
allowedLevel: allowedLevel,
|
||||
size: size);
|
||||
}),
|
||||
],
|
||||
observers: [
|
||||
_NavigatorObserver(),
|
||||
|
||||
@@ -51,6 +51,15 @@ class HomeDrawer extends StatelessWidget {
|
||||
context.push("/users");
|
||||
})
|
||||
: Container(),
|
||||
auth.canQueryLog == true
|
||||
? ListTile(
|
||||
leading: const Icon(Icons.list),
|
||||
title: Text(i18n.serverLogs),
|
||||
onTap: () {
|
||||
Scaffold.of(context).closeDrawer();
|
||||
context.push("/logs");
|
||||
})
|
||||
: Container(),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.settings),
|
||||
title: Text(AppLocalizations.of(context)!.settings),
|
||||
|
||||
227
lib/pages/logs.dart
Normal file
227
lib/pages/logs.dart
Normal file
@@ -0,0 +1,227 @@
|
||||
import 'package:advanced_datatable/advanced_datatable_source.dart';
|
||||
import 'package:advanced_datatable/datatable.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../api/log.dart';
|
||||
import '../globals.dart';
|
||||
import '../main.dart';
|
||||
|
||||
final _log = Logger('LogsPage');
|
||||
|
||||
class LogsPage extends StatefulWidget {
|
||||
const LogsPage(
|
||||
{super.key,
|
||||
this.page,
|
||||
this.type,
|
||||
this.minLevel,
|
||||
this.allowedLevel,
|
||||
this.size});
|
||||
|
||||
static const String routeName = '/logs';
|
||||
|
||||
final int? page;
|
||||
final String? type;
|
||||
final LogLevel? minLevel;
|
||||
final List<LogLevel>? allowedLevel;
|
||||
final int? size;
|
||||
|
||||
@override
|
||||
State<LogsPage> createState() => _LogsPage();
|
||||
}
|
||||
|
||||
class _LogDataSource extends AdvancedDataTableSource<LogEntry> {
|
||||
_LogDataSource(this.logs,
|
||||
{this.type,
|
||||
this.minLevel,
|
||||
this.allowedLevel,
|
||||
this.size = 10,
|
||||
this.count,
|
||||
this.page = 1,
|
||||
this.locale,
|
||||
this.start = 0});
|
||||
final String? type;
|
||||
final LogLevel? minLevel;
|
||||
final List<LogLevel>? allowedLevel;
|
||||
final int size;
|
||||
final String? locale;
|
||||
int? count;
|
||||
int start;
|
||||
int page;
|
||||
List<LogEntry> logs;
|
||||
@override
|
||||
bool get isRowCountApproximate => count == null;
|
||||
@override
|
||||
int get rowCount => count ?? 10;
|
||||
@override
|
||||
int get selectedRowCount => 0;
|
||||
@override
|
||||
Future<RemoteDataSourceDetails<LogEntry>> getNextPage(
|
||||
NextPageRequest pageRequest) async {
|
||||
int npage = pageRequest.offset ~/ pageRequest.pageSize + 1;
|
||||
page = npage;
|
||||
if ((logs.length >= pageRequest.offset + pageRequest.pageSize - start &&
|
||||
pageRequest.offset >= start) ||
|
||||
logs.length == count) {
|
||||
return RemoteDataSourceDetails(count ?? logs.length, logs);
|
||||
}
|
||||
var data = (await api.queryLog(
|
||||
page: page,
|
||||
type: type,
|
||||
minLevel: minLevel?.toInt(),
|
||||
allowedLevel: allowedLevel?.map((e) => e.toInt()).join(","),
|
||||
limit: size))
|
||||
.unwrap();
|
||||
count = data.count;
|
||||
if (pageRequest.offset < start) {
|
||||
logs.insertAll(0, data.datas);
|
||||
start -= data.datas.length;
|
||||
} else {
|
||||
logs.addAll(data.datas);
|
||||
}
|
||||
return RemoteDataSourceDetails(count ?? logs.length, logs);
|
||||
}
|
||||
|
||||
@override
|
||||
DataRow? getRow(int index) {
|
||||
index += (page - 1) * size;
|
||||
index -= start;
|
||||
var log = logs.elementAtOrNull(index);
|
||||
if (log == null) return null;
|
||||
return DataRow(cells: [
|
||||
DataCell(
|
||||
Text(DateFormat.yMd(locale).add_jms().format(log.time.toLocal()))),
|
||||
DataCell(Text(log.message)),
|
||||
DataCell(Text(log.level.name)),
|
||||
DataCell(Text(log.type)),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
class _LogsPage extends State<LogsPage> with ThemeModeWidget, IsTopWidget2 {
|
||||
int? _page;
|
||||
String? _type;
|
||||
LogLevel? _minLevel;
|
||||
List<LogLevel>? _allowedLevel;
|
||||
int _size = 50;
|
||||
bool _pageMode = false;
|
||||
_LogDataSource? _dataSource;
|
||||
CancelToken? _cancel;
|
||||
bool _isLoading = false;
|
||||
LogEntries? _firstPage;
|
||||
|
||||
Future<void> _fetchFirstPage() async {
|
||||
try {
|
||||
_cancel = CancelToken();
|
||||
_isLoading = true;
|
||||
_firstPage = (await api.queryLog(
|
||||
page: _page ?? 1,
|
||||
type: _type,
|
||||
minLevel: _minLevel?.toInt(),
|
||||
allowedLevel: _allowedLevel?.map((e) => e.toInt()).join(","),
|
||||
limit: _size,
|
||||
cancel: _cancel))
|
||||
.unwrap();
|
||||
if (!_cancel!.isCancelled) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (!_cancel!.isCancelled) {
|
||||
_log.severe("Failed to load first page:", e);
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_page = widget.page;
|
||||
_type = widget.type;
|
||||
_minLevel = widget.minLevel ?? LogLevel.log;
|
||||
_allowedLevel = widget.allowedLevel;
|
||||
_size = widget.size ?? 10;
|
||||
_pageMode =
|
||||
_page != null ? true : (prefs.getBool("serverLogsPageMode") ?? true);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_cancel?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
tryInitApi(context);
|
||||
final i18n = AppLocalizations.of(context)!;
|
||||
if (isTop(context)) {
|
||||
setCurrentTitle(i18n.serverLogs);
|
||||
}
|
||||
final locale = MainApp.of(context).lang.toLocale().toString();
|
||||
bool isLoading = false;
|
||||
if (_pageMode) {
|
||||
isLoading = _firstPage == null;
|
||||
if (isLoading && !_isLoading) _fetchFirstPage();
|
||||
if (_dataSource == null && _firstPage != null) {
|
||||
_dataSource = _LogDataSource(_firstPage!.datas,
|
||||
page: _page ?? 1,
|
||||
type: _type,
|
||||
minLevel: _minLevel,
|
||||
allowedLevel: _allowedLevel,
|
||||
size: _size,
|
||||
count: _firstPage!.count,
|
||||
locale: locale,
|
||||
start: (_page ?? 1 - 1) * _size);
|
||||
}
|
||||
}
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () {
|
||||
context.canPop() ? context.pop() : context.go("/");
|
||||
},
|
||||
),
|
||||
title: Text(i18n.serverLogs),
|
||||
actions: [
|
||||
buildThemeModeIcon(context),
|
||||
buildMoreVertSettingsButon(context),
|
||||
],
|
||||
),
|
||||
body: _pageMode
|
||||
? _dataSource != null
|
||||
? AdvancedPaginatedDataTable(
|
||||
columns: <DataColumn>[
|
||||
DataColumn(label: Text(i18n.time)),
|
||||
DataColumn(label: Text(i18n.message)),
|
||||
DataColumn(label: Text(i18n.level)),
|
||||
DataColumn(label: Text(i18n.type)),
|
||||
],
|
||||
source: _dataSource!,
|
||||
rowsPerPage: _size,
|
||||
onPageChanged: (page) {
|
||||
_page = page ~/ _size + 1;
|
||||
var params = {
|
||||
"page": _page?.toString(),
|
||||
"type": _type,
|
||||
"min_level": _minLevel?.name,
|
||||
"allowed_level":
|
||||
_allowedLevel?.map((e) => e.name).join(","),
|
||||
"size": _size.toString(),
|
||||
};
|
||||
params.removeWhere(
|
||||
(key, value) => value == null || value!.isEmpty);
|
||||
context.replaceNamed("/logs", queryParameters: params);
|
||||
})
|
||||
: const Center(child: CircularProgressIndicator())
|
||||
: Container());
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,14 @@ packages:
|
||||
description: dart
|
||||
source: sdk
|
||||
version: "0.3.3"
|
||||
advanced_datatable:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: advanced_datatable
|
||||
sha256: "3537a9a811769c121e9c9d49006357d9e99af4bac030ce059825f5c904a0fb47"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.0.9"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -7,6 +7,7 @@ environment:
|
||||
sdk: '>=3.0.5 <4.0.0'
|
||||
|
||||
dependencies:
|
||||
advanced_datatable: ^0.0.9
|
||||
cryptography: ^2.5.0
|
||||
cryptography_flutter: ^2.3.0
|
||||
dio: ^5.3.2
|
||||
|
||||
Reference in New Issue
Block a user