1 import 'package:flutter/material.dart';
2 import 'package:provider/provider.dart';
3 import 'package:pve_flutter_frontend/bloc/pve_task_log_bloc.dart';
4 import 'package:pve_flutter_frontend/bloc/pve_task_log_viewer_bloc.dart';
5 import 'package:pve_flutter_frontend/states/pve_task_log_state.dart';
6 import 'package:pve_flutter_frontend/states/pve_task_log_viewer_state.dart';
7 import 'package:pve_flutter_frontend/widgets/proxmox_stream_builder_widget.dart';
8 import 'package:pve_flutter_frontend/widgets/proxmox_stream_listener.dart';
9 import 'package:pve_flutter_frontend/widgets/pve_task_log_expansiontile_widget.dart';
11 class PveTaskLog extends StatefulWidget {
12 const PveTaskLog({super.key});
15 State<PveTaskLog> createState() => _PveTaskLogState();
18 class _PveTaskLogState extends State<PveTaskLog> {
19 final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
20 late TextEditingController _userFilterController;
21 late TextEditingController _typeFilterController;
25 final bloc = Provider.of<PveTaskLogBloc>(context, listen: false);
26 final PveTaskLogState state = bloc.latestState;
27 _userFilterController = TextEditingController.fromValue(
28 TextEditingValue(text: state.userFilter ?? ''));
29 _typeFilterController = TextEditingController.fromValue(
30 TextEditingValue(text: state.typeFilter ?? ''));
34 Widget build(BuildContext context) {
35 final bloc = Provider.of<PveTaskLogBloc>(context);
36 return ProxmoxStreamBuilder<PveTaskLogBloc, PveTaskLogState>(
38 builder: (context, state) {
44 icon: const Icon(Icons.close),
45 onPressed: () => Navigator.of(context).pop(),
49 icon: const Icon(Icons.more_vert),
50 onPressed: () => _scaffoldKey.currentState?.openEndDrawer(),
56 padding: const EdgeInsets.fromLTRB(16.0, 20.0, 16.0, 0),
58 crossAxisAlignment: CrossAxisAlignment.start,
62 style: Theme.of(context).textTheme.headlineSmall,
68 decoration: const InputDecoration(
71 prefixIcon: Icon(Icons.person)),
72 onChanged: (newValue) {
73 bloc.events.add(FilterTasksByUser(newValue));
74 bloc.events.add(LoadTasks());
76 controller: _userFilterController,
82 decoration: const InputDecoration(
85 prefixIcon: Icon(Icons.description)),
86 onChanged: (newValue) {
87 bloc.events.add(FilterTasksByType(newValue));
88 bloc.events.add(LoadTasks());
90 controller: _typeFilterController,
95 DropdownButtonFormField<String>(
96 decoration: const InputDecoration(labelText: 'Source'),
98 icon: const Icon(Icons.arrow_downward),
101 onChanged: (String? newValue) {
102 bloc.events.add(FilterTasksBySource(newValue));
103 bloc.events.add(LoadTasks());
109 ].map<DropdownMenuItem<String>>((String value) {
110 return DropdownMenuItem<String>(
112 child: Container(child: Text(value)),
120 builder: (FormFieldState<bool> formFieldState) => Row(
121 mainAxisAlignment: MainAxisAlignment.spaceBetween,
123 const Text("Only errors"),
125 value: state.onlyErrors,
127 formFieldState.didChange(value);
128 bloc.events.add(FilterTasksByError());
129 bloc.events.add(LoadTasks());
139 body: NotificationListener<ScrollNotification>(
140 onNotification: (ScrollNotification scrollInfo) {
141 if (scrollInfo.metrics.pixels >=
142 (0.8 * scrollInfo.metrics.maxScrollExtent)) {
143 if (!state.isLoading) {
144 bloc.events.add(LoadMoreTasks());
149 child: state.tasks.isNotEmpty
151 itemCount: state.tasks.length,
152 itemBuilder: (context, index) => PveTaskExpansionTile(
153 task: state.tasks[index],
157 child: Text("No tasks found"),
168 class PveTaskLogScrollView extends StatefulWidget {
170 final Widget jobTitle;
172 const PveTaskLogScrollView({
175 required this.jobTitle,
178 State<PveTaskLogScrollView> createState() => _PveTaskLogScrollViewState();
181 class _PveTaskLogScrollViewState extends State<PveTaskLogScrollView> {
182 ScrollController _scrollController = ScrollController();
192 Widget build(BuildContext context) {
193 return StreamListener<PveTaskLogViewerState>(
194 stream: Provider.of<PveTaskLogViewerBloc>(context).state.distinct(),
195 onStateChange: (newState) {
196 if (_scrollController.hasClients) {
200 child: ProxmoxStreamBuilder<PveTaskLogViewerBloc, PveTaskLogViewerState>(
201 bloc: Provider.of<PveTaskLogViewerBloc>(context),
202 builder: (context, state) {
203 var indicatorColor = Colors.teal.shade500;
204 var statusChipColor = Colors.teal.shade100;
205 if (state.status?.failed ?? false) {
206 indicatorColor = Colors.red;
207 statusChipColor = Colors.red.shade100;
210 height: MediaQuery.of(context).size.height * 0.5,
212 crossAxisAlignment: CrossAxisAlignment.start,
216 alignment: Alignment.center,
218 padding: EdgeInsets.symmetric(vertical: 8.0),
219 child: Text("Loading log data.."),
222 if (!state.isBlank) ...[
224 padding: const EdgeInsets.fromLTRB(0, 5, 0, 5),
226 alignment: Alignment.topCenter,
234 if (state.status != null)
236 leading: widget.icon,
237 title: AnimatedDefaultTextStyle(
238 style: Theme.of(context)
241 .copyWith(fontWeight: FontWeight.bold),
242 duration: kThemeChangeDuration,
243 child: widget.jobTitle,
247 state.status!.status.name,
248 style: TextStyle(color: indicatorColor),
250 backgroundColor: statusChipColor,
254 if (state.log != null)
257 padding: const EdgeInsets.all(14.0),
258 child: ListView.builder(
259 controller: _scrollController,
260 itemCount: state.log!.lines!.length,
261 itemBuilder: (context, index) {
262 final log = state.log!.lines!;
263 final isLast = index == log.length - 1;
265 log[index].lineText?.contains('ERROR') ??
268 log[index].lineText?.contains('WARNING') ??
271 color: isLast || errorLine
275 padding: const EdgeInsets.all(5.0),
277 log[index].lineText ?? '<unknown>',
279 color: isLast || errorLine
298 void _scrollToBottom() {
299 if (_scrollController.hasClients) {
300 _scrollController.animateTo(_scrollController.position.maxScrollExtent,
301 duration: const Duration(milliseconds: 500), curve: Curves.easeOut);