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