diff --git a/assets/translations/en.json b/assets/translations/en.json index be110df9..a73b1716 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -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": { diff --git a/lib/config/bloc_config.dart b/lib/config/bloc_config.dart index 83027d7b..293d85d4 100644 --- a/lib/config/bloc_config.dart +++ b/lib/config/bloc_config.dart @@ -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 { 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 { connectionStatusBloc = ConnectionStatusBloc(); serverDetailsCubit = ServerDetailsCubit(); volumesBloc = VolumesBloc(); + serverLogsBloc = ServerLogsBloc(); } @override @@ -94,6 +97,9 @@ class BlocAndProviderConfigState extends State { BlocProvider( create: (final _) => JobsCubit(), ), + BlocProvider( + create: (final _) => serverLogsBloc, + ), ], child: widget.child, ); diff --git a/lib/logic/api_maps/graphql_maps/schema/logs.graphql b/lib/logic/api_maps/graphql_maps/schema/logs.graphql new file mode 100644 index 00000000..bad9a19e --- /dev/null +++ b/lib/logic/api_maps/graphql_maps/schema/logs.graphql @@ -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 + } +} diff --git a/lib/logic/api_maps/graphql_maps/schema/logs.graphql.dart b/lib/logic/api_maps/graphql_maps/schema/logs.graphql.dart new file mode 100644 index 00000000..b92e876f --- /dev/null +++ b/lib/logic/api_maps/graphql_maps/schema/logs.graphql.dart @@ -0,0 +1,1588 @@ +import 'dart:async'; +import 'package:gql/ast.dart'; +import 'package:graphql/client.dart' as graphql; +import 'package:selfprivacy/utils/scalars.dart'; + +class Fragment$LogEntry { + Fragment$LogEntry({ + required this.message, + required this.timestamp, + this.priority, + this.systemdUnit, + this.systemdSlice, + required this.cursor, + this.$__typename = 'LogEntry', + }); + + factory Fragment$LogEntry.fromJson(Map json) { + final l$message = json['message']; + final l$timestamp = json['timestamp']; + final l$priority = json['priority']; + final l$systemdUnit = json['systemdUnit']; + final l$systemdSlice = json['systemdSlice']; + final l$cursor = json['cursor']; + final l$$__typename = json['__typename']; + return Fragment$LogEntry( + message: (l$message as String), + timestamp: dateTimeFromJson(l$timestamp), + priority: (l$priority as int?), + systemdUnit: (l$systemdUnit as String?), + systemdSlice: (l$systemdSlice as String?), + cursor: (l$cursor as String), + $__typename: (l$$__typename as String), + ); + } + + final String message; + + final DateTime timestamp; + + final int? priority; + + final String? systemdUnit; + + final String? systemdSlice; + + final String cursor; + + final String $__typename; + + Map toJson() { + final _resultData = {}; + final l$message = message; + _resultData['message'] = l$message; + final l$timestamp = timestamp; + _resultData['timestamp'] = dateTimeToJson(l$timestamp); + final l$priority = priority; + _resultData['priority'] = l$priority; + final l$systemdUnit = systemdUnit; + _resultData['systemdUnit'] = l$systemdUnit; + final l$systemdSlice = systemdSlice; + _resultData['systemdSlice'] = l$systemdSlice; + final l$cursor = cursor; + _resultData['cursor'] = l$cursor; + final l$$__typename = $__typename; + _resultData['__typename'] = l$$__typename; + return _resultData; + } + + @override + int get hashCode { + final l$message = message; + final l$timestamp = timestamp; + final l$priority = priority; + final l$systemdUnit = systemdUnit; + final l$systemdSlice = systemdSlice; + final l$cursor = cursor; + final l$$__typename = $__typename; + return Object.hashAll([ + l$message, + l$timestamp, + l$priority, + l$systemdUnit, + l$systemdSlice, + l$cursor, + l$$__typename, + ]); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (!(other is Fragment$LogEntry) || runtimeType != other.runtimeType) { + return false; + } + final l$message = message; + final lOther$message = other.message; + if (l$message != lOther$message) { + return false; + } + final l$timestamp = timestamp; + final lOther$timestamp = other.timestamp; + if (l$timestamp != lOther$timestamp) { + return false; + } + final l$priority = priority; + final lOther$priority = other.priority; + if (l$priority != lOther$priority) { + return false; + } + final l$systemdUnit = systemdUnit; + final lOther$systemdUnit = other.systemdUnit; + if (l$systemdUnit != lOther$systemdUnit) { + return false; + } + final l$systemdSlice = systemdSlice; + final lOther$systemdSlice = other.systemdSlice; + if (l$systemdSlice != lOther$systemdSlice) { + return false; + } + final l$cursor = cursor; + final lOther$cursor = other.cursor; + if (l$cursor != lOther$cursor) { + return false; + } + final l$$__typename = $__typename; + final lOther$$__typename = other.$__typename; + if (l$$__typename != lOther$$__typename) { + return false; + } + return true; + } +} + +extension UtilityExtension$Fragment$LogEntry on Fragment$LogEntry { + CopyWith$Fragment$LogEntry get copyWith => + CopyWith$Fragment$LogEntry( + this, + (i) => i, + ); +} + +abstract class CopyWith$Fragment$LogEntry { + factory CopyWith$Fragment$LogEntry( + Fragment$LogEntry instance, + TRes Function(Fragment$LogEntry) then, + ) = _CopyWithImpl$Fragment$LogEntry; + + factory CopyWith$Fragment$LogEntry.stub(TRes res) = + _CopyWithStubImpl$Fragment$LogEntry; + + TRes call({ + String? message, + DateTime? timestamp, + int? priority, + String? systemdUnit, + String? systemdSlice, + String? cursor, + String? $__typename, + }); +} + +class _CopyWithImpl$Fragment$LogEntry + implements CopyWith$Fragment$LogEntry { + _CopyWithImpl$Fragment$LogEntry( + this._instance, + this._then, + ); + + final Fragment$LogEntry _instance; + + final TRes Function(Fragment$LogEntry) _then; + + static const _undefined = {}; + + TRes call({ + Object? message = _undefined, + Object? timestamp = _undefined, + Object? priority = _undefined, + Object? systemdUnit = _undefined, + Object? systemdSlice = _undefined, + Object? cursor = _undefined, + Object? $__typename = _undefined, + }) => + _then(Fragment$LogEntry( + message: message == _undefined || message == null + ? _instance.message + : (message as String), + timestamp: timestamp == _undefined || timestamp == null + ? _instance.timestamp + : (timestamp as DateTime), + priority: + priority == _undefined ? _instance.priority : (priority as int?), + systemdUnit: systemdUnit == _undefined + ? _instance.systemdUnit + : (systemdUnit as String?), + systemdSlice: systemdSlice == _undefined + ? _instance.systemdSlice + : (systemdSlice as String?), + cursor: cursor == _undefined || cursor == null + ? _instance.cursor + : (cursor as String), + $__typename: $__typename == _undefined || $__typename == null + ? _instance.$__typename + : ($__typename as String), + )); +} + +class _CopyWithStubImpl$Fragment$LogEntry + implements CopyWith$Fragment$LogEntry { + _CopyWithStubImpl$Fragment$LogEntry(this._res); + + TRes _res; + + call({ + String? message, + DateTime? timestamp, + int? priority, + String? systemdUnit, + String? systemdSlice, + String? cursor, + String? $__typename, + }) => + _res; +} + +const fragmentDefinitionLogEntry = FragmentDefinitionNode( + name: NameNode(value: 'LogEntry'), + typeCondition: TypeConditionNode( + on: NamedTypeNode( + name: NameNode(value: 'LogEntry'), + isNonNull: false, + )), + directives: [], + selectionSet: SelectionSetNode(selections: [ + FieldNode( + name: NameNode(value: 'message'), + alias: null, + arguments: [], + directives: [], + selectionSet: null, + ), + FieldNode( + name: NameNode(value: 'timestamp'), + alias: null, + arguments: [], + directives: [], + selectionSet: null, + ), + FieldNode( + name: NameNode(value: 'priority'), + alias: null, + arguments: [], + directives: [], + selectionSet: null, + ), + FieldNode( + name: NameNode(value: 'systemdUnit'), + alias: null, + arguments: [], + directives: [], + selectionSet: null, + ), + FieldNode( + name: NameNode(value: 'systemdSlice'), + alias: null, + arguments: [], + directives: [], + selectionSet: null, + ), + FieldNode( + name: NameNode(value: 'cursor'), + alias: null, + arguments: [], + directives: [], + selectionSet: null, + ), + FieldNode( + name: NameNode(value: '__typename'), + alias: null, + arguments: [], + directives: [], + selectionSet: null, + ), + ]), +); +const documentNodeFragmentLogEntry = DocumentNode(definitions: [ + fragmentDefinitionLogEntry, +]); + +extension ClientExtension$Fragment$LogEntry on graphql.GraphQLClient { + void writeFragment$LogEntry({ + required Fragment$LogEntry data, + required Map idFields, + bool broadcast = true, + }) => + this.writeFragment( + graphql.FragmentRequest( + idFields: idFields, + fragment: const graphql.Fragment( + fragmentName: 'LogEntry', + document: documentNodeFragmentLogEntry, + ), + ), + data: data.toJson(), + broadcast: broadcast, + ); + Fragment$LogEntry? readFragment$LogEntry({ + required Map idFields, + bool optimistic = true, + }) { + final result = this.readFragment( + graphql.FragmentRequest( + idFields: idFields, + fragment: const graphql.Fragment( + fragmentName: 'LogEntry', + document: documentNodeFragmentLogEntry, + ), + ), + optimistic: optimistic, + ); + return result == null ? null : Fragment$LogEntry.fromJson(result); + } +} + +class Variables$Query$Logs { + factory Variables$Query$Logs({ + required int limit, + String? upCursor, + String? downCursor, + }) => + Variables$Query$Logs._({ + r'limit': limit, + if (upCursor != null) r'upCursor': upCursor, + if (downCursor != null) r'downCursor': downCursor, + }); + + Variables$Query$Logs._(this._$data); + + factory Variables$Query$Logs.fromJson(Map data) { + final result$data = {}; + final l$limit = data['limit']; + result$data['limit'] = (l$limit as int); + if (data.containsKey('upCursor')) { + final l$upCursor = data['upCursor']; + result$data['upCursor'] = (l$upCursor as String?); + } + if (data.containsKey('downCursor')) { + final l$downCursor = data['downCursor']; + result$data['downCursor'] = (l$downCursor as String?); + } + return Variables$Query$Logs._(result$data); + } + + Map _$data; + + int get limit => (_$data['limit'] as int); + + String? get upCursor => (_$data['upCursor'] as String?); + + String? get downCursor => (_$data['downCursor'] as String?); + + Map toJson() { + final result$data = {}; + final l$limit = limit; + result$data['limit'] = l$limit; + if (_$data.containsKey('upCursor')) { + final l$upCursor = upCursor; + result$data['upCursor'] = l$upCursor; + } + if (_$data.containsKey('downCursor')) { + final l$downCursor = downCursor; + result$data['downCursor'] = l$downCursor; + } + return result$data; + } + + CopyWith$Variables$Query$Logs get copyWith => + CopyWith$Variables$Query$Logs( + this, + (i) => i, + ); + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (!(other is Variables$Query$Logs) || runtimeType != other.runtimeType) { + return false; + } + final l$limit = limit; + final lOther$limit = other.limit; + if (l$limit != lOther$limit) { + return false; + } + final l$upCursor = upCursor; + final lOther$upCursor = other.upCursor; + if (_$data.containsKey('upCursor') != + other._$data.containsKey('upCursor')) { + return false; + } + if (l$upCursor != lOther$upCursor) { + return false; + } + final l$downCursor = downCursor; + final lOther$downCursor = other.downCursor; + if (_$data.containsKey('downCursor') != + other._$data.containsKey('downCursor')) { + return false; + } + if (l$downCursor != lOther$downCursor) { + return false; + } + return true; + } + + @override + int get hashCode { + final l$limit = limit; + final l$upCursor = upCursor; + final l$downCursor = downCursor; + return Object.hashAll([ + l$limit, + _$data.containsKey('upCursor') ? l$upCursor : const {}, + _$data.containsKey('downCursor') ? l$downCursor : const {}, + ]); + } +} + +abstract class CopyWith$Variables$Query$Logs { + factory CopyWith$Variables$Query$Logs( + Variables$Query$Logs instance, + TRes Function(Variables$Query$Logs) then, + ) = _CopyWithImpl$Variables$Query$Logs; + + factory CopyWith$Variables$Query$Logs.stub(TRes res) = + _CopyWithStubImpl$Variables$Query$Logs; + + TRes call({ + int? limit, + String? upCursor, + String? downCursor, + }); +} + +class _CopyWithImpl$Variables$Query$Logs + implements CopyWith$Variables$Query$Logs { + _CopyWithImpl$Variables$Query$Logs( + this._instance, + this._then, + ); + + final Variables$Query$Logs _instance; + + final TRes Function(Variables$Query$Logs) _then; + + static const _undefined = {}; + + TRes call({ + Object? limit = _undefined, + Object? upCursor = _undefined, + Object? downCursor = _undefined, + }) => + _then(Variables$Query$Logs._({ + ..._instance._$data, + if (limit != _undefined && limit != null) 'limit': (limit as int), + if (upCursor != _undefined) 'upCursor': (upCursor as String?), + if (downCursor != _undefined) 'downCursor': (downCursor as String?), + })); +} + +class _CopyWithStubImpl$Variables$Query$Logs + implements CopyWith$Variables$Query$Logs { + _CopyWithStubImpl$Variables$Query$Logs(this._res); + + TRes _res; + + call({ + int? limit, + String? upCursor, + String? downCursor, + }) => + _res; +} + +class Query$Logs { + Query$Logs({ + required this.logs, + this.$__typename = 'Query', + }); + + factory Query$Logs.fromJson(Map json) { + final l$logs = json['logs']; + final l$$__typename = json['__typename']; + return Query$Logs( + logs: Query$Logs$logs.fromJson((l$logs as Map)), + $__typename: (l$$__typename as String), + ); + } + + final Query$Logs$logs logs; + + final String $__typename; + + Map toJson() { + final _resultData = {}; + final l$logs = logs; + _resultData['logs'] = l$logs.toJson(); + final l$$__typename = $__typename; + _resultData['__typename'] = l$$__typename; + return _resultData; + } + + @override + int get hashCode { + final l$logs = logs; + final l$$__typename = $__typename; + return Object.hashAll([ + l$logs, + l$$__typename, + ]); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (!(other is Query$Logs) || runtimeType != other.runtimeType) { + return false; + } + final l$logs = logs; + final lOther$logs = other.logs; + if (l$logs != lOther$logs) { + return false; + } + final l$$__typename = $__typename; + final lOther$$__typename = other.$__typename; + if (l$$__typename != lOther$$__typename) { + return false; + } + return true; + } +} + +extension UtilityExtension$Query$Logs on Query$Logs { + CopyWith$Query$Logs get copyWith => CopyWith$Query$Logs( + this, + (i) => i, + ); +} + +abstract class CopyWith$Query$Logs { + factory CopyWith$Query$Logs( + Query$Logs instance, + TRes Function(Query$Logs) then, + ) = _CopyWithImpl$Query$Logs; + + factory CopyWith$Query$Logs.stub(TRes res) = _CopyWithStubImpl$Query$Logs; + + TRes call({ + Query$Logs$logs? logs, + String? $__typename, + }); + CopyWith$Query$Logs$logs get logs; +} + +class _CopyWithImpl$Query$Logs implements CopyWith$Query$Logs { + _CopyWithImpl$Query$Logs( + this._instance, + this._then, + ); + + final Query$Logs _instance; + + final TRes Function(Query$Logs) _then; + + static const _undefined = {}; + + TRes call({ + Object? logs = _undefined, + Object? $__typename = _undefined, + }) => + _then(Query$Logs( + logs: logs == _undefined || logs == null + ? _instance.logs + : (logs as Query$Logs$logs), + $__typename: $__typename == _undefined || $__typename == null + ? _instance.$__typename + : ($__typename as String), + )); + + CopyWith$Query$Logs$logs get logs { + final local$logs = _instance.logs; + return CopyWith$Query$Logs$logs(local$logs, (e) => call(logs: e)); + } +} + +class _CopyWithStubImpl$Query$Logs implements CopyWith$Query$Logs { + _CopyWithStubImpl$Query$Logs(this._res); + + TRes _res; + + call({ + Query$Logs$logs? logs, + String? $__typename, + }) => + _res; + + CopyWith$Query$Logs$logs get logs => + CopyWith$Query$Logs$logs.stub(_res); +} + +const documentNodeQueryLogs = DocumentNode(definitions: [ + OperationDefinitionNode( + type: OperationType.query, + name: NameNode(value: 'Logs'), + variableDefinitions: [ + VariableDefinitionNode( + variable: VariableNode(name: NameNode(value: 'limit')), + type: NamedTypeNode( + name: NameNode(value: 'Int'), + isNonNull: true, + ), + defaultValue: DefaultValueNode(value: null), + directives: [], + ), + VariableDefinitionNode( + variable: VariableNode(name: NameNode(value: 'upCursor')), + type: NamedTypeNode( + name: NameNode(value: 'String'), + isNonNull: false, + ), + defaultValue: DefaultValueNode(value: null), + directives: [], + ), + VariableDefinitionNode( + variable: VariableNode(name: NameNode(value: 'downCursor')), + type: NamedTypeNode( + name: NameNode(value: 'String'), + isNonNull: false, + ), + defaultValue: DefaultValueNode(value: null), + directives: [], + ), + ], + directives: [], + selectionSet: SelectionSetNode(selections: [ + FieldNode( + name: NameNode(value: 'logs'), + alias: null, + arguments: [], + directives: [], + selectionSet: SelectionSetNode(selections: [ + FieldNode( + name: NameNode(value: 'paginated'), + alias: null, + arguments: [ + ArgumentNode( + name: NameNode(value: 'limit'), + value: VariableNode(name: NameNode(value: 'limit')), + ), + ArgumentNode( + name: NameNode(value: 'upCursor'), + value: VariableNode(name: NameNode(value: 'upCursor')), + ), + ArgumentNode( + name: NameNode(value: 'downCursor'), + value: VariableNode(name: NameNode(value: 'downCursor')), + ), + ], + directives: [], + selectionSet: SelectionSetNode(selections: [ + FieldNode( + name: NameNode(value: 'pageMeta'), + alias: null, + arguments: [], + directives: [], + selectionSet: SelectionSetNode(selections: [ + FieldNode( + name: NameNode(value: 'upCursor'), + alias: null, + arguments: [], + directives: [], + selectionSet: null, + ), + FieldNode( + name: NameNode(value: 'downCursor'), + alias: null, + arguments: [], + directives: [], + selectionSet: null, + ), + FieldNode( + name: NameNode(value: '__typename'), + alias: null, + arguments: [], + directives: [], + selectionSet: null, + ), + ]), + ), + FieldNode( + name: NameNode(value: 'entries'), + alias: null, + arguments: [], + directives: [], + selectionSet: SelectionSetNode(selections: [ + FragmentSpreadNode( + name: NameNode(value: 'LogEntry'), + directives: [], + ), + FieldNode( + name: NameNode(value: '__typename'), + alias: null, + arguments: [], + directives: [], + selectionSet: null, + ), + ]), + ), + FieldNode( + name: NameNode(value: '__typename'), + alias: null, + arguments: [], + directives: [], + selectionSet: null, + ), + ]), + ), + FieldNode( + name: NameNode(value: '__typename'), + alias: null, + arguments: [], + directives: [], + selectionSet: null, + ), + ]), + ), + FieldNode( + name: NameNode(value: '__typename'), + alias: null, + arguments: [], + directives: [], + selectionSet: null, + ), + ]), + ), + fragmentDefinitionLogEntry, +]); +Query$Logs _parserFn$Query$Logs(Map data) => + Query$Logs.fromJson(data); +typedef OnQueryComplete$Query$Logs = FutureOr Function( + Map?, + Query$Logs?, +); + +class Options$Query$Logs extends graphql.QueryOptions { + Options$Query$Logs({ + String? operationName, + required Variables$Query$Logs variables, + graphql.FetchPolicy? fetchPolicy, + graphql.ErrorPolicy? errorPolicy, + graphql.CacheRereadPolicy? cacheRereadPolicy, + Object? optimisticResult, + Query$Logs? typedOptimisticResult, + Duration? pollInterval, + graphql.Context? context, + OnQueryComplete$Query$Logs? onComplete, + graphql.OnQueryError? onError, + }) : onCompleteWithParsed = onComplete, + super( + variables: variables.toJson(), + operationName: operationName, + fetchPolicy: fetchPolicy, + errorPolicy: errorPolicy, + cacheRereadPolicy: cacheRereadPolicy, + optimisticResult: optimisticResult ?? typedOptimisticResult?.toJson(), + pollInterval: pollInterval, + context: context, + onComplete: onComplete == null + ? null + : (data) => onComplete( + data, + data == null ? null : _parserFn$Query$Logs(data), + ), + onError: onError, + document: documentNodeQueryLogs, + parserFn: _parserFn$Query$Logs, + ); + + final OnQueryComplete$Query$Logs? onCompleteWithParsed; + + @override + List get properties => [ + ...super.onComplete == null + ? super.properties + : super.properties.where((property) => property != onComplete), + onCompleteWithParsed, + ]; +} + +class WatchOptions$Query$Logs extends graphql.WatchQueryOptions { + WatchOptions$Query$Logs({ + String? operationName, + required Variables$Query$Logs variables, + graphql.FetchPolicy? fetchPolicy, + graphql.ErrorPolicy? errorPolicy, + graphql.CacheRereadPolicy? cacheRereadPolicy, + Object? optimisticResult, + Query$Logs? typedOptimisticResult, + graphql.Context? context, + Duration? pollInterval, + bool? eagerlyFetchResults, + bool carryForwardDataOnException = true, + bool fetchResults = false, + }) : super( + variables: variables.toJson(), + operationName: operationName, + fetchPolicy: fetchPolicy, + errorPolicy: errorPolicy, + cacheRereadPolicy: cacheRereadPolicy, + optimisticResult: optimisticResult ?? typedOptimisticResult?.toJson(), + context: context, + document: documentNodeQueryLogs, + pollInterval: pollInterval, + eagerlyFetchResults: eagerlyFetchResults, + carryForwardDataOnException: carryForwardDataOnException, + fetchResults: fetchResults, + parserFn: _parserFn$Query$Logs, + ); +} + +class FetchMoreOptions$Query$Logs extends graphql.FetchMoreOptions { + FetchMoreOptions$Query$Logs({ + required graphql.UpdateQuery updateQuery, + required Variables$Query$Logs variables, + }) : super( + updateQuery: updateQuery, + variables: variables.toJson(), + document: documentNodeQueryLogs, + ); +} + +extension ClientExtension$Query$Logs on graphql.GraphQLClient { + Future> query$Logs( + Options$Query$Logs options) async => + await this.query(options); + graphql.ObservableQuery watchQuery$Logs( + WatchOptions$Query$Logs options) => + this.watchQuery(options); + void writeQuery$Logs({ + required Query$Logs data, + required Variables$Query$Logs variables, + bool broadcast = true, + }) => + this.writeQuery( + graphql.Request( + operation: graphql.Operation(document: documentNodeQueryLogs), + variables: variables.toJson(), + ), + data: data.toJson(), + broadcast: broadcast, + ); + Query$Logs? readQuery$Logs({ + required Variables$Query$Logs variables, + bool optimistic = true, + }) { + final result = this.readQuery( + graphql.Request( + operation: graphql.Operation(document: documentNodeQueryLogs), + variables: variables.toJson(), + ), + optimistic: optimistic, + ); + return result == null ? null : Query$Logs.fromJson(result); + } +} + +class Query$Logs$logs { + Query$Logs$logs({ + required this.paginated, + this.$__typename = 'Logs', + }); + + factory Query$Logs$logs.fromJson(Map json) { + final l$paginated = json['paginated']; + final l$$__typename = json['__typename']; + return Query$Logs$logs( + paginated: Query$Logs$logs$paginated.fromJson( + (l$paginated as Map)), + $__typename: (l$$__typename as String), + ); + } + + final Query$Logs$logs$paginated paginated; + + final String $__typename; + + Map toJson() { + final _resultData = {}; + final l$paginated = paginated; + _resultData['paginated'] = l$paginated.toJson(); + final l$$__typename = $__typename; + _resultData['__typename'] = l$$__typename; + return _resultData; + } + + @override + int get hashCode { + final l$paginated = paginated; + final l$$__typename = $__typename; + return Object.hashAll([ + l$paginated, + l$$__typename, + ]); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (!(other is Query$Logs$logs) || runtimeType != other.runtimeType) { + return false; + } + final l$paginated = paginated; + final lOther$paginated = other.paginated; + if (l$paginated != lOther$paginated) { + return false; + } + final l$$__typename = $__typename; + final lOther$$__typename = other.$__typename; + if (l$$__typename != lOther$$__typename) { + return false; + } + return true; + } +} + +extension UtilityExtension$Query$Logs$logs on Query$Logs$logs { + CopyWith$Query$Logs$logs get copyWith => + CopyWith$Query$Logs$logs( + this, + (i) => i, + ); +} + +abstract class CopyWith$Query$Logs$logs { + factory CopyWith$Query$Logs$logs( + Query$Logs$logs instance, + TRes Function(Query$Logs$logs) then, + ) = _CopyWithImpl$Query$Logs$logs; + + factory CopyWith$Query$Logs$logs.stub(TRes res) = + _CopyWithStubImpl$Query$Logs$logs; + + TRes call({ + Query$Logs$logs$paginated? paginated, + String? $__typename, + }); + CopyWith$Query$Logs$logs$paginated get paginated; +} + +class _CopyWithImpl$Query$Logs$logs + implements CopyWith$Query$Logs$logs { + _CopyWithImpl$Query$Logs$logs( + this._instance, + this._then, + ); + + final Query$Logs$logs _instance; + + final TRes Function(Query$Logs$logs) _then; + + static const _undefined = {}; + + TRes call({ + Object? paginated = _undefined, + Object? $__typename = _undefined, + }) => + _then(Query$Logs$logs( + paginated: paginated == _undefined || paginated == null + ? _instance.paginated + : (paginated as Query$Logs$logs$paginated), + $__typename: $__typename == _undefined || $__typename == null + ? _instance.$__typename + : ($__typename as String), + )); + + CopyWith$Query$Logs$logs$paginated get paginated { + final local$paginated = _instance.paginated; + return CopyWith$Query$Logs$logs$paginated( + local$paginated, (e) => call(paginated: e)); + } +} + +class _CopyWithStubImpl$Query$Logs$logs + implements CopyWith$Query$Logs$logs { + _CopyWithStubImpl$Query$Logs$logs(this._res); + + TRes _res; + + call({ + Query$Logs$logs$paginated? paginated, + String? $__typename, + }) => + _res; + + CopyWith$Query$Logs$logs$paginated get paginated => + CopyWith$Query$Logs$logs$paginated.stub(_res); +} + +class Query$Logs$logs$paginated { + Query$Logs$logs$paginated({ + required this.pageMeta, + required this.entries, + this.$__typename = 'PaginatedEntries', + }); + + factory Query$Logs$logs$paginated.fromJson(Map json) { + final l$pageMeta = json['pageMeta']; + final l$entries = json['entries']; + final l$$__typename = json['__typename']; + return Query$Logs$logs$paginated( + pageMeta: Query$Logs$logs$paginated$pageMeta.fromJson( + (l$pageMeta as Map)), + entries: (l$entries as List) + .map((e) => Fragment$LogEntry.fromJson((e as Map))) + .toList(), + $__typename: (l$$__typename as String), + ); + } + + final Query$Logs$logs$paginated$pageMeta pageMeta; + + final List entries; + + final String $__typename; + + Map toJson() { + final _resultData = {}; + final l$pageMeta = pageMeta; + _resultData['pageMeta'] = l$pageMeta.toJson(); + final l$entries = entries; + _resultData['entries'] = l$entries.map((e) => e.toJson()).toList(); + final l$$__typename = $__typename; + _resultData['__typename'] = l$$__typename; + return _resultData; + } + + @override + int get hashCode { + final l$pageMeta = pageMeta; + final l$entries = entries; + final l$$__typename = $__typename; + return Object.hashAll([ + l$pageMeta, + Object.hashAll(l$entries.map((v) => v)), + l$$__typename, + ]); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (!(other is Query$Logs$logs$paginated) || + runtimeType != other.runtimeType) { + return false; + } + final l$pageMeta = pageMeta; + final lOther$pageMeta = other.pageMeta; + if (l$pageMeta != lOther$pageMeta) { + return false; + } + final l$entries = entries; + final lOther$entries = other.entries; + if (l$entries.length != lOther$entries.length) { + return false; + } + for (int i = 0; i < l$entries.length; i++) { + final l$entries$entry = l$entries[i]; + final lOther$entries$entry = lOther$entries[i]; + if (l$entries$entry != lOther$entries$entry) { + return false; + } + } + final l$$__typename = $__typename; + final lOther$$__typename = other.$__typename; + if (l$$__typename != lOther$$__typename) { + return false; + } + return true; + } +} + +extension UtilityExtension$Query$Logs$logs$paginated + on Query$Logs$logs$paginated { + CopyWith$Query$Logs$logs$paginated get copyWith => + CopyWith$Query$Logs$logs$paginated( + this, + (i) => i, + ); +} + +abstract class CopyWith$Query$Logs$logs$paginated { + factory CopyWith$Query$Logs$logs$paginated( + Query$Logs$logs$paginated instance, + TRes Function(Query$Logs$logs$paginated) then, + ) = _CopyWithImpl$Query$Logs$logs$paginated; + + factory CopyWith$Query$Logs$logs$paginated.stub(TRes res) = + _CopyWithStubImpl$Query$Logs$logs$paginated; + + TRes call({ + Query$Logs$logs$paginated$pageMeta? pageMeta, + List? entries, + String? $__typename, + }); + CopyWith$Query$Logs$logs$paginated$pageMeta get pageMeta; + TRes entries( + Iterable Function( + Iterable>) + _fn); +} + +class _CopyWithImpl$Query$Logs$logs$paginated + implements CopyWith$Query$Logs$logs$paginated { + _CopyWithImpl$Query$Logs$logs$paginated( + this._instance, + this._then, + ); + + final Query$Logs$logs$paginated _instance; + + final TRes Function(Query$Logs$logs$paginated) _then; + + static const _undefined = {}; + + TRes call({ + Object? pageMeta = _undefined, + Object? entries = _undefined, + Object? $__typename = _undefined, + }) => + _then(Query$Logs$logs$paginated( + pageMeta: pageMeta == _undefined || pageMeta == null + ? _instance.pageMeta + : (pageMeta as Query$Logs$logs$paginated$pageMeta), + entries: entries == _undefined || entries == null + ? _instance.entries + : (entries as List), + $__typename: $__typename == _undefined || $__typename == null + ? _instance.$__typename + : ($__typename as String), + )); + + CopyWith$Query$Logs$logs$paginated$pageMeta get pageMeta { + final local$pageMeta = _instance.pageMeta; + return CopyWith$Query$Logs$logs$paginated$pageMeta( + local$pageMeta, (e) => call(pageMeta: e)); + } + + TRes entries( + Iterable Function( + Iterable>) + _fn) => + call( + entries: _fn(_instance.entries.map((e) => CopyWith$Fragment$LogEntry( + e, + (i) => i, + ))).toList()); +} + +class _CopyWithStubImpl$Query$Logs$logs$paginated + implements CopyWith$Query$Logs$logs$paginated { + _CopyWithStubImpl$Query$Logs$logs$paginated(this._res); + + TRes _res; + + call({ + Query$Logs$logs$paginated$pageMeta? pageMeta, + List? entries, + String? $__typename, + }) => + _res; + + CopyWith$Query$Logs$logs$paginated$pageMeta get pageMeta => + CopyWith$Query$Logs$logs$paginated$pageMeta.stub(_res); + + entries(_fn) => _res; +} + +class Query$Logs$logs$paginated$pageMeta { + Query$Logs$logs$paginated$pageMeta({ + this.upCursor, + this.downCursor, + this.$__typename = 'LogsPageMeta', + }); + + factory Query$Logs$logs$paginated$pageMeta.fromJson( + Map json) { + final l$upCursor = json['upCursor']; + final l$downCursor = json['downCursor']; + final l$$__typename = json['__typename']; + return Query$Logs$logs$paginated$pageMeta( + upCursor: (l$upCursor as String?), + downCursor: (l$downCursor as String?), + $__typename: (l$$__typename as String), + ); + } + + final String? upCursor; + + final String? downCursor; + + final String $__typename; + + Map toJson() { + final _resultData = {}; + final l$upCursor = upCursor; + _resultData['upCursor'] = l$upCursor; + final l$downCursor = downCursor; + _resultData['downCursor'] = l$downCursor; + final l$$__typename = $__typename; + _resultData['__typename'] = l$$__typename; + return _resultData; + } + + @override + int get hashCode { + final l$upCursor = upCursor; + final l$downCursor = downCursor; + final l$$__typename = $__typename; + return Object.hashAll([ + l$upCursor, + l$downCursor, + l$$__typename, + ]); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (!(other is Query$Logs$logs$paginated$pageMeta) || + runtimeType != other.runtimeType) { + return false; + } + final l$upCursor = upCursor; + final lOther$upCursor = other.upCursor; + if (l$upCursor != lOther$upCursor) { + return false; + } + final l$downCursor = downCursor; + final lOther$downCursor = other.downCursor; + if (l$downCursor != lOther$downCursor) { + return false; + } + final l$$__typename = $__typename; + final lOther$$__typename = other.$__typename; + if (l$$__typename != lOther$$__typename) { + return false; + } + return true; + } +} + +extension UtilityExtension$Query$Logs$logs$paginated$pageMeta + on Query$Logs$logs$paginated$pageMeta { + CopyWith$Query$Logs$logs$paginated$pageMeta< + Query$Logs$logs$paginated$pageMeta> + get copyWith => CopyWith$Query$Logs$logs$paginated$pageMeta( + this, + (i) => i, + ); +} + +abstract class CopyWith$Query$Logs$logs$paginated$pageMeta { + factory CopyWith$Query$Logs$logs$paginated$pageMeta( + Query$Logs$logs$paginated$pageMeta instance, + TRes Function(Query$Logs$logs$paginated$pageMeta) then, + ) = _CopyWithImpl$Query$Logs$logs$paginated$pageMeta; + + factory CopyWith$Query$Logs$logs$paginated$pageMeta.stub(TRes res) = + _CopyWithStubImpl$Query$Logs$logs$paginated$pageMeta; + + TRes call({ + String? upCursor, + String? downCursor, + String? $__typename, + }); +} + +class _CopyWithImpl$Query$Logs$logs$paginated$pageMeta + implements CopyWith$Query$Logs$logs$paginated$pageMeta { + _CopyWithImpl$Query$Logs$logs$paginated$pageMeta( + this._instance, + this._then, + ); + + final Query$Logs$logs$paginated$pageMeta _instance; + + final TRes Function(Query$Logs$logs$paginated$pageMeta) _then; + + static const _undefined = {}; + + TRes call({ + Object? upCursor = _undefined, + Object? downCursor = _undefined, + Object? $__typename = _undefined, + }) => + _then(Query$Logs$logs$paginated$pageMeta( + upCursor: + upCursor == _undefined ? _instance.upCursor : (upCursor as String?), + downCursor: downCursor == _undefined + ? _instance.downCursor + : (downCursor as String?), + $__typename: $__typename == _undefined || $__typename == null + ? _instance.$__typename + : ($__typename as String), + )); +} + +class _CopyWithStubImpl$Query$Logs$logs$paginated$pageMeta + implements CopyWith$Query$Logs$logs$paginated$pageMeta { + _CopyWithStubImpl$Query$Logs$logs$paginated$pageMeta(this._res); + + TRes _res; + + call({ + String? upCursor, + String? downCursor, + String? $__typename, + }) => + _res; +} + +class Subscription$LogEntries { + Subscription$LogEntries({ + required this.logEntries, + this.$__typename = 'Subscription', + }); + + factory Subscription$LogEntries.fromJson(Map json) { + final l$logEntries = json['logEntries']; + final l$$__typename = json['__typename']; + return Subscription$LogEntries( + logEntries: + Fragment$LogEntry.fromJson((l$logEntries as Map)), + $__typename: (l$$__typename as String), + ); + } + + final Fragment$LogEntry logEntries; + + final String $__typename; + + Map toJson() { + final _resultData = {}; + final l$logEntries = logEntries; + _resultData['logEntries'] = l$logEntries.toJson(); + final l$$__typename = $__typename; + _resultData['__typename'] = l$$__typename; + return _resultData; + } + + @override + int get hashCode { + final l$logEntries = logEntries; + final l$$__typename = $__typename; + return Object.hashAll([ + l$logEntries, + l$$__typename, + ]); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (!(other is Subscription$LogEntries) || + runtimeType != other.runtimeType) { + return false; + } + final l$logEntries = logEntries; + final lOther$logEntries = other.logEntries; + if (l$logEntries != lOther$logEntries) { + return false; + } + final l$$__typename = $__typename; + final lOther$$__typename = other.$__typename; + if (l$$__typename != lOther$$__typename) { + return false; + } + return true; + } +} + +extension UtilityExtension$Subscription$LogEntries on Subscription$LogEntries { + CopyWith$Subscription$LogEntries get copyWith => + CopyWith$Subscription$LogEntries( + this, + (i) => i, + ); +} + +abstract class CopyWith$Subscription$LogEntries { + factory CopyWith$Subscription$LogEntries( + Subscription$LogEntries instance, + TRes Function(Subscription$LogEntries) then, + ) = _CopyWithImpl$Subscription$LogEntries; + + factory CopyWith$Subscription$LogEntries.stub(TRes res) = + _CopyWithStubImpl$Subscription$LogEntries; + + TRes call({ + Fragment$LogEntry? logEntries, + String? $__typename, + }); + CopyWith$Fragment$LogEntry get logEntries; +} + +class _CopyWithImpl$Subscription$LogEntries + implements CopyWith$Subscription$LogEntries { + _CopyWithImpl$Subscription$LogEntries( + this._instance, + this._then, + ); + + final Subscription$LogEntries _instance; + + final TRes Function(Subscription$LogEntries) _then; + + static const _undefined = {}; + + TRes call({ + Object? logEntries = _undefined, + Object? $__typename = _undefined, + }) => + _then(Subscription$LogEntries( + logEntries: logEntries == _undefined || logEntries == null + ? _instance.logEntries + : (logEntries as Fragment$LogEntry), + $__typename: $__typename == _undefined || $__typename == null + ? _instance.$__typename + : ($__typename as String), + )); + + CopyWith$Fragment$LogEntry get logEntries { + final local$logEntries = _instance.logEntries; + return CopyWith$Fragment$LogEntry( + local$logEntries, (e) => call(logEntries: e)); + } +} + +class _CopyWithStubImpl$Subscription$LogEntries + implements CopyWith$Subscription$LogEntries { + _CopyWithStubImpl$Subscription$LogEntries(this._res); + + TRes _res; + + call({ + Fragment$LogEntry? logEntries, + String? $__typename, + }) => + _res; + + CopyWith$Fragment$LogEntry get logEntries => + CopyWith$Fragment$LogEntry.stub(_res); +} + +const documentNodeSubscriptionLogEntries = DocumentNode(definitions: [ + OperationDefinitionNode( + type: OperationType.subscription, + name: NameNode(value: 'LogEntries'), + variableDefinitions: [], + directives: [], + selectionSet: SelectionSetNode(selections: [ + FieldNode( + name: NameNode(value: 'logEntries'), + alias: null, + arguments: [], + directives: [], + selectionSet: SelectionSetNode(selections: [ + FragmentSpreadNode( + name: NameNode(value: 'LogEntry'), + directives: [], + ), + FieldNode( + name: NameNode(value: '__typename'), + alias: null, + arguments: [], + directives: [], + selectionSet: null, + ), + ]), + ), + FieldNode( + name: NameNode(value: '__typename'), + alias: null, + arguments: [], + directives: [], + selectionSet: null, + ), + ]), + ), + fragmentDefinitionLogEntry, +]); +Subscription$LogEntries _parserFn$Subscription$LogEntries( + Map data) => + Subscription$LogEntries.fromJson(data); + +class Options$Subscription$LogEntries + extends graphql.SubscriptionOptions { + Options$Subscription$LogEntries({ + String? operationName, + graphql.FetchPolicy? fetchPolicy, + graphql.ErrorPolicy? errorPolicy, + graphql.CacheRereadPolicy? cacheRereadPolicy, + Object? optimisticResult, + Subscription$LogEntries? typedOptimisticResult, + graphql.Context? context, + }) : super( + operationName: operationName, + fetchPolicy: fetchPolicy, + errorPolicy: errorPolicy, + cacheRereadPolicy: cacheRereadPolicy, + optimisticResult: optimisticResult ?? typedOptimisticResult?.toJson(), + context: context, + document: documentNodeSubscriptionLogEntries, + parserFn: _parserFn$Subscription$LogEntries, + ); +} + +class WatchOptions$Subscription$LogEntries + extends graphql.WatchQueryOptions { + WatchOptions$Subscription$LogEntries({ + String? operationName, + graphql.FetchPolicy? fetchPolicy, + graphql.ErrorPolicy? errorPolicy, + graphql.CacheRereadPolicy? cacheRereadPolicy, + Object? optimisticResult, + Subscription$LogEntries? typedOptimisticResult, + graphql.Context? context, + Duration? pollInterval, + bool? eagerlyFetchResults, + bool carryForwardDataOnException = true, + bool fetchResults = false, + }) : super( + operationName: operationName, + fetchPolicy: fetchPolicy, + errorPolicy: errorPolicy, + cacheRereadPolicy: cacheRereadPolicy, + optimisticResult: optimisticResult ?? typedOptimisticResult?.toJson(), + context: context, + document: documentNodeSubscriptionLogEntries, + pollInterval: pollInterval, + eagerlyFetchResults: eagerlyFetchResults, + carryForwardDataOnException: carryForwardDataOnException, + fetchResults: fetchResults, + parserFn: _parserFn$Subscription$LogEntries, + ); +} + +class FetchMoreOptions$Subscription$LogEntries + extends graphql.FetchMoreOptions { + FetchMoreOptions$Subscription$LogEntries( + {required graphql.UpdateQuery updateQuery}) + : super( + updateQuery: updateQuery, + document: documentNodeSubscriptionLogEntries, + ); +} + +extension ClientExtension$Subscription$LogEntries on graphql.GraphQLClient { + Stream> subscribe$LogEntries( + [Options$Subscription$LogEntries? options]) => + this.subscribe(options ?? Options$Subscription$LogEntries()); + graphql.ObservableQuery watchSubscription$LogEntries( + [WatchOptions$Subscription$LogEntries? options]) => + this.watchQuery(options ?? WatchOptions$Subscription$LogEntries()); +} diff --git a/lib/logic/api_maps/graphql_maps/server_api/logs_api.dart b/lib/logic/api_maps/graphql_maps/server_api/logs_api.dart new file mode 100644 index 00000000..2da7b091 --- /dev/null +++ b/lib/logic/api_maps/graphql_maps/server_api/logs_api.dart @@ -0,0 +1,53 @@ +part of 'server_api.dart'; + +mixin LogsApi on GraphQLApiMap { + Future<(List, ServerLogsPageMeta)> getServerLogs({ + required final int limit, + final String? upCursor, + final String? downCursor, + }) async { + QueryResult response; + List 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( + (final log) => ServerLogEntry.fromGraphQL(log), + ) + .toList() ?? + []; + pageMeta = ServerLogsPageMeta.fromGraphQL( + response.parsedData!.logs.paginated.pageMeta, + ); + } catch (e) { + print(e); + } + + return (logsList, pageMeta); + } + + Stream 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; + } + } +} diff --git a/lib/logic/api_maps/graphql_maps/server_api/server_api.dart b/lib/logic/api_maps/graphql_maps/server_api/server_api.dart index ac384c01..057fa5e8 100644 --- a/lib/logic/api_maps/graphql_maps/server_api/server_api.dart +++ b/lib/logic/api_maps/graphql_maps/server_api/server_api.dart @@ -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, diff --git a/lib/logic/bloc/server_logs/server_logs_bloc.dart b/lib/logic/bloc/server_logs/server_logs_bloc.dart new file mode 100644 index 00000000..f115ec5d --- /dev/null +++ b/lib/logic/bloc/server_logs/server_logs_bloc.dart @@ -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 { + ServerLogsBloc() : super(ServerLogsInitial()) { + on((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().api.getServerLogsStream().listen( + (final ServerLogEntry logEntry) { + print('Got new log entry'); + print(logEntry); + add(ServerLogsGotNewEntry(logEntry)); + }, + ); + } catch (e) { + emit(ServerLogsError(e.toString())); + } + }); + + on((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((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((final event, final emit) { + _apiLogsSubscription?.cancel(); + emit(ServerLogsInitial()); + }); + } + + Future<(List, 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().api.getServerLogs( + limit: limit, + upCursor: upCursor, + downCursor: downCursor, + ); + + @override + Future close() { + _apiLogsSubscription?.cancel(); + return super.close(); + } + + @override + void onChange(final Change change) { + super.onChange(change); + } + + StreamSubscription? _apiLogsSubscription; +} diff --git a/lib/logic/bloc/server_logs/server_logs_event.dart b/lib/logic/bloc/server_logs/server_logs_event.dart new file mode 100644 index 00000000..cf1f9127 --- /dev/null +++ b/lib/logic/bloc/server_logs/server_logs_event.dart @@ -0,0 +1,29 @@ +part of 'server_logs_bloc.dart'; + +sealed class ServerLogsEvent extends Equatable { + const ServerLogsEvent(); +} + +final class ServerLogsFetch extends ServerLogsEvent { + @override + List get props => []; +} + +final class ServerLogsFetchMore extends ServerLogsEvent { + @override + List get props => []; +} + +final class ServerLogsGotNewEntry extends ServerLogsEvent { + const ServerLogsGotNewEntry(this.entry); + + final ServerLogEntry entry; + + @override + List get props => [entry]; +} + +final class ServerLogsDisconnect extends ServerLogsEvent { + @override + List get props => []; +} diff --git a/lib/logic/bloc/server_logs/server_logs_state.dart b/lib/logic/bloc/server_logs/server_logs_state.dart new file mode 100644 index 00000000..66d28314 --- /dev/null +++ b/lib/logic/bloc/server_logs/server_logs_state.dart @@ -0,0 +1,51 @@ +part of 'server_logs_bloc.dart'; + +sealed class ServerLogsState extends Equatable { + const ServerLogsState(); +} + +final class ServerLogsInitial extends ServerLogsState { + @override + List get props => []; +} + +final class ServerLogsLoading extends ServerLogsState { + @override + List get props => []; +} + +final class ServerLogsLoaded extends ServerLogsState { + const ServerLogsLoaded(this.entries, this.meta, this.loadingMore); + + final List entries; + final ServerLogsPageMeta meta; + final bool loadingMore; + + List get systemdUnits => entries + .map((final entry) => entry.systemdUnit ?? 'kernel') + .toSet() + .toList(); + + (List, 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 get props => [entries, meta]; +} + +final class ServerLogsError extends ServerLogsState { + const ServerLogsError(this.error); + + final Object error; + + @override + List get props => [error]; +} diff --git a/lib/logic/models/server_logs.dart b/lib/logic/models/server_logs.dart new file mode 100644 index 00000000..1ff2dbaf --- /dev/null +++ b/lib/logic/models/server_logs.dart @@ -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 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 get props => [ + downCursor, + upCursor, + ]; +} diff --git a/lib/ui/pages/more/console/console_page.dart b/lib/ui/pages/more/console/console_page.dart index 9fa28325..dca7ff41 100644 --- a/lib/ui/pages/more/console/console_page.dart +++ b/lib/ui/pages/more/console/console_page.dart @@ -54,9 +54,7 @@ class _ConsolePageState extends State { 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, ), diff --git a/lib/ui/pages/server_details/logs/logs_screen.dart b/lib/ui/pages/server_details/logs/logs_screen.dart new file mode 100644 index 00000000..f8f6a1ac --- /dev/null +++ b/lib/ui/pages/server_details/logs/logs_screen.dart @@ -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 createState() => _ServerLogsScreenState(); +} + +class _ServerLogsScreenState extends State { + final ScrollController _scrollController = ScrollController(); + late ServerLogsBloc _serverLogsBloc; + + String? _selectedSystemdUnit; + + @override + void initState() { + super.initState(); + _serverLogsBloc = BlocProvider.of(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 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( + builder: (final context, final state) { + if (state is ServerLogsLoaded) { + return _buildDrawer(state.systemdUnits); + } + return const SizedBox.shrink(); + }, + ), + body: BlocBuilder( + 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( + 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( + 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( + 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), + ), + ); +} diff --git a/lib/ui/pages/server_details/server_details_screen.dart b/lib/ui/pages/server_details/server_details_screen.dart index 6a57e318..49543991 100644 --- a/lib/ui/pages/server_details/server_details_screen.dart +++ b/lib/ui/pages/server_details/server_details_screen.dart @@ -80,6 +80,11 @@ class _ServerDetailsScreenState extends State 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(), diff --git a/lib/ui/router/router.dart b/lib/ui/router/router.dart index 96eec87f..ead07b3a 100644 --- a/lib/ui/router/router.dart +++ b/lib/ui/router/router.dart @@ -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': diff --git a/lib/ui/router/router.gr.dart b/lib/ui/router/router.gr.dart index d45ecec8..4fcf7c7c 100644 --- a/lib/ui/router/router.gr.dart +++ b/lib/ui/router/router.gr.dart @@ -132,6 +132,12 @@ abstract class _$RootRouter extends RootStackRouter { child: const ServerDetailsScreen(), ); }, + ServerLogsRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const ServerLogsScreen(), + ); + }, ServerSettingsRoute.name: (routeData) { return AutoRoutePage( routeData: routeData, @@ -510,6 +516,20 @@ class ServerDetailsRoute extends PageRouteInfo { static const PageInfo page = PageInfo(name); } +/// generated route for +/// [ServerLogsScreen] +class ServerLogsRoute extends PageRouteInfo { + const ServerLogsRoute({List? children}) + : super( + ServerLogsRoute.name, + initialChildren: children, + ); + + static const String name = 'ServerLogsRoute'; + + static const PageInfo page = PageInfo(name); +} + /// generated route for /// [ServerSettingsScreen] class ServerSettingsRoute extends PageRouteInfo {