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