Added logs screen and list

This commit is contained in:
Juan Gilsanz Polo 2022-09-30 23:33:57 +02:00
parent e657b2e626
commit 1235d6c75e
10 changed files with 666 additions and 2 deletions

View file

@ -4,7 +4,9 @@ import 'package:adguard_home_manager/screens/connect/fab.dart';
import 'package:adguard_home_manager/screens/home/appbar.dart'; import 'package:adguard_home_manager/screens/home/appbar.dart';
import 'package:adguard_home_manager/screens/connect/appbar.dart'; import 'package:adguard_home_manager/screens/connect/appbar.dart';
import 'package:adguard_home_manager/screens/clients/fab.dart'; import 'package:adguard_home_manager/screens/clients/fab.dart';
import 'package:adguard_home_manager/screens/logs/logs.dart';
import 'package:adguard_home_manager/screens/connect/connect.dart'; import 'package:adguard_home_manager/screens/connect/connect.dart';
import 'package:adguard_home_manager/screens/logs/appbar.dart';
import 'package:adguard_home_manager/screens/home/home.dart'; import 'package:adguard_home_manager/screens/home/home.dart';
import 'package:adguard_home_manager/screens/clients/clients.dart'; import 'package:adguard_home_manager/screens/clients/clients.dart';
import 'package:adguard_home_manager/screens/settings/appbar.dart'; import 'package:adguard_home_manager/screens/settings/appbar.dart';
@ -43,6 +45,12 @@ List<AppScreen> screensServerConnected = [
body: Clients(), body: Clients(),
fab: ClientsFab() fab: ClientsFab()
), ),
const AppScreen(
name: "logs",
icon: Icons.list_alt_rounded,
body: Logs(),
appBar: LogsAppBar()
),
const AppScreen( const AppScreen(
name: "settings", name: "settings",
icon: Icons.settings_rounded, icon: Icons.settings_rounded,

View file

@ -0,0 +1,12 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
String formatTimestamp(DateTime timestamp, String format) {
DateFormat f = DateFormat(format);
return f.format(timestamp);
}
String formatTimeOfDay(TimeOfDay timestamp, String format) {
DateFormat f = DateFormat(format);
return f.format(DateTime(0, 0, 0, timestamp.hour, timestamp.minute));
}

View file

@ -106,5 +106,10 @@
"advancedSetupDescription": "Advanced options", "advancedSetupDescription": "Advanced options",
"settingsUpdatedSuccessfully": "Settings updated successfully.", "settingsUpdatedSuccessfully": "Settings updated successfully.",
"cannotUpdateSettings": "Settings cannot be updated.", "cannotUpdateSettings": "Settings cannot be updated.",
"restartAppTakeEffect": "Restart the application" "restartAppTakeEffect": "Restart the application",
"loadingLogs": "Loading logs...",
"logsNotLoaded": "Logs list could not be loaded",
"processed": "Processed (no list)",
"blockedBlacklist": "Blocked (blacklist)",
"processedWhitelist": "Processed (whitelist)"
} }

View file

@ -106,5 +106,10 @@
"advancedSetupDescription": "Opciones avanzadas", "advancedSetupDescription": "Opciones avanzadas",
"settingsUpdatedSuccessfully": "Ajustes actualizados correctamente.", "settingsUpdatedSuccessfully": "Ajustes actualizados correctamente.",
"cannotUpdateSettings": "No se han podido actualizar los ajustes.", "cannotUpdateSettings": "No se han podido actualizar los ajustes.",
"restartAppTakeEffect": "Reinicia la aplicación para que se apliquen los cambios." "restartAppTakeEffect": "Reinicia la aplicación para que se apliquen los cambios.",
"loadingLogs": "Cargando registros...",
"logsNotLoaded": "No se pudieron cargar los registros",
"processed": "Procesada (sin lista)",
"blockedBlacklist": "Bloqueada (lista negra)",
"processedWhitelist": "Procesada (lista blanca)"
} }

216
lib/models/logs.dart Normal file
View file

