]>
Commit | Line | Data |
---|---|---|
3628aaea | 1 | import 'package:built_collection/built_collection.dart'; |
00a48038 | 2 | import 'package:flutter/material.dart'; |
3628aaea TM |
3 | import 'package:flutter/rendering.dart'; |
4 | import 'package:font_awesome_flutter/font_awesome_flutter.dart'; | |
b6823e19 | 5 | import 'package:intl/intl.dart'; |
3628aaea TM |
6 | import 'package:provider/provider.dart'; |
7 | import 'package:proxmox_dart_api_client/proxmox_dart_api_client.dart'; | |
5ad24bd7 | 8 | import 'package:pve_flutter_frontend/bloc/pve_access_management_bloc.dart'; |
3628aaea TM |
9 | import 'package:pve_flutter_frontend/bloc/pve_authentication_bloc.dart'; |
10 | import 'package:pve_flutter_frontend/bloc/pve_cluster_status_bloc.dart'; | |
11 | import 'package:pve_flutter_frontend/bloc/pve_file_selector_bloc.dart'; | |
12 | import 'package:pve_flutter_frontend/bloc/pve_resource_bloc.dart'; | |
13 | import 'package:pve_flutter_frontend/bloc/pve_storage_selector_bloc.dart'; | |
5ad24bd7 | 14 | import 'package:pve_flutter_frontend/states/pve_access_management_state.dart'; |
3628aaea TM |
15 | import 'package:pve_flutter_frontend/states/pve_cluster_status_state.dart'; |
16 | import 'package:pve_flutter_frontend/states/pve_file_selector_state.dart'; | |
17 | import 'package:pve_flutter_frontend/states/pve_resource_state.dart'; | |
18 | import 'package:pve_flutter_frontend/states/pve_storage_selector_state.dart'; | |
19 | import 'package:pve_flutter_frontend/utils/renderers.dart'; | |
3628aaea | 20 | import 'package:pve_flutter_frontend/widgets/proxmox_capacity_indicator.dart'; |
bbc12f2a TM |
21 | import 'package:pve_flutter_frontend/widgets/proxmox_gauge_chart.dart'; |
22 | import 'package:pve_flutter_frontend/widgets/proxmox_heartbeat_indicator.dart'; | |
3628aaea TM |
23 | import 'package:pve_flutter_frontend/widgets/proxmox_stream_builder_widget.dart'; |
24 | import 'package:pve_flutter_frontend/widgets/pve_file_selector_widget.dart'; | |
25 | import 'package:pve_flutter_frontend/widgets/pve_help_icon_button_widget.dart'; | |
c96ce032 | 26 | import 'package:pve_flutter_frontend/widgets/pve_resource_data_card_widget.dart'; |
3628aaea | 27 | import 'package:pve_flutter_frontend/widgets/pve_resource_status_chip_widget.dart'; |
bbc12f2a | 28 | import 'package:pve_flutter_frontend/widgets/pve_subscription_alert_dialog.dart'; |
3628aaea | 29 | import 'package:rxdart/rxdart.dart'; |
00a48038 | 30 | |
3628aaea TM |
31 | class MainLayoutSlim extends StatefulWidget { |
32 | @override | |
33 | _MainLayoutSlimState createState() => _MainLayoutSlimState(); | |
34 | } | |
35 | ||
36 | class _MainLayoutSlimState extends State<MainLayoutSlim> { | |
37 | BehaviorSubject<int> pageSelector = BehaviorSubject.seeded(0); | |
00a48038 TM |
38 | @override |
39 | Widget build(BuildContext context) { | |
3628aaea | 40 | final apiClient = Provider.of<ProxmoxApiClient>(context); |
5f79edf2 TM |
41 | return MultiProvider( |
42 | providers: [ | |
43 | Provider.value( | |
44 | value: pageSelector, | |
45 | ), | |
46 | Provider<PveResourceBloc>( | |
47 | create: (context) => PveResourceBloc( | |
48 | apiClient: apiClient, | |
49 | init: PveResourceState.init().rebuild( | |
50 | (b) => b..typeFilter.replace({'qemu', 'lxc', 'storage'}), | |
51 | ), | |
52 | )..events.add(PollResources()), | |
53 | dispose: (context, bloc) => bloc.dispose(), | |
54 | ), | |
55 | Provider<PveAccessManagementBloc>( | |
56 | create: (context) => PveAccessManagementBloc( | |
57 | apiClient: apiClient, | |
58 | init: | |
59 | PveAccessManagementState.init(apiClient.credentials.username)) | |
60 | ..events.add(LoadUsers()), | |
61 | dispose: (context, bloc) => bloc.dispose(), | |
62 | ) | |
63 | ], | |
d9e05b58 TM |
64 | child: WillPopScope( |
65 | onWillPop: () async { | |
66 | if (pageSelector.value != 0) { | |
67 | pageSelector.add(0); | |
68 | return false; | |
3628aaea | 69 | } |
d9e05b58 | 70 | return true; |
3628aaea | 71 | }, |
d9e05b58 TM |
72 | child: StreamBuilder<int>( |
73 | stream: pageSelector.stream, | |
74 | initialData: pageSelector.value, | |
75 | builder: (context, snapshot) { | |
76 | if (snapshot.hasData) { | |
77 | switch (snapshot.data) { | |
78 | case 0: | |
79 | return MobileDashboard(); | |
80 | break; | |
81 | case 1: | |
5f79edf2 | 82 | return MobileResourceOverview(); |
d9e05b58 TM |
83 | break; |
84 | case 2: | |
5f79edf2 | 85 | return MobileAccessManagement(); |
d9e05b58 TM |
86 | break; |
87 | default: | |
88 | } | |
89 | } | |
90 | return Container(); | |
91 | }, | |
92 | ), | |
3628aaea TM |
93 | ), |
94 | ); | |
95 | } | |
96 | ||
97 | @override | |
98 | dispose() { | |
99 | pageSelector.close(); | |
100 | super.dispose(); | |
101 | } | |
102 | } | |
103 | ||
104 | class PveMobileBottomNavigationbar extends StatelessWidget { | |
105 | @override | |
106 | Widget build(BuildContext context) { | |
107 | final pageSelector = Provider.of<BehaviorSubject<int>>(context); | |
108 | return BottomNavigationBar( | |
109 | backgroundColor: Colors.white, | |
110 | items: [ | |
111 | BottomNavigationBarItem( | |
5ad24bd7 TM |
112 | icon: Icon( |
113 | Icons.dashboard, | |
114 | ), | |
453d9410 | 115 | label: "Dashboard", |
5ad24bd7 | 116 | ), |
3628aaea | 117 | BottomNavigationBarItem( |
5ad24bd7 TM |
118 | icon: Icon( |
119 | Icons.developer_board, | |
120 | ), | |
453d9410 | 121 | label: "Resources", |
5ad24bd7 TM |
122 | ), |
123 | BottomNavigationBarItem( | |
124 | icon: Icon( | |
125 | Icons.supervised_user_circle, | |
126 | ), | |
453d9410 | 127 | label: "Access", |
5ad24bd7 | 128 | ), |
3628aaea TM |
129 | ], |
130 | currentIndex: pageSelector.value, | |
131 | onTap: (index) => pageSelector.add(index)); | |
132 | } | |
133 | } | |
134 | ||
135 | class MobileDashboard extends StatelessWidget { | |
136 | @override | |
137 | Widget build(BuildContext context) { | |
138 | final cBloc = Provider.of<PveClusterStatusBloc>(context); | |
35e7d210 | 139 | final rBloc = Provider.of<PveResourceBloc>(context); |
00a48038 | 140 | return Scaffold( |
bbc12f2a TM |
141 | appBar: AppBar( |
142 | title: Text( | |
143 | 'PROXMOX', | |
144 | style: TextStyle( | |
145 | fontFamily: 'Proxmox', | |
146 | fontSize: 28, | |
147 | ), | |
148 | ), | |
149 | elevation: 0.0, | |
150 | leading: Padding( | |
151 | padding: const EdgeInsets.all(12.0), | |
152 | child: Image.asset( | |
153 | 'assets/images/proxmox_logo_icon_white.png', | |
154 | ), | |
155 | ), | |
156 | automaticallyImplyLeading: false, | |
157 | actions: <Widget>[ | |
35b1eb44 TM |
158 | PveHelpIconButton( |
159 | baseUrl: Provider.of<PveResourceBloc>(context) | |
160 | .apiClient | |
161 | .credentials | |
162 | .apiBaseUrl, | |
163 | docPath: 'index.html'), | |
bbc12f2a TM |
164 | ], |
165 | ), | |
166 | body: Stack(children: [ | |
167 | Container( | |
168 | height: 350, | |
169 | color: Theme.of(context).primaryColor, | |
170 | ), | |
171 | ProxmoxStreamBuilder<PveClusterStatusBloc, PveClusterStatusState>( | |
172 | bloc: cBloc, | |
173 | builder: (context, cState) { | |
174 | return ListView(children: <Widget>[ | |
175 | if (cState.cluster != null) ...[ | |
176 | ListTile( | |
177 | title: Text( | |
178 | "Status", | |
179 | style: TextStyle( | |
180 | fontWeight: FontWeight.bold, | |
181 | color: Colors.white, | |
182 | ), | |
183 | ), | |
184 | subtitle: Text( | |
185 | cState.cluster?.name ?? "Datacenter", | |
186 | style: TextStyle( | |
187 | color: Colors.white, | |
188 | ), | |
189 | ), | |
190 | trailing: Container( | |
191 | width: 96, | |
192 | height: 48, | |
193 | child: ProxmoxHeartbeatIndicator( | |
194 | isHealthy: cState.healthy, | |
195 | healthyColor: Colors.greenAccent, | |
196 | warningColor: Colors.orangeAccent, | |
197 | ), | |
198 | ), | |
199 | ), | |
200 | ], | |
201 | Container( | |
202 | height: 80, | |
203 | child: ListView( | |
204 | padding: EdgeInsets.symmetric(horizontal: 8), | |
205 | shrinkWrap: true, | |
206 | scrollDirection: Axis.horizontal, | |
207 | children: [ | |
208 | if (cState.missingSubscription) | |
209 | ActionChip( | |
210 | backgroundColor: Color(0xE6003752), | |
211 | avatar: Icon(Icons.report, color: Colors.red), | |
212 | label: Text( | |
213 | 'Subscription', | |
214 | style: TextStyle( | |
215 | fontWeight: FontWeight.bold, | |
216 | color: Colors.white), | |
217 | ), | |
218 | onPressed: () => showDialog( | |
219 | context: context, | |
220 | builder: (c) => PveSubscriptionAlertDialog(), | |
221 | ), | |
222 | ), | |
223 | Padding( | |
224 | padding: const EdgeInsets.symmetric(horizontal: 8.0), | |
225 | child: ActionChip( | |
226 | backgroundColor: Color(0xE6003752), | |
227 | avatar: Icon( | |
228 | Renderers.getDefaultResourceIcon('qemu'), | |
229 | color: Colors.black, | |
230 | size: 20, | |
231 | ), | |
232 | label: Text( | |
233 | 'Virtual Machines', | |
234 | style: TextStyle( | |
235 | fontWeight: FontWeight.bold, | |
236 | color: Colors.white), | |
237 | ), | |
238 | onPressed: () => | |
239 | Provider.of<BehaviorSubject<int>>(context).add(1), | |
240 | ), | |
241 | ), | |
242 | Padding( | |
243 | padding: const EdgeInsets.symmetric(horizontal: 8.0), | |
244 | child: ActionChip( | |
bbc12f2a TM |
245 | labelPadding: EdgeInsets.symmetric(horizontal: 8), |
246 | backgroundColor: Color(0xE6003752), | |
247 | avatar: Icon( | |
248 | Renderers.getDefaultResourceIcon('lxc'), | |
249 | color: Colors.black, | |
250 | size: 20, | |
251 | ), | |
252 | label: Text( | |
253 | 'Linux Containers', | |
254 | style: TextStyle( | |
255 | fontWeight: FontWeight.bold, | |
256 | color: Colors.white), | |
257 | ), | |
258 | onPressed: () => | |
259 | Provider.of<BehaviorSubject<int>>(context).add(1), | |
260 | ), | |
261 | ), | |
262 | ], | |
263 | ), | |
264 | ), | |
265 | ProxmoxStreamBuilder<PveResourceBloc, PveResourceState>( | |
35e7d210 TM |
266 | bloc: rBloc, |
267 | builder: (context, rState) { | |
bbc12f2a TM |
268 | final nodes = rState.nodes; |
269 | var aggrCpus = 0.0; | |
270 | var aggrCpuUsage = 0.0; | |
271 | var aggrMemUsage = 0.0; | |
272 | var aggrMem = 0.0; | |
273 | nodes.forEach((element) { | |
274 | aggrCpuUsage += | |
275 | (element.cpu ?? 0) * (element.maxcpu ?? 0); | |
276 | aggrCpus += element.maxcpu ?? 0; | |
277 | aggrMemUsage += element.mem ?? 0; | |
278 | aggrMem += element.maxmem ?? 0; | |
35e7d210 | 279 | }); |
bbc12f2a TM |
280 | final cpuUsagePercent = |
281 | ((aggrCpuUsage / aggrCpus) * 100).toStringAsFixed(2); | |
282 | final memUsagePercent = | |
283 | ((aggrMemUsage / aggrMem) * 100).toStringAsFixed(2); | |
284 | return PveResourceDataCardWidget( | |
285 | title: Text( | |
286 | 'Analytics', | |
287 | style: TextStyle( | |
288 | fontWeight: FontWeight.bold, | |
289 | fontSize: 20, | |
290 | ), | |
291 | ), | |
292 | subtitle: Text('Usage across all online nodes'), | |
293 | children: [ | |
35e7d210 | 294 | Padding( |
bbc12f2a TM |
295 | padding: const EdgeInsets.symmetric(vertical: 16.0), |
296 | child: ProxmoxGaugeChartListTile( | |
297 | title: Text('CPU'), | |
298 | subtitle: | |
299 | Text('$aggrCpus Cores ${nodes.length} Nodes'), | |
300 | legend: Text('$cpuUsagePercent %'), | |
301 | value: aggrCpuUsage, | |
302 | maxValue: aggrCpus, | |
303 | ), | |
304 | ), | |
305 | Padding( | |
306 | padding: const EdgeInsets.symmetric(vertical: 16.0), | |
307 | child: ProxmoxGaugeChartListTile( | |
308 | title: Text('Memory'), | |
309 | subtitle: Text( | |
310 | '${Renderers.formatSize(aggrMemUsage)} of ${Renderers.formatSize(aggrMem)}'), | |
311 | legend: Text('$memUsagePercent %'), | |
312 | value: aggrMemUsage, | |
313 | maxValue: aggrMem, | |
35e7d210 TM |
314 | ), |
315 | ), | |
3628aaea | 316 | ], |
35e7d210 TM |
317 | ); |
318 | }), | |
bbc12f2a TM |
319 | PveResourceDataCardWidget( |
320 | title: Text( | |
321 | 'Nodes', | |
322 | style: TextStyle( | |
323 | fontWeight: FontWeight.bold, | |
324 | fontSize: 20, | |
3628aaea | 325 | ), |
bbc12f2a TM |
326 | ), |
327 | children: [ | |
328 | ...cState.nodes.map((node) { | |
329 | return PveNodeListTile( | |
330 | name: node.name, | |
331 | online: node.online, | |
332 | type: node.type, | |
333 | level: node.level, | |
334 | ip: node.ip, | |
335 | ); | |
336 | }), | |
337 | ], | |
338 | ), | |
339 | ProxmoxStreamBuilder<PveResourceBloc, PveResourceState>( | |
340 | bloc: rBloc, | |
341 | builder: (context, rState) { | |
342 | final onlineVMs = rState.vms.where((e) => | |
343 | e.getStatus() == PveResourceStatusType.running); | |
344 | final onlineCTs = rState.container.where((e) => | |
345 | e.getStatus() == PveResourceStatusType.running); | |
346 | final totalVMs = rState.vms.length; | |
347 | final offVMs = totalVMs - onlineVMs.length; | |
348 | final offCTs = totalVMs - onlineCTs.length; | |
349 | final totalCTs = rState.container.length; | |
350 | return PveResourceDataCardWidget( | |
351 | title: Text( | |
352 | 'Guests', | |
353 | style: TextStyle( | |
354 | fontWeight: FontWeight.bold, | |
355 | fontSize: 20, | |
356 | ), | |
357 | ), | |
358 | children: <Widget>[ | |
359 | ListTile( | |
360 | title: Text("Virtual Machines"), | |
361 | trailing: Text(totalVMs.toString() ?? '?'), | |
362 | leading: | |
363 | Icon(Renderers.getDefaultResourceIcon('qemu')), | |
364 | ), | |
365 | ListTile( | |
366 | dense: true, | |
367 | title: Text( | |
368 | "Online", | |
369 | style: TextStyle(fontSize: 14), | |
370 | ), | |
371 | leading: Icon(Icons.play_circle_outline, | |
372 | color: Colors.green), | |
373 | trailing: Text(onlineVMs.length.toString()), | |
374 | ), | |
375 | ListTile( | |
376 | dense: true, | |
377 | title: Text( | |
378 | "Offline", | |
f5aa4476 TM |
379 | style: TextStyle( |
380 | fontSize: 14, | |
381 | ), | |
bbc12f2a TM |
382 | ), |
383 | leading: Icon(Icons.stop), | |
384 | trailing: Text(offVMs.toString()), | |
385 | ), | |
d84177c2 TM |
386 | Divider( |
387 | indent: 10, | |
388 | ), | |
bbc12f2a TM |
389 | ListTile( |
390 | title: Text("LXC Container"), | |
391 | trailing: Text(totalCTs.toString() ?? '?'), | |
392 | leading: | |
393 | Icon(Renderers.getDefaultResourceIcon('lxc')), | |
394 | ), | |
bbc12f2a TM |
395 | ListTile( |
396 | dense: true, | |
c96ce032 | 397 | title: Text( |
bbc12f2a TM |
398 | "Online", |
399 | style: TextStyle(fontSize: 14), | |
400 | ), | |
401 | leading: Icon(Icons.play_circle_outline, | |
402 | color: Colors.green), | |
403 | trailing: Text(onlineCTs.length.toString()), | |
404 | ), | |
405 | ListTile( | |
406 | dense: true, | |
407 | title: Text( | |
408 | "Offline", | |
c96ce032 | 409 | style: TextStyle( |
f5aa4476 TM |
410 | fontSize: 14, |
411 | ), | |
35e7d210 | 412 | ), |
bbc12f2a TM |
413 | leading: Icon(Icons.stop), |
414 | trailing: Text(offCTs.toString()), | |
415 | ), | |
416 | ], | |
417 | ); | |
418 | }), | |
419 | ]); | |
420 | }), | |
421 | ]), | |
3628aaea TM |
422 | bottomNavigationBar: PveMobileBottomNavigationbar(), |
423 | ); | |
424 | } | |
425 | } | |
426 | ||
427 | class PveNodeListTile extends StatelessWidget { | |
428 | final String name; | |
429 | final bool online; | |
430 | final String type; | |
431 | final String level; | |
432 | final String ip; | |
433 | const PveNodeListTile( | |
434 | {Key key, | |
435 | @required this.name, | |
436 | @required this.online, | |
437 | @required this.type, | |
438 | this.level, | |
439 | this.ip = ''}) | |
440 | : super(key: key); | |
441 | @override | |
442 | Widget build(BuildContext context) { | |
443 | return ListTile( | |
444 | leading: Icon( | |
445 | Renderers.getDefaultResourceIcon(type), | |
00a48038 | 446 | ), |
3628aaea TM |
447 | title: Text(name ?? "unkown"), |
448 | subtitle: Text(getNodeTileSubtitle(online, level, ip)), | |
449 | trailing: Icon(Icons.power, color: online ? Colors.green : Colors.grey), | |
450 | onTap: () => Navigator.pushNamed(context, '/nodes/$name'), | |
451 | ); | |
452 | } | |
453 | ||
454 | String getNodeTileSubtitle(bool online, String level, String ip) { | |
455 | if (online) { | |
456 | if (level != null && level.isNotEmpty) { | |
b172a2d8 | 457 | return '$ip - ' + Renderers.renderSupportLevel(level); |
3628aaea TM |
458 | } |
459 | return '$ip - no support'; | |
460 | } | |
461 | return 'offline'; | |
462 | } | |
463 | } | |
464 | ||
465 | class MobileResourceOverview extends StatelessWidget { | |
466 | @override | |
467 | Widget build(BuildContext context) { | |
468 | final rBloc = Provider.of<PveResourceBloc>(context); | |
469 | return ProxmoxStreamBuilder<PveResourceBloc, PveResourceState>( | |
470 | bloc: rBloc, | |
471 | builder: (context, rstate) { | |
3628aaea | 472 | final fResources = rstate.filterResources.toList(); |
4e30c09d TM |
473 | return GestureDetector( |
474 | onTap: () { | |
475 | FocusScope.of(context).unfocus(); | |
476 | }, | |
477 | child: Scaffold( | |
478 | endDrawer: _MobileResourceFilterSheet(), | |
479 | appBar: AppBar( | |
480 | automaticallyImplyLeading: false, | |
481 | backgroundColor: Theme.of(context).scaffoldBackgroundColor, | |
482 | elevation: 0, | |
483 | title: AppbarSearchTextField( | |
484 | onChanged: (filter) => rBloc.events.add(FilterByName(filter)), | |
485 | ), | |
486 | actions: <Widget>[AppBarFilterIconButton()], | |
3628aaea | 487 | ), |
4e30c09d TM |
488 | body: ListView.separated( |
489 | itemCount: fResources.length, | |
490 | separatorBuilder: (context, index) => Divider(), | |
491 | itemBuilder: (context, index) { | |
492 | final resource = fResources[index]; | |
493 | var listWidget; | |
494 | if (const ['lxc', 'qemu'].contains(resource.type)) { | |
495 | listWidget = PveGuestListTile(resource: resource); | |
496 | } | |
497 | if (resource.type == 'node') { | |
498 | listWidget = PveNodeListTile( | |
499 | name: resource.node, | |
500 | online: | |
501 | resource.getStatus() == PveResourceStatusType.running, | |
502 | type: resource.type, | |
503 | level: resource.level, | |
504 | ); | |
505 | } | |
506 | if (resource.type == 'storage') { | |
507 | listWidget = PveStorageListeTile( | |
508 | resource: resource, | |
509 | ); | |
510 | } | |
511 | if (listWidget != null) { | |
512 | if (otherCategory(fResources, index)) { | |
513 | return Column( | |
514 | crossAxisAlignment: CrossAxisAlignment.start, | |
515 | children: [ | |
516 | Padding( | |
517 | padding: const EdgeInsets.all(12.0), | |
518 | child: Text( | |
519 | resource.type.toUpperCase(), | |
520 | style: TextStyle( | |
521 | fontSize: 18, | |
522 | fontWeight: FontWeight.bold, | |
523 | ), | |
6e210f1b TM |
524 | ), |
525 | ), | |
4e30c09d TM |
526 | listWidget, |
527 | ], | |
528 | ); | |
529 | } else { | |
530 | return listWidget; | |
531 | } | |
7cf5af08 | 532 | } |
7cf5af08 | 533 | |
4e30c09d TM |
534 | return ListTile( |
535 | title: Text('Unkown resource type'), | |
536 | ); | |
537 | }, | |
538 | ), | |
539 | bottomNavigationBar: PveMobileBottomNavigationbar(), | |
3628aaea | 540 | ), |
3628aaea TM |
541 | ); |
542 | }, | |
543 | ); | |
544 | } | |
7cf5af08 TM |
545 | |
546 | bool otherCategory(List<PveClusterResourcesModel> fResources, index) { | |
547 | var previous; | |
548 | if (index > 0) { | |
549 | previous = fResources[index - 1]; | |
550 | } | |
551 | final current = fResources[index]; | |
552 | return previous == null || previous.type != current.type; | |
553 | } | |
554 | } | |
555 | ||
556 | class PveGuestListTile extends StatelessWidget { | |
557 | const PveGuestListTile({ | |
558 | Key key, | |
559 | @required this.resource, | |
560 | }) : super(key: key); | |
561 | ||
562 | final PveClusterResourcesModel resource; | |
563 | ||
564 | @override | |
565 | Widget build(BuildContext context) { | |
566 | return ListTile( | |
567 | leading: Icon( | |
568 | Renderers.getDefaultResourceIcon(resource.type, | |
569 | shared: resource.shared), | |
570 | ), | |
571 | title: Text(resource.displayName), | |
572 | subtitle: Row( | |
573 | mainAxisAlignment: MainAxisAlignment.spaceBetween, | |
574 | children: [ | |
575 | Text(resource.node), | |
576 | StatusChip( | |
577 | status: resource.getStatus(), | |
578 | fontzsize: 12, | |
579 | ), | |
580 | ], | |
581 | ), | |
582 | onTap: () { | |
583 | if (['qemu', 'lxc'].contains(resource.type)) { | |
584 | Navigator.pushNamed( | |
585 | context, '/nodes/${resource.node}/${resource.id}'); | |
586 | } | |
587 | }, | |
588 | ); | |
589 | } | |
590 | } | |
591 | ||
592 | class PveStorageListeTile extends StatelessWidget { | |
593 | const PveStorageListeTile({ | |
594 | Key key, | |
595 | @required this.resource, | |
596 | }) : super(key: key); | |
597 | ||
598 | final PveClusterResourcesModel resource; | |
599 | ||
600 | @override | |
601 | Widget build(BuildContext context) { | |
602 | final apiClient = Provider.of<ProxmoxApiClient>(context); | |
603 | ||
604 | return ListTile( | |
605 | title: Text(resource.displayName), | |
606 | subtitle: Column( | |
607 | crossAxisAlignment: CrossAxisAlignment.start, | |
608 | children: <Widget>[ | |
609 | Row( | |
610 | mainAxisAlignment: MainAxisAlignment.spaceBetween, | |
611 | children: [ | |
612 | Text(resource.node), | |
613 | StatusChip( | |
614 | status: resource.getStatus(), | |
615 | fontzsize: 12, | |
616 | ), | |
617 | ], | |
618 | ), | |
619 | if (resource.getStatus() == PveResourceStatusType.running) | |
620 | ProxmoxCapacityIndicator( | |
621 | usedValue: Renderers.formatSize(resource.disk ?? 0), | |
622 | totalValue: Renderers.formatSize(resource.maxdisk ?? 0), | |
623 | usedPercent: (resource.disk ?? 0.0) / (resource.maxdisk ?? 100.0), | |
624 | icon: Icon( | |
625 | Renderers.getDefaultResourceIcon(resource.type, | |
626 | shared: resource.shared), | |
627 | ), | |
628 | ), | |
629 | ], | |
630 | ), | |
631 | onTap: resource.getStatus() == PveResourceStatusType.running | |
632 | ? () => Navigator.of(context).push(MaterialPageRoute( | |
633 | builder: (context) => PveFileSelector( | |
634 | fBloc: PveFileSelectorBloc( | |
635 | apiClient: apiClient, | |
636 | init: PveFileSelectorState.init(nodeID: resource.node) | |
637 | .rebuild((b) => b..storageID = resource.storage)), | |
638 | sBloc: PveStorageSelectorBloc( | |
639 | apiClient: apiClient, | |
640 | init: PveStorageSelectorState.init(nodeID: resource.node) | |
641 | .rebuild((b) => b..storage = resource.storage), | |
642 | )..events.add(LoadStoragesEvent()), | |
643 | ), | |
644 | )) | |
645 | : null, | |
646 | ); | |
647 | } | |
3628aaea TM |
648 | } |
649 | ||
650 | class AppbarSearchTextField extends StatefulWidget { | |
651 | final ValueChanged<String> onChanged; | |
652 | ||
653 | const AppbarSearchTextField({Key key, this.onChanged}) : super(key: key); | |
654 | @override | |
655 | _AppbarSearchTextFieldState createState() => _AppbarSearchTextFieldState(); | |
656 | } | |
657 | ||
658 | class _AppbarSearchTextFieldState extends State<AppbarSearchTextField> { | |
659 | TextEditingController _controller; | |
660 | ||
661 | void initState() { | |
662 | super.initState(); | |
663 | _controller = TextEditingController(); | |
664 | } | |
665 | ||
666 | void dispose() { | |
667 | _controller.dispose(); | |
668 | super.dispose(); | |
669 | } | |
670 | ||
671 | @override | |
672 | Widget build(BuildContext context) { | |
673 | return TextField( | |
674 | decoration: InputDecoration( | |
675 | prefixIcon: Icon( | |
676 | Icons.search, | |
677 | size: 20, | |
678 | ), | |
4e30c09d TM |
679 | suffixIcon: _controller.text.isNotEmpty |
680 | ? IconButton( | |
681 | padding: EdgeInsets.zero, | |
682 | iconSize: 20, | |
683 | icon: Icon(Icons.close), | |
684 | onPressed: () { | |
685 | _controller.clear(); | |
686 | widget.onChanged(''); | |
687 | FocusScope.of(context).unfocus(); | |
688 | }, | |
689 | ) | |
690 | : null, | |
3628aaea TM |
691 | contentPadding: EdgeInsets.fromLTRB(20, 5, 8, 5), |
692 | prefixIconConstraints: BoxConstraints(minHeight: 32, minWidth: 32), | |
4e30c09d | 693 | suffixIconConstraints: BoxConstraints(maxHeight: 32, maxWidth: 32), |
3628aaea TM |
694 | fillColor: Color(0xFFF1F2F4), |
695 | filled: true, | |
696 | isDense: true, | |
697 | enabledBorder: | |
698 | OutlineInputBorder(borderSide: BorderSide(color: Colors.white))), | |
699 | style: TextStyle(fontSize: 20), | |
700 | onChanged: (value) => widget.onChanged(value), | |
701 | controller: _controller, | |
702 | ); | |
703 | } | |
704 | } | |
705 | ||
706 | class _MobileResourceFilterSheet extends StatelessWidget { | |
707 | @override | |
708 | Widget build(BuildContext context) { | |
709 | final rBloc = Provider.of<PveResourceBloc>(context); | |
710 | ||
711 | return ProxmoxStreamBuilder<PveResourceBloc, PveResourceState>( | |
712 | bloc: rBloc, | |
713 | builder: (context, state) => Drawer( | |
714 | child: SingleChildScrollView( | |
57d2bbd2 TM |
715 | child: Column( |
716 | crossAxisAlignment: CrossAxisAlignment.start, | |
717 | children: <Widget>[ | |
718 | Padding( | |
719 | padding: const EdgeInsets.fromLTRB(8.0, 20.0, 8.0, 0), | |
720 | child: ListTile( | |
721 | title: Text( | |
722 | 'Filter Results', | |
723 | style: TextStyle(color: Colors.grey.shade700), | |
724 | ), | |
3628aaea | 725 | ), |
57d2bbd2 TM |
726 | ), |
727 | Divider( | |
728 | indent: 0, | |
729 | endIndent: 0, | |
730 | ), | |
731 | Padding( | |
732 | padding: const EdgeInsets.fromLTRB(8.0, 0, 8.0, 0), | |
733 | child: Column( | |
734 | children: [ | |
735 | ListTile( | |
736 | title: Text( | |
737 | 'Type', | |
738 | style: TextStyle( | |
739 | color: Colors.black, fontWeight: FontWeight.bold), | |
740 | ), | |
741 | ), | |
742 | CheckboxListTile( | |
743 | dense: true, | |
744 | title: Text( | |
745 | 'Nodes', | |
746 | style: TextStyle(color: Colors.grey.shade700), | |
747 | ), | |
748 | value: state.typeFilter.contains('node'), | |
749 | onChanged: (v) => rBloc.events.add(FilterByType( | |
750 | addOrRemove(v, 'node', state.typeFilter))), | |
751 | ), | |
752 | CheckboxListTile( | |
753 | dense: true, | |
754 | title: Text( | |
755 | 'Qemu', | |
756 | style: TextStyle(color: Colors.grey.shade700), | |
757 | ), | |
758 | value: state.typeFilter.contains('qemu'), | |
759 | onChanged: (v) => rBloc.events.add(FilterByType( | |
760 | addOrRemove(v, 'qemu', state.typeFilter))), | |
761 | ), | |
762 | CheckboxListTile( | |
763 | dense: true, | |
764 | title: Text( | |
765 | 'LXC', | |
766 | style: TextStyle(color: Colors.grey.shade700), | |
767 | ), | |
768 | value: state.typeFilter.contains('lxc'), | |
769 | onChanged: (v) => rBloc.events.add(FilterByType( | |
770 | addOrRemove(v, 'lxc', state.typeFilter))), | |
771 | ), | |
772 | CheckboxListTile( | |
773 | dense: true, | |
774 | title: Text( | |
775 | 'Storage', | |
776 | style: TextStyle(color: Colors.grey.shade700), | |
777 | ), | |
778 | value: state.typeFilter.contains('storage'), | |
779 | onChanged: (v) => rBloc.events.add(FilterByType( | |
780 | addOrRemove(v, 'storage', state.typeFilter))), | |
781 | ), | |
782 | ], | |
3628aaea | 783 | ), |
57d2bbd2 TM |
784 | ), |
785 | ], | |
3628aaea TM |
786 | ), |
787 | ), | |
00a48038 TM |
788 | ), |
789 | ); | |
790 | } | |
3628aaea TM |
791 | |
792 | BuiltSet<String> addOrRemove( | |
793 | bool value, String element, BuiltSet<String> filter) { | |
794 | if (value) { | |
795 | return filter.rebuild((b) => b..add(element)); | |
796 | } else { | |
797 | return filter.rebuild((b) => b..remove(element)); | |
798 | } | |
799 | } | |
800 | } | |
801 | ||
802 | class AppBarFilterIconButton extends StatelessWidget { | |
803 | @override | |
804 | Widget build(BuildContext context) { | |
805 | return IconButton( | |
806 | icon: Icon( | |
807 | FontAwesomeIcons.filter, | |
808 | color: Colors.black, | |
809 | ), | |
810 | onPressed: () => Scaffold.of(context).openEndDrawer()); | |
811 | } | |
812 | } | |
5ad24bd7 TM |
813 | |
814 | class MobileAccessManagement extends StatelessWidget { | |
815 | @override | |
816 | Widget build(BuildContext context) { | |
817 | final aBloc = Provider.of<PveAccessManagementBloc>(context); | |
818 | return DefaultTabController( | |
b6823e19 | 819 | length: 5, |
5ad24bd7 TM |
820 | child: Scaffold( |
821 | appBar: AppBar( | |
822 | title: Text('Permissions'), | |
823 | //backgroundColor: Colors.transparent, | |
824 | elevation: 0.0, | |
825 | automaticallyImplyLeading: false, | |
bbc12f2a TM |
826 | actions: [ |
827 | IconButton( | |
828 | icon: Icon(Icons.input), | |
829 | tooltip: "Logout", | |
830 | onPressed: () { | |
831 | Provider.of<PveAuthenticationBloc>(context) | |
832 | .events | |
833 | .add(LoggedOut()); | |
834 | Navigator.of(context).pushReplacementNamed('/login'); | |
835 | }) | |
836 | ], | |
5ad24bd7 TM |
837 | bottom: TabBar(isScrollable: true, tabs: [ |
838 | Tab( | |
839 | text: 'Users', | |
840 | icon: Icon(Icons.person), | |
841 | ), | |
b6823e19 TM |
842 | Tab( |
843 | text: 'API Tokens', | |
844 | icon: Icon(Icons.person_outline), | |
845 | ), | |
5ad24bd7 TM |
846 | Tab( |
847 | text: 'Groups', | |
848 | icon: Icon(Icons.group), | |
849 | ), | |
850 | Tab( | |
851 | text: 'Roles', | |
852 | icon: Icon(Icons.lock_open), | |
853 | ), | |
854 | Tab( | |
855 | text: 'Domains', | |
856 | icon: Icon(Icons.domain), | |
857 | ) | |
858 | ]), | |
859 | ), | |
860 | body: ProxmoxStreamBuilder<PveAccessManagementBloc, | |
861 | PveAccessManagementState>( | |
862 | bloc: aBloc, | |
863 | builder: (context, aState) { | |
864 | return TabBarView(children: [ | |
865 | ListView.builder( | |
866 | itemCount: aState.users.length, | |
867 | itemBuilder: (context, index) { | |
868 | final user = aState.users[index]; | |
869 | return ListTile( | |
870 | title: Text(user.userid), | |
871 | subtitle: Text(user.email ?? ''), | |
872 | trailing: aState.apiUser == user.userid | |
873 | ? Icon(Icons.person_pin_circle) | |
874 | : null, | |
875 | ); | |
876 | }), | |
b6823e19 TM |
877 | ListView.builder( |
878 | itemCount: aState.tokens.length, | |
879 | itemBuilder: (context, index) { | |
880 | final token = aState.tokens[index]; | |
881 | var expireDate = 'infinite'; | |
882 | if (token.expire != null) { | |
883 | expireDate = DateFormat.yMd().format(token.expire); | |
884 | } | |
885 | ||
886 | return ListTile( | |
887 | title: Text('${token.userid} ${token.tokenid}'), | |
888 | subtitle: Text('Expires: $expireDate'), | |
889 | ); | |
890 | }), | |
5ad24bd7 TM |
891 | ListView.builder( |
892 | itemCount: aState.groups.length, | |
893 | itemBuilder: (context, index) { | |
894 | final group = aState.groups[index]; | |
895 | final users = | |
896 | group.users.isNotEmpty ? group.users.split(',') : []; | |
897 | return ListTile( | |
898 | title: Text(group.groupid), | |
899 | subtitle: Text(group.comment ?? ''), | |
900 | trailing: Icon(Icons.arrow_right), | |
901 | onTap: () => showModalBottomSheet( | |
902 | shape: RoundedRectangleBorder( | |
903 | borderRadius: BorderRadius.vertical( | |
904 | top: Radius.circular(10))), | |
905 | context: context, | |
906 | builder: (context) { | |
907 | return Container( | |
908 | height: MediaQuery.of(context).size.height * 0.5, | |
909 | child: Column( | |
910 | crossAxisAlignment: CrossAxisAlignment.start, | |
911 | children: <Widget>[ | |
912 | Padding( | |
913 | padding: EdgeInsets.fromLTRB(0, 5, 0, 5), | |
914 | child: Align( | |
915 | alignment: Alignment.topCenter, | |
916 | child: Container( | |
917 | width: 40, | |
918 | height: 3, | |
919 | color: Colors.black, | |
920 | ), | |
921 | ), | |
922 | ), | |
923 | ListTile( | |
924 | title: | |
925 | Text('Group members (${users.length})'), | |
926 | ), | |
927 | Divider(), | |
928 | Expanded( | |
929 | child: Padding( | |
930 | padding: const EdgeInsets.all(14.0), | |
931 | child: ListView.builder( | |
932 | itemCount: users.length, | |
933 | itemBuilder: (context, index) => | |
934 | ListTile( | |
935 | title: Text(users[index]), | |
936 | ), | |
937 | ), | |
938 | ), | |
939 | ) | |
940 | ], | |
941 | ), | |
942 | ); | |
943 | }, | |
944 | ), | |
945 | ); | |
946 | }), | |
947 | ListView.builder( | |
948 | itemCount: aState.roles.length, | |
949 | itemBuilder: (context, index) { | |
950 | final role = aState.roles[index]; | |
951 | final perms = role.privs.split(','); | |
952 | return ListTile( | |
953 | title: Text(role.roleid), | |
954 | subtitle: | |
955 | Text(role.special ? 'Built in Role' : 'Custom'), | |
956 | trailing: Icon(Icons.arrow_right), | |
957 | onTap: () => showModalBottomSheet( | |
958 | shape: RoundedRectangleBorder( | |
959 | borderRadius: BorderRadius.vertical( | |
960 | top: Radius.circular(10))), | |
961 | context: context, | |
962 | builder: (context) { | |
963 | return Container( | |
964 | height: MediaQuery.of(context).size.height * 0.5, | |
965 | child: Column( | |
966 | crossAxisAlignment: CrossAxisAlignment.start, | |
967 | children: <Widget>[ | |
968 | Padding( | |
969 | padding: EdgeInsets.fromLTRB(0, 5, 0, 5), | |
970 | child: Align( | |
971 | alignment: Alignment.topCenter, | |
972 | child: Container( | |
973 | width: 40, | |
974 | height: 3, | |
975 | color: Colors.black, | |
976 | ), | |
977 | ), | |
978 | ), | |
979 | ListTile( | |
980 | title: Text('Privileges (${perms.length})'), | |
981 | ), | |
982 | Divider(), | |
983 | Expanded( | |
984 | child: Padding( | |
985 | padding: const EdgeInsets.all(14.0), | |
986 | child: ListView.builder( | |
987 | itemCount: perms.length, | |
988 | itemBuilder: (context, index) => | |
989 | ListTile( | |
990 | title: Text(perms[index]), | |
991 | ), | |
992 | ), | |
993 | ), | |
994 | ) | |
995 | ], | |
996 | ), | |
997 | ); | |
998 | }, | |
999 | ), | |
1000 | ); | |
1001 | }), | |
1002 | ListView.builder( | |
1003 | itemCount: aState.domains.length, | |
1004 | itemBuilder: (context, index) { | |
1005 | final domain = aState.domains[index]; | |
1006 | return ListTile( | |
1007 | title: Text(domain.realm), | |
1008 | subtitle: Text(domain.comment ?? ''), | |
1009 | trailing: domain.tfa?.isNotEmpty ?? false | |
1010 | ? Icon(Icons.looks_two) | |
1011 | : null, | |
1012 | ); | |
1013 | }), | |
1014 | ]); | |
1015 | }), | |
1016 | bottomNavigationBar: PveMobileBottomNavigationbar(), | |
1017 | ), | |
1018 | ); | |
1019 | } | |
1020 | } |