]> git.proxmox.com Git - flutter/pve_flutter_frontend.git/blob - lib/widgets/pve_task_log_widget.dart
tree-wide: prefer sized box for whitespace
[flutter/pve_flutter_frontend.git] / lib / widgets / pve_task_log_widget.dart
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';
10
11 class PveTaskLog extends StatefulWidget {
12 const PveTaskLog({super.key});
13
14 @override
15 _PveTaskLogState createState() => _PveTaskLogState();
16 }
17
18 class _PveTaskLogState extends State<PveTaskLog> {
19 final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
20 late TextEditingController _userFilterController;
21 late TextEditingController _typeFilterController;
22 @override
23 void initState() {
24 super.initState();
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 ?? ''));
31 }
32
33 @override
34 Widget build(BuildContext context) {
35 final bloc = Provider.of<PveTaskLogBloc>(context);
36 return ProxmoxStreamBuilder<PveTaskLogBloc, PveTaskLogState>(
37 bloc: bloc,
38 builder: (context, state) {
39 return SafeArea(
40 child: Scaffold(
41 key: _scaffoldKey,
42 appBar: AppBar(
43 leading: IconButton(
44 icon: const Icon(Icons.close),
45 onPressed: () => Navigator.of(context).pop(),
46 ),
47 actions: <Widget>[
48 IconButton(
49 icon: const Icon(Icons.more_vert),
50 onPressed: () => _scaffoldKey.currentState?.openEndDrawer(),
51 )
52 ],
53 ),
54 endDrawer: Drawer(
55 child: Padding(
56 padding: const EdgeInsets.fromLTRB(16.0, 20.0, 16.0, 0),
57 child: Column(
58 crossAxisAlignment: CrossAxisAlignment.start,
59 children: <Widget>[
60 Text(
61 'Filters',
62 style: Theme.of(context).textTheme.headlineSmall,
63 ),
64 const SizedBox(
65 height: 20,
66 ),
67 TextFormField(
68 decoration: const InputDecoration(
69 labelText: 'by user',
70 filled: true,
71 prefixIcon: Icon(Icons.person)),
72 onChanged: (newValue) {
73 bloc.events.add(FilterTasksByUser(newValue));
74 bloc.events.add(LoadTasks());
75 },
76 controller: _userFilterController,
77 ),
78 const SizedBox(
79 height: 20,
80 ),
81 TextFormField(
82 decoration: const InputDecoration(
83 labelText: 'by type',
84 filled: true,
85 prefixIcon: Icon(Icons.description)),
86 onChanged: (newValue) {
87 bloc.events.add(FilterTasksByType(newValue));
88 bloc.events.add(LoadTasks());
89 },
90 controller: _typeFilterController,
91 ),
92 const SizedBox(
93 height: 20,
94 ),
95 DropdownButtonFormField<String>(
96 decoration: const InputDecoration(labelText: 'Source'),
97 value: state.source,
98 icon: const Icon(Icons.arrow_downward),
99 iconSize: 24,
100 elevation: 16,
101 onChanged: (String? newValue) {
102 bloc.events.add(FilterTasksBySource(newValue));
103 bloc.events.add(LoadTasks());
104 },
105 items: <String>[
106 'all',
107 'active',
108 'archive',
109 ].map<DropdownMenuItem<String>>((String value) {
110 return DropdownMenuItem<String>(
111 value: value,
112 child: Container(child: Text(value)),
113 );
114 }).toList(),
115 ),
116 const SizedBox(
117 height: 20,
118 ),
119 FormField(
120 builder: (FormFieldState<bool> formFieldState) => Row(
121 mainAxisAlignment: MainAxisAlignment.spaceBetween,
122 children: <Widget>[
123 const Text("Only errors"),
124 Checkbox(
125 value: state.onlyErrors,
126 onChanged: (value) {
127 formFieldState.didChange(value);
128 bloc.events.add(FilterTasksByError());
129 bloc.events.add(LoadTasks());
130 },
131 ),
132 ],
133 ),
134 )
135 ],
136 ),
137 ),
138 ),
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());
145 }
146 }
147 return false;
148 },
149 child: state.tasks.isNotEmpty
150 ? ListView.builder(
151 itemCount: state.tasks.length,
152 itemBuilder: (context, index) => PveTaskExpansionTile(
153 task: state.tasks[index],
154 ),
155 )
156 : const Center(
157 child: Text("No tasks found"),
158 ),
159 ),
160 ),
161 );
162
163 return Container();
164 });
165 }
166 }
167
168 class PveTaskLogScrollView extends StatefulWidget {
169 final Widget icon;
170 final Widget jobTitle;
171
172 const PveTaskLogScrollView({
173 super.key,
174 required this.icon,
175 required this.jobTitle,
176 });
177 @override
178 _PveTaskLogScrollViewState createState() => _PveTaskLogScrollViewState();
179 }
180
181 class _PveTaskLogScrollViewState extends State<PveTaskLogScrollView> {
182 ScrollController _scrollController = ScrollController();
183 @override
184 void initState() {
185 super.initState();
186 if (mounted) {
187 _scrollToBottom();
188 }
189 }
190
191 @override
192 Widget build(BuildContext context) {
193 return StreamListener<PveTaskLogViewerState>(
194 stream: Provider.of<PveTaskLogViewerBloc>(context).state.distinct(),
195 onStateChange: (newState) {
196 if (_scrollController.hasClients) {
197 _scrollToBottom();
198 }
199 },
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;
208 }
209 return SizedBox(
210 height: MediaQuery.of(context).size.height * 0.5,
211 child: Column(
212 crossAxisAlignment: CrossAxisAlignment.start,
213 children: <Widget>[
214 if (state.isBlank)
215 const Align(
216 alignment: Alignment.center,
217 child: Padding(
218 padding: EdgeInsets.symmetric(vertical: 8.0),
219 child: Text("Loading log data.."),
220 ),
221 ),
222 if (!state.isBlank) ...[
223 Padding(
224 padding: const EdgeInsets.fromLTRB(0, 5, 0, 5),
225 child: Align(
226 alignment: Alignment.topCenter,
227 child: Container(
228 width: 40,
229 height: 3,
230 color: Colors.black,
231 ),
232 ),
233 ),
234 if (state.status != null)
235 ListTile(
236 leading: widget.icon,
237 title: AnimatedDefaultTextStyle(
238 style: Theme.of(context)
239 .textTheme
240 .titleMedium!
241 .copyWith(fontWeight: FontWeight.bold),
242 duration: kThemeChangeDuration,
243 child: widget.jobTitle,
244 ),
245 trailing: Chip(
246 label: Text(
247 state.status!.status.name,
248 style: TextStyle(color: indicatorColor),
249 ),
250 backgroundColor: statusChipColor,
251 ),
252 ),
253 const Divider(),
254 if (state.log != null)
255 Expanded(
256 child: Padding(
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;
264 final errorLine =
265 log[index].lineText?.contains('ERROR') ??
266 false;
267 final warningLine =
268 log[index].lineText?.contains('WARNING') ??
269 false;
270 return Card(
271 color: isLast || errorLine
272 ? indicatorColor
273 : Colors.white,
274 child: Padding(
275 padding: const EdgeInsets.all(5.0),
276 child: Text(
277 log[index].lineText ?? '<unknown>',
278 style: TextStyle(
279 color: isLast || errorLine
280 ? Colors.white
281 : Colors.black,
282 ),
283 ),
284 ),
285 );
286 },
287 ),
288 ),
289 ),
290 ]
291 ],
292 ),
293 );
294 }),
295 );
296 }
297
298 void _scrollToBottom() {
299 if (_scrollController.hasClients) {
300 _scrollController.animateTo(_scrollController.position.maxScrollExtent,
301 duration: const Duration(milliseconds: 500), curve: Curves.easeOut);
302 }
303 }
304 }