@ -0,0 +1,216 @@
import 'dart:convert';
class LogsList {
int loadStatus = 0;
LogsData? logsData;
LogsList({
required this.loadStatus,
this.logsData
});
}
LogsData logsFromJson(String str) => LogsData.fromJson(json.decode(str));
String logsToJson(LogsData data) => json.encode(data.toJson());
class LogsData {
List<Log> data;
final DateTime oldest;
LogsData({
required this.data,
required this.oldest,
});
factory LogsData.fromJson(Map<String, dynamic> json) => LogsData(
data: List<Log>.from(json["data"].map((x) => Log.fromJson(x))),
oldest: DateTime.parse(json["oldest"]),
);
Map<String, dynamic> toJson() => {
"data": List<dynamic>.from(data.map((x) => x.toJson())),
"oldest": oldest.toIso8601String(),
};
}
class Log {
final bool answerDnssec;
final bool cached;
final String client;
final ClientInfo clientInfo;
final String clientProto;
final String elapsedMs;
final Question question;
final String reason;
final List<Rule> rules;
final String status;
final DateTime time;
final String upstream;
final List<Answer>? answer;
final int? filterId;
final String? rule;
final List<Answer>? originalAnswer;
Log({
required this.answerDnssec,
required this.cached,
required this.client,
required this.clientInfo,
required this.clientProto,
required this.elapsedMs,
required this.question,
required this.reason,
required this.rules,
required this.status,
required this.time,
required this.upstream,
this.answer,
this.filterId,
this.rule,
this.originalAnswer,
});
factory Log.fromJson(Map<String, dynamic> json) => Log(
answerDnssec: json["answer_dnssec"],
cached: json["cached"],
client: json["client"],
clientInfo: ClientInfo.fromJson(json["client_info"]),
clientProto: json["client_proto"],
elapsedMs: json["elapsedMs"],
question: Question.fromJson(json["question"]),
reason: json["reason"],
rules: List<Rule>.from(json["rules"].map((x) => Rule.fromJson(x))),
status: json["status"],
time: DateTime.parse(json["time"]),
upstream: json["upstream"],
answer: json["answer"] == null ? null : List<Answer>.from(json["answer"].map((x) => Answer.fromJson(x))),
filterId: json["filterId"],
rule: json["rule"],
originalAnswer: json["original_answer"] == null ? null : List<Answer>.from(json["original_answer"].map((x) => Answer.fromJson(x))),
);
Map<String, dynamic> toJson() => {
"answer_dnssec": answerDnssec,
"cached": cached,
"client": client,
"client_info": clientInfo.toJson(),
"client_proto": clientProto,
"elapsedMs": elapsedMs,
"question": question.toJson(),
"reason":reason,
"rules": List<dynamic>.from(rules.map((x) => x.toJson())),
"status": status,
"time": time.toIso8601String(),
"upstream": upstream,
"answer": answer == null ? null : List<dynamic>.from(answer!.map((x) => x.toJson())),
"filterId": filterId,
"rule": rule,
"original_answer": originalAnswer == null ? null : List<dynamic>.from(originalAnswer!.map((x) => x.toJson())),
};
}
class Answer {
final String type;
final String value;
final int ttl;
Answer({
required this.type,
required this.value,
required this.ttl,
});
factory Answer.fromJson(Map<String, dynamic> json) => Answer(
type: json["type"],
value: json["value"],
ttl: json["ttl"],
);
Map<String, dynamic> toJson() => {
"type": type,
"value": value,
"ttl": ttl,
};
}
class ClientInfo {
final Whois whois;
final String name;
final String disallowedRule;
final bool disallowed;
ClientInfo({
required this.whois,
required this.name,
required this.disallowedRule,
required this.disallowed,
});
factory ClientInfo.fromJson(Map<String, dynamic> json) => ClientInfo(
whois: Whois.fromJson(json["whois"]),
name: json["name"],
disallowedRule: json["disallowed_rule"],
disallowed: json["disallowed"],
);
Map<String, dynamic> toJson() => {
"whois": whois.toJson(),
"name": name,
"disallowed_rule": disallowedRule,
"disallowed": disallowed,
};
}
class Whois {
Whois();
factory Whois.fromJson(Map<String, dynamic> json) => Whois();
Map<String, dynamic> toJson() => {};
}
class Question {
final String questionClass;
final String name;
final String type;
Question({
required this.questionClass,
required this.name,
required this.type,
});
factory Question.fromJson(Map<String, dynamic> json) => Question(
questionClass: json["class"],
name: json["name"],
type: json["type"],
);
Map<String, dynamic> toJson() => {
"class": questionClass,
"name": name,
"type": type,
};
}
class Rule {
final int filterListId;
final String text;
Rule({
required this.filterListId,
required this.text,
});
factory Rule.fromJson(Map<String, dynamic> json) => Rule(
filterListId: json["filter_list_id"],
text: json["text"],
);
Map<String, dynamic> toJson() => {
"filter_list_id": filterListId,
"text": text,
};
}

