]> git.proxmox.com Git - flutter/pve_flutter_frontend.git/blame - lib/pages/main_layout_slim.dart
resource overview: adapt filter sheet design
[flutter/pve_flutter_frontend.git] / lib / pages / main_layout_slim.dart
CommitLineData
3628aaea 1import 'package:built_collection/built_collection.dart';
00a48038 2import 'package:flutter/material.dart';
3628aaea
TM
3import 'package:flutter/rendering.dart';
4import 'package:font_awesome_flutter/font_awesome_flutter.dart';
b6823e19 5import 'package:intl/intl.dart';
3628aaea
TM
6import 'package:provider/provider.dart';
7import 'package:proxmox_dart_api_client/proxmox_dart_api_client.dart';
5ad24bd7 8import 'package:pve_flutter_frontend/bloc/pve_access_management_bloc.dart';
3628aaea
TM
9import 'package:pve_flutter_frontend/bloc/pve_authentication_bloc.dart';
10import 'package:pve_flutter_frontend/bloc/pve_cluster_status_bloc.dart';
11import 'package:pve_flutter_frontend/bloc/pve_file_selector_bloc.dart';
12import 'package:pve_flutter_frontend/bloc/pve_resource_bloc.dart';
13import 'package:pve_flutter_frontend/bloc/pve_storage_selector_bloc.dart';
5ad24bd7 14import 'package:pve_flutter_frontend/states/pve_access_management_state.dart';
3628aaea
TM
15import 'package:pve_flutter_frontend/states/pve_cluster_status_state.dart';
16import 'package:pve_flutter_frontend/states/pve_file_selector_state.dart';
17import 'package:pve_flutter_frontend/states/pve_resource_state.dart';
18import 'package:pve_flutter_frontend/states/pve_storage_selector_state.dart';
19import 'package:pve_flutter_frontend/utils/renderers.dart';
3628aaea 20import 'package:pve_flutter_frontend/widgets/proxmox_capacity_indicator.dart';
bbc12f2a
TM
21import 'package:pve_flutter_frontend/widgets/proxmox_gauge_chart.dart';
22import 'package:pve_flutter_frontend/widgets/proxmox_heartbeat_indicator.dart';
3628aaea
TM
23import 'package:pve_flutter_frontend/widgets/proxmox_stream_builder_widget.dart';
24import 'package:pve_flutter_frontend/widgets/pve_file_selector_widget.dart';
25import 'package:pve_flutter_frontend/widgets/pve_help_icon_button_widget.dart';
c96ce032 26import 'package:pve_flutter_frontend/widgets/pve_resource_data_card_widget.dart';
3628aaea 27import 'package:pve_flutter_frontend/widgets/pve_resource_status_chip_widget.dart';
bbc12f2a 28import 'package:pve_flutter_frontend/widgets/pve_subscription_alert_dialog.dart';
3628aaea 29import 'package:rxdart/rxdart.dart';
00a48038 30
3628aaea
TM
31class MainLayoutSlim extends StatefulWidget {
32 @override
33 _MainLayoutSlimState createState() => _MainLayoutSlimState();
34}
35
36class _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
104class 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
135class 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
427class 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
465class 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
556class 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
592class 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
650class 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
658class _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
706class _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
802class 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
814class 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}