mirror of
https://git.selfprivacy.org/kherel/selfprivacy.org.app.git
synced 2024-11-19 07:09:14 +00:00
feat: Server logs screen
This commit is contained in:
parent
39babdeeaa
commit
515b0e2c67
|
@ -170,6 +170,18 @@
|
|||
"few": "{} cores",
|
||||
"many": "{} cores",
|
||||
"other": "{} cores"
|
||||
},
|
||||
"logs": "Server logs",
|
||||
"logs_empty": "No logs yet",
|
||||
"filter_by_systemd_unit": "Filter by systemd unit",
|
||||
"all_units": "All units",
|
||||
"log_dialog": {
|
||||
"metadata": "Metadata",
|
||||
"cursor": "Cursor",
|
||||
"priority": "Priority",
|
||||
"systemd_unit": "Systemd unit",
|
||||
"systemd_slice": "Systemd slice",
|
||||
"message": "Message"
|
||||
}
|
||||
},
|
||||
"domain": {
|
||||
|
|
|
@ -5,6 +5,7 @@ import 'package:selfprivacy/logic/bloc/connection_status_bloc.dart';
|
|||
import 'package:selfprivacy/logic/bloc/devices/devices_bloc.dart';
|
||||
import 'package:selfprivacy/logic/bloc/recovery_key/recovery_key_bloc.dart';
|
||||
import 'package:selfprivacy/logic/bloc/server_jobs/server_jobs_bloc.dart';
|
||||
import 'package:selfprivacy/logic/bloc/server_logs/server_logs_bloc.dart';
|
||||
import 'package:selfprivacy/logic/bloc/services/services_bloc.dart';
|
||||
import 'package:selfprivacy/logic/bloc/users/users_bloc.dart';
|
||||
import 'package:selfprivacy/logic/bloc/volumes/volumes_bloc.dart';
|
||||
|
@ -36,6 +37,7 @@ class BlocAndProviderConfigState extends State<BlocAndProviderConfig> {
|
|||
late final ConnectionStatusBloc connectionStatusBloc;
|
||||
late final ServerDetailsCubit serverDetailsCubit;
|
||||
late final VolumesBloc volumesBloc;
|
||||
late final ServerLogsBloc serverLogsBloc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
@ -52,6 +54,7 @@ class BlocAndProviderConfigState extends State<BlocAndProviderConfig> {
|
|||
connectionStatusBloc = ConnectionStatusBloc();
|
||||
serverDetailsCubit = ServerDetailsCubit();
|
||||
volumesBloc = VolumesBloc();
|
||||
serverLogsBloc = ServerLogsBloc();
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -94,6 +97,9 @@ class BlocAndProviderConfigState extends State<BlocAndProviderConfig> {
|
|||
BlocProvider(
|
||||
create: (final _) => JobsCubit(),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (final _) => serverLogsBloc,
|
||||
),
|
||||
],
|
||||
child: widget.child,
|
||||
);
|
||||
|
|
28
lib/logic/api_maps/graphql_maps/schema/logs.graphql
Normal file
28
lib/logic/api_maps/graphql_maps/schema/logs.graphql
Normal file
|
@ -0,0 +1,28 @@
|
|||
fragment LogEntry on LogEntry {
|
||||
message
|
||||
timestamp
|
||||
priority
|
||||
systemdUnit
|
||||
systemdSlice
|
||||
cursor
|
||||
}
|
||||
|
||||
query Logs($limit: Int!, $upCursor: String, $downCursor: String) {
|
||||
logs {
|
||||
paginated(limit: $limit, upCursor: $upCursor, downCursor: $downCursor) {
|
||||
pageMeta {
|
||||
upCursor
|
||||
downCursor
|
||||
}
|
||||
entries {
|
||||
...LogEntry
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subscription LogEntries {
|
||||
logEntries {
|
||||
...LogEntry
|
||||
}
|
||||
}
|
1588
lib/logic/api_maps/graphql_maps/schema/logs.graphql.dart
Normal file
1588
lib/logic/api_maps/graphql_maps/schema/logs.graphql.dart
Normal file
File diff suppressed because it is too large
Load diff
53
lib/logic/api_maps/graphql_maps/server_api/logs_api.dart
Normal file
53
lib/logic/api_maps/graphql_maps/server_api/logs_api.dart
Normal file
|
@ -0,0 +1,53 @@
|
|||
part of 'server_api.dart';
|
||||
|
||||
mixin LogsApi on GraphQLApiMap {
|
||||
Future<(List<ServerLogEntry>, ServerLogsPageMeta)> getServerLogs({
|
||||
required final int limit,
|
||||
final String? upCursor,
|
||||
final String? downCursor,
|
||||
}) async {
|
||||
QueryResult<Query$Logs> response;
|
||||
List<ServerLogEntry> logsList = [];
|
||||
ServerLogsPageMeta pageMeta =
|
||||
const ServerLogsPageMeta(downCursor: null, upCursor: null);
|
||||
|
||||
try {
|
||||
final GraphQLClient client = await getClient();
|
||||
final variables = Variables$Query$Logs(
|
||||
upCursor: upCursor,
|
||||
downCursor: downCursor,
|
||||
limit: limit,
|
||||
);
|
||||
final query = Options$Query$Logs(variables: variables);
|
||||
response = await client.query$Logs(query);
|
||||
if (response.hasException) {
|
||||
print(response.exception.toString());
|
||||
}
|
||||
if (response.parsedData == null) {
|
||||
return (logsList, pageMeta);
|
||||
}
|
||||
logsList = response.parsedData?.logs.paginated.entries
|
||||
.map<ServerLogEntry>(
|
||||
(final log) => ServerLogEntry.fromGraphQL(log),
|
||||
)
|
||||
.toList() ??
|
||||
[];
|
||||
pageMeta = ServerLogsPageMeta.fromGraphQL(
|
||||
response.parsedData!.logs.paginated.pageMeta,
|
||||
);
|
||||
} catch (e) {
|
||||
print(e);
|
||||
}
|
||||
|
||||
return (logsList, pageMeta);
|
||||
}
|
||||
|
||||
Stream<ServerLogEntry> getServerLogsStream() async* {
|
||||
final GraphQLClient client = await getSubscriptionClient();
|
||||
final subscription = client.subscribe$LogEntries();
|
||||
await for (final response in subscription) {
|
||||
final log = ServerLogEntry.fromGraphQL(response.parsedData!.logEntries);
|
||||
yield log;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ import 'package:selfprivacy/logic/api_maps/generic_result.dart';
|
|||
import 'package:selfprivacy/logic/api_maps/graphql_maps/graphql_api_map.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/graphql_maps/schema/backups.graphql.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/graphql_maps/schema/disk_volumes.graphql.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/graphql_maps/schema/logs.graphql.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/graphql_maps/schema/schema.graphql.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/graphql_maps/schema/server_api.graphql.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/graphql_maps/schema/server_settings.graphql.dart';
|
||||
|
@ -22,6 +23,7 @@ import 'package:selfprivacy/logic/models/json/dns_records.dart';
|
|||
import 'package:selfprivacy/logic/models/json/recovery_token_status.dart';
|
||||
import 'package:selfprivacy/logic/models/json/server_disk_volume.dart';
|
||||
import 'package:selfprivacy/logic/models/json/server_job.dart';
|
||||
import 'package:selfprivacy/logic/models/server_logs.dart';
|
||||
import 'package:selfprivacy/logic/models/service.dart';
|
||||
import 'package:selfprivacy/logic/models/ssh_settings.dart';
|
||||
import 'package:selfprivacy/logic/models/system_settings.dart';
|
||||
|
@ -34,6 +36,7 @@ part 'server_actions_api.dart';
|
|||
part 'services_api.dart';
|
||||
part 'users_api.dart';
|
||||
part 'volume_api.dart';
|
||||
part 'logs_api.dart';
|
||||
|
||||
class ServerApi extends GraphQLApiMap
|
||||
with
|
||||
|
@ -42,7 +45,8 @@ class ServerApi extends GraphQLApiMap
|
|||
ServerActionsApi,
|
||||
ServicesApi,
|
||||
UsersApi,
|
||||
BackupsApi {
|
||||
BackupsApi,
|
||||
LogsApi {
|
||||
ServerApi({
|
||||
this.hasLogger = false,
|
||||
this.isWithToken = true,
|
||||
|
|
104
lib/logic/bloc/server_logs/server_logs_bloc.dart
Normal file
104
lib/logic/bloc/server_logs/server_logs_bloc.dart
Normal file
|
@ -0,0 +1,104 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:selfprivacy/config/get_it_config.dart';
|
||||
import 'package:selfprivacy/logic/models/server_logs.dart';
|
||||
|
||||
part 'server_logs_event.dart';
|
||||
part 'server_logs_state.dart';
|
||||
|
||||
class ServerLogsBloc extends Bloc<ServerLogsEvent, ServerLogsState> {
|
||||
ServerLogsBloc() : super(ServerLogsInitial()) {
|
||||
on<ServerLogsFetch>((final event, final emit) async {
|
||||
emit(ServerLogsLoading());
|
||||
try {
|
||||
final (logsData, meta) = await _getLogs(limit: 50);
|
||||
emit(ServerLogsLoaded(logsData, meta, false));
|
||||
if (_apiLogsSubscription != null) {
|
||||
await _apiLogsSubscription?.cancel();
|
||||
}
|
||||
_apiLogsSubscription =
|
||||
getIt<ApiConnectionRepository>().api.getServerLogsStream().listen(
|
||||
(final ServerLogEntry logEntry) {
|
||||
print('Got new log entry');
|
||||
print(logEntry);
|
||||
add(ServerLogsGotNewEntry(logEntry));
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
emit(ServerLogsError(e.toString()));
|
||||
}
|
||||
});
|
||||
|
||||
on<ServerLogsFetchMore>((final event, final emit) async {
|
||||
final currentState = state;
|
||||
if (currentState is ServerLogsLoaded &&
|
||||
!currentState.loadingMore &&
|
||||
currentState.meta.upCursor != null) {
|
||||
try {
|
||||
final (logsData, meta) =
|
||||
await _getLogs(limit: 50, downCursor: currentState.meta.upCursor);
|
||||
final allEntries = currentState.entries
|
||||
..addAll(logsData)
|
||||
..sort((final a, final b) => b.timestamp.compareTo(a.timestamp));
|
||||
emit(ServerLogsLoaded(allEntries.toSet().toList(), meta, false));
|
||||
} catch (e) {
|
||||
emit(ServerLogsError(e.toString()));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
on<ServerLogsGotNewEntry>((final event, final emit) {
|
||||
final currentState = state;
|
||||
if (currentState is ServerLogsLoaded) {
|
||||
final entries = currentState.entries;
|
||||
if (!entries.any((final entry) => entry.cursor == event.entry.cursor)) {
|
||||
entries.add(event.entry);
|
||||
entries
|
||||
.sort((final a, final b) => b.timestamp.compareTo(a.timestamp));
|
||||
emit(
|
||||
ServerLogsLoaded(
|
||||
entries,
|
||||
currentState.meta,
|
||||
currentState.loadingMore,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
on<ServerLogsDisconnect>((final event, final emit) {
|
||||
_apiLogsSubscription?.cancel();
|
||||
emit(ServerLogsInitial());
|
||||
});
|
||||
}
|
||||
|
||||
Future<(List<ServerLogEntry>, ServerLogsPageMeta)> _getLogs({
|
||||
// No more than 50
|
||||
required final int limit,
|
||||
// All entries returned will be lesser than this cursor. Sets upper bound on results.
|
||||
final String? upCursor,
|
||||
// All entries returned will be greater than this cursor. Sets lower bound on results.
|
||||
final String? downCursor,
|
||||
// Only one cursor can be set at a time.
|
||||
}) =>
|
||||
getIt<ApiConnectionRepository>().api.getServerLogs(
|
||||
limit: limit,
|
||||
upCursor: upCursor,
|
||||
downCursor: downCursor,
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
_apiLogsSubscription?.cancel();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
@override
|
||||
void onChange(final Change<ServerLogsState> change) {
|
||||
super.onChange(change);
|
||||
}
|
||||
|
||||
StreamSubscription? _apiLogsSubscription;
|
||||
}
|
29
lib/logic/bloc/server_logs/server_logs_event.dart
Normal file
29
lib/logic/bloc/server_logs/server_logs_event.dart
Normal file
|
@ -0,0 +1,29 @@
|
|||
part of 'server_logs_bloc.dart';
|
||||
|
||||
sealed class ServerLogsEvent extends Equatable {
|
||||
const ServerLogsEvent();
|
||||
}
|
||||
|
||||
final class ServerLogsFetch extends ServerLogsEvent {
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
final class ServerLogsFetchMore extends ServerLogsEvent {
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
final class ServerLogsGotNewEntry extends ServerLogsEvent {
|
||||
const ServerLogsGotNewEntry(this.entry);
|
||||
|
||||
final ServerLogEntry entry;
|
||||
|
||||
@override
|
||||
List<Object> get props => [entry];
|
||||
}
|
||||
|
||||
final class ServerLogsDisconnect extends ServerLogsEvent {
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
51
lib/logic/bloc/server_logs/server_logs_state.dart
Normal file
51
lib/logic/bloc/server_logs/server_logs_state.dart
Normal file
|
@ -0,0 +1,51 @@
|
|||
part of 'server_logs_bloc.dart';
|
||||
|
||||
sealed class ServerLogsState extends Equatable {
|
||||
const ServerLogsState();
|
||||
}
|
||||
|
||||
final class ServerLogsInitial extends ServerLogsState {
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
final class ServerLogsLoading extends ServerLogsState {
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
final class ServerLogsLoaded extends ServerLogsState {
|
||||
const ServerLogsLoaded(this.entries, this.meta, this.loadingMore);
|
||||
|
||||
final List<ServerLogEntry> entries;
|
||||
final ServerLogsPageMeta meta;
|
||||
final bool loadingMore;
|
||||
|
||||
List<String> get systemdUnits => entries
|
||||
.map((final entry) => entry.systemdUnit ?? 'kernel')
|
||||
.toSet()
|
||||
.toList();
|
||||
|
||||
(List<ServerLogEntry>, int) entriesForUnit(final String unit) {
|
||||
if (unit == 'kernel') {
|
||||
final filteredEntries =
|
||||
entries.where((final entry) => entry.systemdUnit == null).toList();
|
||||
return (filteredEntries, filteredEntries.length);
|
||||
}
|
||||
final filteredEntries =
|
||||
entries.where((final entry) => entry.systemdUnit == unit).toList();
|
||||
return (filteredEntries, filteredEntries.length);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object> get props => [entries, meta];
|
||||
}
|
||||
|
||||
final class ServerLogsError extends ServerLogsState {
|
||||
const ServerLogsError(this.error);
|
||||
|
||||
final Object error;
|
||||
|
||||
@override
|
||||
List<Object> get props => [error];
|
||||
}
|
67
lib/logic/models/server_logs.dart
Normal file
67
lib/logic/models/server_logs.dart
Normal file
|
@ -0,0 +1,67 @@
|
|||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:selfprivacy/logic/api_maps/graphql_maps/schema/logs.graphql.dart';
|
||||
|
||||
class ServerLogEntry extends Equatable {
|
||||
ServerLogEntry.fromGraphQL(final Fragment$LogEntry log)
|
||||
: this(
|
||||
message: log.message,
|
||||
cursor: log.cursor,
|
||||
priority: log.priority,
|
||||
systemdSlice: log.systemdSlice,
|
||||
systemdUnit: log.systemdUnit,
|
||||
timestamp: log.timestamp,
|
||||
);
|
||||
|
||||
const ServerLogEntry({
|
||||
required this.message,
|
||||
required this.cursor,
|
||||
required this.priority,
|
||||
required this.systemdSlice,
|
||||
required this.systemdUnit,
|
||||
required this.timestamp,
|
||||
});
|
||||
|
||||
final String message;
|
||||
final String cursor;
|
||||
final int? priority;
|
||||
final String? systemdSlice;
|
||||
final String? systemdUnit;
|
||||
final DateTime timestamp;
|
||||
|
||||
static final DateFormat _formatter = DateFormat('hh:mm:ss');
|
||||
String get timeString => _formatter.format(timestamp);
|
||||
String get fullUTCString => timestamp.toUtc().toIso8601String();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
message,
|
||||
cursor,
|
||||
priority,
|
||||
systemdSlice,
|
||||
systemdUnit,
|
||||
timestamp,
|
||||
];
|
||||
}
|
||||
|
||||
class ServerLogsPageMeta extends Equatable {
|
||||
ServerLogsPageMeta.fromGraphQL(final Query$Logs$logs$paginated$pageMeta meta)
|
||||
: this(
|
||||
downCursor: meta.downCursor,
|
||||
upCursor: meta.upCursor,
|
||||
);
|
||||
|
||||
const ServerLogsPageMeta({
|
||||
required this.downCursor,
|
||||
required this.upCursor,
|
||||
});
|
||||
|
||||
final String? downCursor;
|
||||
final String? upCursor;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
downCursor,
|
||||
upCursor,
|
||||
];
|
||||
}
|
|
@ -54,9 +54,7 @@ class _ConsolePageState extends State<ConsolePage> {
|
|||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
console.paused
|
||||
? Icons.play_arrow_outlined
|
||||
: Icons.pause_outlined,
|
||||
console.paused ? Icons.play_arrow_outlined : Icons.pause_outlined,
|
||||
),
|
||||
onPressed: console.paused ? console.play : console.pause,
|
||||
),
|
||||
|
|
336
lib/ui/pages/server_details/logs/logs_screen.dart
Normal file
336
lib/ui/pages/server_details/logs/logs_screen.dart
Normal file
|
@ -0,0 +1,336 @@
|
|||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:selfprivacy/logic/bloc/server_logs/server_logs_bloc.dart';
|
||||
import 'package:selfprivacy/logic/models/server_logs.dart';
|
||||
import 'package:selfprivacy/utils/platform_adapter.dart';
|
||||
|
||||
@RoutePage()
|
||||
class ServerLogsScreen extends StatefulWidget {
|
||||
const ServerLogsScreen({super.key});
|
||||
|
||||
@override
|
||||
State<ServerLogsScreen> createState() => _ServerLogsScreenState();
|
||||
}
|
||||
|
||||
class _ServerLogsScreenState extends State<ServerLogsScreen> {
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
late ServerLogsBloc _serverLogsBloc;
|
||||
|
||||
String? _selectedSystemdUnit;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_serverLogsBloc = BlocProvider.of<ServerLogsBloc>(context);
|
||||
_scrollController.addListener(_onScroll);
|
||||
_serverLogsBloc.add(ServerLogsFetch());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.dispose();
|
||||
_serverLogsBloc.add(ServerLogsDisconnect());
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onScroll() {
|
||||
if (_scrollController.position.pixels >=
|
||||
_scrollController.position.maxScrollExtent) {
|
||||
_serverLogsBloc.add(ServerLogsFetchMore());
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildDrawer(final List<String> systemdUnits) => Drawer(
|
||||
child: ListView(
|
||||
padding: EdgeInsets.zero,
|
||||
children: [
|
||||
DrawerHeader(
|
||||
child: Text('server.filter_by_systemd_unit'.tr()),
|
||||
),
|
||||
// a tile to reset filter
|
||||
RadioListTile(
|
||||
title: Text('server.all_units'.tr()),
|
||||
value: null,
|
||||
groupValue: _selectedSystemdUnit,
|
||||
onChanged: (final value) {
|
||||
setState(() {
|
||||
_selectedSystemdUnit = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
for (final unit in systemdUnits.sorted())
|
||||
RadioListTile(
|
||||
title: Text(unit),
|
||||
value: unit,
|
||||
groupValue: _selectedSystemdUnit,
|
||||
onChanged: (final value) {
|
||||
setState(() {
|
||||
_selectedSystemdUnit = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) => Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('server.logs'.tr()),
|
||||
),
|
||||
endDrawer: BlocBuilder<ServerLogsBloc, ServerLogsState>(
|
||||
builder: (final context, final state) {
|
||||
if (state is ServerLogsLoaded) {
|
||||
return _buildDrawer(state.systemdUnits);
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
body: BlocBuilder<ServerLogsBloc, ServerLogsState>(
|
||||
builder: (final context, final state) {
|
||||
final isLoadingMore =
|
||||
state is ServerLogsLoaded && state.loadingMore;
|
||||
if (state is ServerLogsLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
} else if (state is ServerLogsLoaded) {
|
||||
if (_selectedSystemdUnit == null) {
|
||||
return ListView.builder(
|
||||
controller: _scrollController,
|
||||
itemCount: state.entries.length + (isLoadingMore ? 1 : 0),
|
||||
itemBuilder: (final context, final index) {
|
||||
if (isLoadingMore && index == state.entries.length) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
final logEntry = state.entries[index];
|
||||
return LogEntryWidget(
|
||||
logEntry: logEntry,
|
||||
key: ValueKey(logEntry.cursor),
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
final (filteredLogs, filteredLength) =
|
||||
state.entriesForUnit(_selectedSystemdUnit!);
|
||||
return ListView.builder(
|
||||
controller: _scrollController,
|
||||
itemCount: filteredLength + (isLoadingMore ? 1 : 0),
|
||||
itemBuilder: (final context, final index) {
|
||||
if (isLoadingMore && index == filteredLength) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
final logEntry = filteredLogs[index];
|
||||
return LogEntryWidget(
|
||||
logEntry: logEntry,
|
||||
key: ValueKey(logEntry.cursor),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
} else if (state is ServerLogsError) {
|
||||
return Center(child: Text('Error: ${state.error}'));
|
||||
}
|
||||
return Center(child: Text('server.no_logs'.tr()));
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class LogEntryWidget extends StatelessWidget {
|
||||
const LogEntryWidget({
|
||||
required this.logEntry,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final ServerLogEntry logEntry;
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) {
|
||||
final Color color = logEntry.priority == 4
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: logEntry.priority != null && logEntry.priority! <= 3
|
||||
? Theme.of(context).colorScheme.error
|
||||
: Theme.of(context).colorScheme.onSurface;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: ListTile(
|
||||
dense: true,
|
||||
title: Text.rich(
|
||||
TextSpan(
|
||||
style: DefaultTextStyle.of(context).style,
|
||||
children: <TextSpan>[
|
||||
TextSpan(
|
||||
text: '${logEntry.timeString}: ',
|
||||
style: TextStyle(
|
||||
fontFeatures: const [FontFeature.tabularFigures()],
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: logEntry.systemdUnit,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
logEntry.message,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 6,
|
||||
),
|
||||
onTap: () => showDialog(
|
||||
context: context,
|
||||
builder: (final BuildContext context) =>
|
||||
ServerLogEntryDialog(log: logEntry),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// dialog with detailed log content
|
||||
class ServerLogEntryDialog extends StatelessWidget {
|
||||
const ServerLogEntryDialog({
|
||||
required this.log,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final ServerLogEntry log;
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) => AlertDialog(
|
||||
scrollable: true,
|
||||
title: Text(log.timeString),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
vertical: 16,
|
||||
horizontal: 12,
|
||||
),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const Divider(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: SelectableText.rich(
|
||||
TextSpan(
|
||||
style: DefaultTextStyle.of(context).style,
|
||||
children: <TextSpan>[
|
||||
TextSpan(
|
||||
text: '${'console_page.logged_at'.tr()}: ',
|
||||
style: const TextStyle(),
|
||||
),
|
||||
TextSpan(
|
||||
text: '${log.timeString} (${log.fullUTCString})',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
fontFeatures: [FontFeature.tabularFigures()],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
_SectionRow('server.log_dialog.metadata'.tr()),
|
||||
_KeyValueRow('server.log_dialog.cursor'.tr(), log.cursor),
|
||||
if (log.priority != null)
|
||||
_KeyValueRow(
|
||||
'server.log_dialog.priority'.tr(), log.priority?.toString()),
|
||||
if (log.systemdSlice != null)
|
||||
_KeyValueRow(
|
||||
'server.log_dialog.systemd_slice'.tr(), log.systemdSlice),
|
||||
if (log.systemdUnit != null)
|
||||
_KeyValueRow(
|
||||
'server.log_dialog.systemd_unit'.tr(), log.systemdUnit),
|
||||
const Divider(),
|
||||
_SectionRow('server.log_dialog.message'.tr()),
|
||||
_DataRow(log.message),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
// A button to copy the request to the clipboard
|
||||
if (log.message.isNotEmpty)
|
||||
TextButton(
|
||||
onPressed: () => PlatformAdapter.setClipboard(log.message),
|
||||
child: Text('console_page.copy'.tr()),
|
||||
),
|
||||
// close dialog
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text('basis.close'.tr()),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
class _SectionRow extends StatelessWidget {
|
||||
const _SectionRow(this.title);
|
||||
|
||||
final String title;
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Theme.of(context).colorScheme.outlineVariant,
|
||||
width: 2.4,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: SelectableText(
|
||||
title.tr(),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _KeyValueRow extends StatelessWidget {
|
||||
const _KeyValueRow(this.title, this.value);
|
||||
|
||||
final String title;
|
||||
final String? value;
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: SelectableText.rich(
|
||||
TextSpan(
|
||||
style: DefaultTextStyle.of(context).style,
|
||||
children: <TextSpan>[
|
||||
TextSpan(
|
||||
text: '$title: ',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
TextSpan(text: value ?? ''),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _DataRow extends StatelessWidget {
|
||||
const _DataRow(this.data);
|
||||
|
||||
final String? data;
|
||||
|
||||
@override
|
||||
Widget build(final BuildContext context) => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: SelectableText(
|
||||
data ?? 'null',
|
||||
style: const TextStyle(fontWeight: FontWeight.w400),
|
||||
),
|
||||
);
|
||||
}
|
|
@ -80,6 +80,11 @@ class _ServerDetailsScreenState extends State<ServerDetailsScreen>
|
|||
leading: const Icon(BrandIcons.settings),
|
||||
onTap: () => context.pushRoute(const ServerSettingsRoute()),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('server.logs'.tr()),
|
||||
leading: const Icon(Icons.manage_search_outlined),
|
||||
onTap: () => context.pushRoute(const ServerLogsRoute()),
|
||||
),
|
||||
const Divider(height: 32),
|
||||
Text(
|
||||
'server.resource_usage'.tr(),
|
||||
|
|
|
@ -16,6 +16,7 @@ import 'package:selfprivacy/ui/pages/onboarding/onboarding.dart';
|
|||
import 'package:selfprivacy/ui/pages/providers/providers.dart';
|
||||
import 'package:selfprivacy/ui/pages/recovery_key/recovery_key.dart';
|
||||
import 'package:selfprivacy/ui/pages/root_route.dart';
|
||||
import 'package:selfprivacy/ui/pages/server_details/logs/logs_screen.dart';
|
||||
import 'package:selfprivacy/ui/pages/server_details/server_details_screen.dart';
|
||||
import 'package:selfprivacy/ui/pages/server_details/server_settings_screen.dart';
|
||||
import 'package:selfprivacy/ui/pages/server_storage/binds_migration/services_migration.dart';
|
||||
|
@ -105,6 +106,7 @@ class RootRouter extends _$RootRouter {
|
|||
AutoRoute(page: ServerStorageRoute.page),
|
||||
AutoRoute(page: ExtendingVolumeRoute.page),
|
||||
AutoRoute(page: ServerSettingsRoute.page),
|
||||
AutoRoute(page: ServerLogsRoute.page),
|
||||
],
|
||||
),
|
||||
AutoRoute(page: ServicesMigrationRoute.page),
|
||||
|
@ -150,6 +152,8 @@ String getRouteTitle(final String routeName) {
|
|||
return 'server.card_title';
|
||||
case 'ServerSettingsRoute':
|
||||
return 'server.settings';
|
||||
case 'ServerLogsRoute':
|
||||
return 'server.logs';
|
||||
case 'BackupDetailsRoute':
|
||||
return 'backup.card_title';
|
||||
case 'BackupsListRoute':
|
||||
|
|
|
@ -132,6 +132,12 @@ abstract class _$RootRouter extends RootStackRouter {
|
|||
child: const ServerDetailsScreen(),
|
||||
);
|
||||
},
|
||||
ServerLogsRoute.name: (routeData) {
|
||||
return AutoRoutePage<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const ServerLogsScreen(),
|
||||
);
|
||||
},
|
||||
ServerSettingsRoute.name: (routeData) {
|
||||
return AutoRoutePage<dynamic>(
|
||||
routeData: routeData,
|
||||
|
@ -510,6 +516,20 @@ class ServerDetailsRoute extends PageRouteInfo<void> {
|
|||
static const PageInfo<void> page = PageInfo<void>(name);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [ServerLogsScreen]
|
||||
class ServerLogsRoute extends PageRouteInfo<void> {
|
||||
const ServerLogsRoute({List<PageRouteInfo>? children})
|
||||
: super(
|
||||
ServerLogsRoute.name,
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'ServerLogsRoute';
|
||||
|
||||
static const PageInfo<void> page = PageInfo<void>(name);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [ServerSettingsScreen]
|
||||
class ServerSettingsRoute extends PageRouteInfo<void> {
|
||||
|
|
Loading…
Reference in a new issue