View file

@ -0,0 +1,17 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class LogsAppBar extends StatelessWidget with PreferredSizeWidget {
const LogsAppBar({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return AppBar(
title: Text(AppLocalizations.of(context)!.logs),
centerTitle: true,
);
}
@override
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
}

View file

@ -0,0 +1,139 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/models/logs.dart';
import 'package:adguard_home_manager/functions/format_time.dart';
class LogTile extends StatelessWidget {
final Log log;
final int length;
final int index;
const LogTile({
Key? key,
required this.log,
required this.length,
required this.index
}) : super(key: key);
@override
Widget build(BuildContext context) {
final width = MediaQuery.of(context).size.width;
Widget logStatusWidget({
required IconData icon,
required Color color,
required String text
}) {
return Row(
children: [
Icon(
icon,
color: color,
size: 14,
),
const SizedBox(width: 10),
Text(
text,
style: TextStyle(
color: color,
fontWeight: FontWeight.bold,
fontSize: 12
),
)
]
);
}
Widget generateLogStatus() {
switch (log.reason) {
case "NotFilteredNotFound":
return logStatusWidget(
icon: Icons.verified_user_rounded,
color: Colors.green,
text: AppLocalizations.of(context)!.processed,
);
case "FilteredBlackList":
return logStatusWidget(
icon: Icons.verified_user_rounded,
color: Colors.red,
text:AppLocalizations.of(context)!.blockedBlacklist,
);
case "NotFilteredWhiteList":
return logStatusWidget(
icon: Icons.verified_user_rounded,
color: Colors.green,
text: AppLocalizations.of(context)!.processedWhitelist,
);
default:
return logStatusWidget(
icon: Icons.shield_rounded,
color: Colors.grey,
text: "Unknwon"
);
}
}
return Material(
color: Colors.transparent,
child: InkWell(
onTap: () => {},
child: Container(
width: double.maxFinite,
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
border: index < length
? Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor
)
)
: null
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
generateLogStatus(),
const SizedBox(height: 10),
SizedBox(
width: width-100,
child: Text(
log.question.name,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontWeight: FontWeight.w500,
fontSize: 16
),
),
),
const SizedBox(height: 10),
SizedBox(
width: width-100,
child: Text(
log.client,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Colors.grey,
fontSize: 13
),
),
)
],
),
Text(
formatTimestamp(log.time, 'HH:mm:ss')
),
],
),
),
),
);
}
}

189
lib/screens/logs/logs.dart Normal file
View file

@ -0,0 +1,189 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:adguard_home_manager/screens/logs/log_tile.dart';
import 'package:adguard_home_manager/models/app_log.dart';
import 'package:adguard_home_manager/providers/app_config_provider.dart';
import 'package:adguard_home_manager/services/http_requests.dart';
import 'package:adguard_home_manager/models/logs.dart';
import 'package:adguard_home_manager/models/server.dart';
import 'package:adguard_home_manager/providers/servers_provider.dart';
class Logs extends StatelessWidget {
const Logs({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final serversProvider = Provider.of<ServersProvider>(context);
final appConfigProvider = Provider.of<AppConfigProvider>(context);
return LogsWidget(
server: serversProvider.selectedServer!,
createLog: appConfigProvider.addLog
);
}
}
class LogsWidget extends StatefulWidget {
final Server server;
final void Function(AppLog) createLog;
const LogsWidget({
Key? key,
required this.server,
required this.createLog
}) : super(key: key);
@override
State<LogsWidget> createState() => _LogsWidgetState();
}
class _LogsWidgetState extends State<LogsWidget> {
LogsList logsList = LogsList(
loadStatus: 0,
logsData: null
);
int itemsPerLoad = 100;
int offset = 0;
late ScrollController scrollController;
bool isLoadingMore = false;
Future fetchLogs({
int? inOffset,
bool? loadingMore
}) async {
int offst = inOffset ?? offset;
if (loadingMore != null && loadingMore == true) {
setState(() => isLoadingMore = true);
}
final result = await getLogs(server: widget.server, count: itemsPerLoad, offset: offst);
if (loadingMore != null && loadingMore == true) {
setState(() => isLoadingMore = false);
}
if (result['result'] == 'success') {
setState(() {
offset = inOffset != null ? inOffset+itemsPerLoad : offset+itemsPerLoad;
if (loadingMore != null && loadingMore == true) {
logsList.logsData!.data = [...logsList.logsData!.data, ...result['data'].data];
}
else {
logsList.logsData = result['data'];
}
logsList.loadStatus = 1;
});
}
else {
setState(() {
logsList.loadStatus = 2;
});
widget.createLog(result['log']);
}
}
void scrollListener() {
if (scrollController.position.extentAfter < 500 && isLoadingMore == false) {
fetchLogs(loadingMore: true);
}
}
@override
void initState() {
scrollController = ScrollController()..addListener(scrollListener);
fetchLogs(inOffset: 0);
super.initState();
}
@override
Widget build(BuildContext context) {
switch (logsList.loadStatus) {
case 0:
return SizedBox(
width: double.maxFinite,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 30),
Text(
AppLocalizations.of(context)!.loadingLogs,
style: const TextStyle(
fontSize: 22,
color: Colors.grey,
fontWeight: FontWeight.w500
),
)
],
),
);
case 1:
return RefreshIndicator(
onRefresh: () async {
await fetchLogs(inOffset: 0);
},
child: ListView.builder(
controller: scrollController,
padding: const EdgeInsets.only(top: 0),
itemCount: isLoadingMore == true
? logsList.logsData!.data.length+1
: logsList.logsData!.data.length,
itemBuilder: (context, index) {
if (isLoadingMore == true && index == logsList.logsData!.data.length) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 20),
child: Center(
child: CircularProgressIndicator(),
),
);
}
else {
return LogTile(
log: logsList.logsData!.data[index],
index: index,
length: logsList.logsData!.data.length,
);
}
}
),
);
case 2:
return SizedBox(
width: double.maxFinite,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(
Icons.error,
color: Colors.red,
size: 50,
),
const SizedBox(height: 30),
Text(
AppLocalizations.of(context)!.logsNotLoaded,
style: const TextStyle(
fontSize: 22,
color: Colors.grey,
fontWeight: FontWeight.w500
),
)
],
),
);
default:
return const SizedBox();
}
}
}

View file

@ -5,6 +5,7 @@ import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:adguard_home_manager/models/logs.dart';
import 'package:adguard_home_manager/models/app_log.dart'; import 'package:adguard_home_manager/models/app_log.dart';
import 'package:adguard_home_manager/models/server_status.dart'; import 'package:adguard_home_manager/models/server_status.dart';
import 'package:adguard_home_manager/models/clients.dart'; import 'package:adguard_home_manager/models/clients.dart';
@ -355,4 +356,73 @@ Future requestAllowedBlockedClientsHosts(Server server, Map<String, List<String>
} catch (e) { } catch (e) {
return {'result': 'error'}; return {'result': 'error'};
} }
}
Future getLogs({
required Server server,
required int count,
int? offset
}) async {
final result = await getRequest(
urlPath: '/querylog?limit=$count${offset != null ? '&offset=$offset' : ''}',
server: server
);
if (result.statusCode == 200) {
return {
'result': 'success',
'data': LogsData.fromJson(jsonDecode(result.body))
};
}
else {
return {
'result': 'error',
'log': AppLog(
type: 'logs',
dateTime: DateTime.now(),
message: 'error_code_not_expected',
statusCode: result.statusCode,
resBody: result.body
)
};
}
try {
} on SocketException {
return {
'result': 'no_connection',
'log': AppLog(
type: 'logs',
dateTime: DateTime.now(),
message: 'SocketException'
)
};
} on TimeoutException {
return {
'result': 'no_connection',
'log': AppLog(
type: 'logs',
dateTime: DateTime.now(),
message: 'TimeoutException'
)
};
} on HandshakeException {
return {
'result': 'ssl_error',
'message': 'HandshakeException',
'log': AppLog(
type: 'logs',
dateTime: DateTime.now(),
message: 'TimeoutException'
)
};
} catch (e) {
return {
'result': 'error',
'log': AppLog(
type: 'logs',
dateTime: DateTime.now(),
message: e.toString()
)
};
}
} }

View file

@ -35,6 +35,9 @@ class BottomNavBar extends StatelessWidget {
case 'clients': case 'clients':
return AppLocalizations.of(context)!.clients; return AppLocalizations.of(context)!.clients;
case 'logs':
return AppLocalizations.of(context)!.logs;
default: default:
return ''; return '';
} }