]> git.proxmox.com Git - flutter/pve_flutter_frontend.git/blame - lib/pages/main_layout_slim.dart
bump version to 1.7.2+43
[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,
5f9cc4f9 380 online: node.online ?? false,
bbc12f2a
TM
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;
5f9cc4f9 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,
5f9cc4f9 558 required this.online,
598a93b4 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 569 title: Text(name),
5f9cc4f9
DC
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 },
453eb819
DC
596 child: SafeArea(
597 child: Scaffold(
4e30c09d
TM
598 endDrawer: _MobileResourceFilterSheet(),
599 appBar: AppBar(
600 automaticallyImplyLeading: false,
9bb60c76 601 backgroundColor: Theme.of(context).colorScheme.primary,
4e30c09d
TM
602 elevation: 0,
603 title: AppbarSearchTextField(
d48d8e9d
TM
604 onChanged: (filter) =>
605 rBloc.events.add(FilterResources(nameFilter: filter)),
4e30c09d
TM
606 ),
607 actions: <Widget>[AppBarFilterIconButton()],
3628aaea 608 ),
4e30c09d
TM
609 body: ListView.separated(
610 itemCount: fResources.length,
611 separatorBuilder: (context, index) => Divider(),
612 itemBuilder: (context, index) {
613 final resource = fResources[index];
614 var listWidget;
615 if (const ['lxc', 'qemu'].contains(resource.type)) {
616 listWidget = PveGuestListTile(resource: resource);
617 }
618 if (resource.type == 'node') {
619 listWidget = PveNodeListTile(
598a93b4 620 name: resource.node!,
4e30c09d
TM
621 online:
622 resource.getStatus() == PveResourceStatusType.running,
623 type: resource.type,
624 level: resource.level,
625 );
626 }
627 if (resource.type == 'storage') {
628 listWidget = PveStorageListeTile(
629 resource: resource,
630 );
631 }
632 if (listWidget != null) {
633 if (otherCategory(fResources, index)) {
634 return Column(
635 crossAxisAlignment: CrossAxisAlignment.start,
636 children: [
637 Padding(
638 padding: const EdgeInsets.all(12.0),
639 child: Text(
640 resource.type.toUpperCase(),
641 style: TextStyle(
642 fontSize: 18,
643 fontWeight: FontWeight.bold,
644 ),
6e210f1b
TM
645 ),
646 ),
4e30c09d
TM
647 listWidget,
648 ],
649 );
650 } else {
651 return listWidget;
652 }
7cf5af08 653 }
7cf5af08 654
4e30c09d
TM
655 return ListTile(
656 title: Text('Unkown resource type'),
657 );
658 },
659 ),
660 bottomNavigationBar: PveMobileBottomNavigationbar(),
453eb819 661 )),
3628aaea
TM
662 );
663 },
664 );
665 }
7cf5af08
TM
666
667 bool otherCategory(List<PveClusterResourcesModel> fResources, index) {
668 var previous;
669 if (index > 0) {
670 previous = fResources[index - 1];
671 }
672 final current = fResources[index];
673 return previous == null || previous.type != current.type;
674 }
675}
676
677class PveGuestListTile extends StatelessWidget {
678 const PveGuestListTile({
598a93b4
TL
679 Key? key,
680 required this.resource,
7cf5af08
TM
681 }) : super(key: key);
682
683 final PveClusterResourcesModel resource;
684
685 @override
686 Widget build(BuildContext context) {
910168d6 687 final status = resource.getStatus();
5b5b3f73 688
7cf5af08 689 return ListTile(
5b5b3f73
TM
690 leading: PveGuestIcon(
691 type: resource.type,
692 template: resource.template,
693 status: status,
7cf5af08
TM
694 ),
695 title: Text(resource.displayName),
696 subtitle: Row(
697 mainAxisAlignment: MainAxisAlignment.spaceBetween,
698 children: [
598a93b4 699 Text(resource.node!),
7cf5af08 700 StatusChip(
5b5b3f73 701 status: status,
7cf5af08
TM
702 fontzsize: 12,
703 ),
704 ],
705 ),
706 onTap: () {
707 if (['qemu', 'lxc'].contains(resource.type)) {
708 Navigator.pushNamed(
709 context, '/nodes/${resource.node}/${resource.id}');
710 }
711 },
712 );
713 }
714}
715
716class PveStorageListeTile extends StatelessWidget {
717 const PveStorageListeTile({
598a93b4
TL
718 Key? key,
719 required this.resource,
7cf5af08
TM
720 }) : super(key: key);
721
722 final PveClusterResourcesModel resource;
723
724 @override
725 Widget build(BuildContext context) {
726 final apiClient = Provider.of<ProxmoxApiClient>(context);
aae9f01d 727 final usedPercent = (resource.disk ?? 0.0) / (resource.maxdisk ?? 100.0);
7cf5af08
TM
728 return ListTile(
729 title: Text(resource.displayName),
730 subtitle: Column(
731 crossAxisAlignment: CrossAxisAlignment.start,
732 children: <Widget>[
733 Row(
734 mainAxisAlignment: MainAxisAlignment.spaceBetween,
735 children: [
598a93b4 736 Text(resource.node!),
7cf5af08
TM
737 StatusChip(
738 status: resource.getStatus(),
739 fontzsize: 12,
740 ),
741 ],
742 ),
aae9f01d
TM
743 if (resource.getStatus() == PveResourceStatusType.running &&
744 !(usedPercent.isNaN || usedPercent.isInfinite))
7cf5af08
TM
745 ProxmoxCapacityIndicator(
746 usedValue: Renderers.formatSize(resource.disk ?? 0),
747 totalValue: Renderers.formatSize(resource.maxdisk ?? 0),
aae9f01d
TM
748 usedPercent:
749 usedPercent.isNaN || usedPercent.isInfinite ? 0 : usedPercent,
7cf5af08
TM
750 icon: Icon(
751 Renderers.getDefaultResourceIcon(resource.type,
752 shared: resource.shared),
753 ),
754 ),
755 ],
756 ),
757 onTap: resource.getStatus() == PveResourceStatusType.running
758 ? () => Navigator.of(context).push(MaterialPageRoute(
759 builder: (context) => PveFileSelector(
760 fBloc: PveFileSelectorBloc(
761 apiClient: apiClient,
762 init: PveFileSelectorState.init(nodeID: resource.node)
763 .rebuild((b) => b..storageID = resource.storage)),
764 sBloc: PveStorageSelectorBloc(
765 apiClient: apiClient,
766 init: PveStorageSelectorState.init(nodeID: resource.node)
767 .rebuild((b) => b..storage = resource.storage),
768 )..events.add(LoadStoragesEvent()),
769 ),
770 ))
771 : null,
772 );
773 }
3628aaea
TM
774}
775
776class AppbarSearchTextField extends StatefulWidget {
598a93b4 777 final ValueChanged<String>? onChanged;
3628aaea 778
598a93b4 779 const AppbarSearchTextField({Key? key, this.onChanged}) : super(key: key);
3628aaea
TM
780 @override
781 _AppbarSearchTextFieldState createState() => _AppbarSearchTextFieldState();
782}
783
784class _AppbarSearchTextFieldState extends State<AppbarSearchTextField> {
9bb60c76 785 late TextEditingController _controller;
3628aaea
TM
786
787 void initState() {
788 super.initState();
789 _controller = TextEditingController();
790 }
791
792 void dispose() {
9bb60c76 793 _controller.dispose();
3628aaea
TM
794 super.dispose();
795 }
796
797 @override
798 Widget build(BuildContext context) {
799 return TextField(
800 decoration: InputDecoration(
9bb60c76
TL
801 prefixIcon: Icon(
802 Icons.search,
803 size: 20,
804 color: Theme.of(context).colorScheme.onSurface,
805 ),
806 suffixIcon: _controller.text.isNotEmpty
807 ? IconButton(
808 padding: EdgeInsets.zero,
809 iconSize: 20,
810 icon: Icon(
811 Icons.close,
812 color: Theme.of(context).colorScheme.onSurface,
813 ),
814 onPressed: () {
815 _controller.clear();
816 widget.onChanged!('');
817 FocusScope.of(context).unfocus();
818 },
819 )
820 : null,
821 contentPadding: EdgeInsets.fromLTRB(20, 5, 8, 5),
822 prefixIconConstraints: BoxConstraints(minHeight: 32, minWidth: 32),
823 suffixIconConstraints: BoxConstraints(maxHeight: 32, maxWidth: 32),
824 //fillColor: Color(0xFFF1F2F4),
825 fillColor: Theme.of(context).colorScheme.surface,
826 filled: true,
827 isDense: true,
828 enabledBorder:
829 OutlineInputBorder(borderSide: BorderSide(color: Colors.white)),
830 ),
3628aaea 831 style: TextStyle(fontSize: 20),
598a93b4 832 onChanged: (value) => widget.onChanged!(value),
3628aaea
TM
833 controller: _controller,
834 );
835 }
836}
837
838class _MobileResourceFilterSheet extends StatelessWidget {
839 @override
840 Widget build(BuildContext context) {
841 final rBloc = Provider.of<PveResourceBloc>(context);
842
843 return ProxmoxStreamBuilder<PveResourceBloc, PveResourceState>(
844 bloc: rBloc,
845 builder: (context, state) => Drawer(
846 child: SingleChildScrollView(
57d2bbd2
TM
847 child: Column(
848 crossAxisAlignment: CrossAxisAlignment.start,
849 children: <Widget>[
850 Padding(
851 padding: const EdgeInsets.fromLTRB(8.0, 20.0, 8.0, 0),
852 child: ListTile(
853 title: Text(
854 'Filter Results',
57d2bbd2 855 ),
d48d8e9d 856 trailing: rBloc.isFiltered
83b68bff 857 ? TextButton(
d48d8e9d
TM
858 onPressed: () => rBloc.events.add(ResetFilter()),
859 child: Text(
860 'Reset',
861 style: TextStyle(
58eadc41 862 color: Theme.of(context).colorScheme.secondary,
d48d8e9d
TM
863 ),
864 ),
865 )
866 : null,
3628aaea 867 ),
57d2bbd2
TM
868 ),
869 Divider(
870 indent: 0,
871 endIndent: 0,
872 ),
873 Padding(
874 padding: const EdgeInsets.fromLTRB(8.0, 0, 8.0, 0),
875 child: Column(
876 children: [
877 ListTile(
878 title: Text(
879 'Type',
9bb60c76 880 style: TextStyle(fontWeight: FontWeight.bold),
57d2bbd2
TM
881 ),
882 ),
883 CheckboxListTile(
884 dense: true,
885 title: Text(
886 'Nodes',
9bb60c76
TL
887 style: TextStyle(
888 color: Theme.of(context)
889 .colorScheme
890 .onSurface
891 .withOpacity(0.75)),
57d2bbd2
TM
892 ),
893 value: state.typeFilter.contains('node'),
d48d8e9d 894 onChanged: (v) => rBloc.events.add(FilterResources(
598a93b4 895 typeFilter: addOrRemove(v!, 'node', state.typeFilter),
d48d8e9d 896 )),
57d2bbd2
TM
897 ),
898 CheckboxListTile(
899 dense: true,
900 title: Text(
901 'Qemu',
9bb60c76
TL
902 style: TextStyle(
903 color: Theme.of(context)
904 .colorScheme
905 .onSurface
906 .withOpacity(0.75)),
57d2bbd2
TM
907 ),
908 value: state.typeFilter.contains('qemu'),
d48d8e9d 909 onChanged: (v) => rBloc.events.add(FilterResources(
598a93b4 910 typeFilter: addOrRemove(v!, 'qemu', state.typeFilter),
d48d8e9d 911 )),
57d2bbd2
TM
912 ),
913 CheckboxListTile(
914 dense: true,
915 title: Text(
916 'LXC',
9bb60c76
TL
917 style: TextStyle(
918 color: Theme.of(context)
919 .colorScheme
920 .onSurface
921 .withOpacity(0.75)),
57d2bbd2
TM
922 ),
923 value: state.typeFilter.contains('lxc'),
d48d8e9d 924 onChanged: (v) => rBloc.events.add(FilterResources(
598a93b4 925 typeFilter: addOrRemove(v!, 'lxc', state.typeFilter),
d48d8e9d 926 )),
57d2bbd2
TM
927 ),
928 CheckboxListTile(
929 dense: true,
930 title: Text(
931 'Storage',
9bb60c76
TL
932 style: TextStyle(
933 color: Theme.of(context)
934 .colorScheme
935 .onSurface
936 .withOpacity(0.75)),
57d2bbd2
TM
937 ),
938 value: state.typeFilter.contains('storage'),
d48d8e9d 939 onChanged: (v) => rBloc.events.add(FilterResources(
598a93b4
TL
940 typeFilter:
941 addOrRemove(v!, 'storage', state.typeFilter),
d48d8e9d 942 )),
57d2bbd2
TM
943 ),
944 ],
3628aaea 945 ),
57d2bbd2 946 ),
d48d8e9d
TM
947 Padding(
948 padding: const EdgeInsets.fromLTRB(8.0, 0, 8.0, 0),
949 child: Column(
950 children: [
951 ListTile(
952 title: Text(
953 'Status',
9bb60c76 954 style: TextStyle(fontWeight: FontWeight.bold),
d48d8e9d
TM
955 ),
956 ),
957 CheckboxListTile(
958 dense: true,
959 title: Text(
960 'Online',
9bb60c76
TL
961 style: TextStyle(
962 color: Theme.of(context)
963 .colorScheme
964 .onSurface
965 .withOpacity(0.75)),
d48d8e9d
TM
966 ),
967 value: state.statusFilter
968 .contains(PveResourceStatusType.running),
969 onChanged: (v) => rBloc.events.add(FilterResources(
598a93b4 970 statusFilter: addOrRemove(v!,
d48d8e9d
TM
971 PveResourceStatusType.running, state.statusFilter),
972 )),
973 ),
974 CheckboxListTile(
975 dense: true,
976 title: Text(
977 'Offline',
9bb60c76
TL
978 style: TextStyle(
979 color: Theme.of(context)
980 .colorScheme
981 .onSurface
982 .withOpacity(0.75)),
d48d8e9d
TM
983 ),
984 value: state.statusFilter
985 .contains(PveResourceStatusType.stopped),
986 onChanged: (v) => rBloc.events.add(FilterResources(
598a93b4 987 statusFilter: addOrRemove(v!,
d48d8e9d
TM
988 PveResourceStatusType.stopped, state.statusFilter),
989 )),
990 ),
991 ],
992 ),
993 )
57d2bbd2 994 ],
3628aaea
TM
995 ),
996 ),
00a48038
TM
997 ),
998 );
999 }
3628aaea 1000
d48d8e9d 1001 BuiltSet<S> addOrRemove<S>(bool value, S element, BuiltSet<S> filter) {
3628aaea
TM
1002 if (value) {
1003 return filter.rebuild((b) => b..add(element));
1004 } else {
1005 return filter.rebuild((b) => b..remove(element));
1006 }
1007 }
1008}
1009
1010class AppBarFilterIconButton extends StatelessWidget {
1011 @override
1012 Widget build(BuildContext context) {
d48d8e9d
TM
1013 final rBloc = Provider.of<PveResourceBloc>(context);
1014
1015 return ProxmoxStreamBuilder<PveResourceBloc, PveResourceState>(
1016 bloc: rBloc,
1017 builder: (context, state) {
1018 return IconButton(
1019 icon: rBloc.isFiltered
1020 ? Icon(
1021 FontAwesomeIcons.filter,
1022 color: Colors.black,
1023 )
1024 : Icon(
1025 FontAwesomeIcons.filter,
1026 color: Colors.grey,
1027 ),
1028 onPressed: () => Scaffold.of(context).openEndDrawer(),
1029 );
1030 });
3628aaea
TM
1031 }
1032}
5ad24bd7
TM
1033
1034class MobileAccessManagement extends StatelessWidget {
1035 @override
1036 Widget build(BuildContext context) {
1037 final aBloc = Provider.of<PveAccessManagementBloc>(context);
1038 return DefaultTabController(
b6823e19 1039 length: 5,
5ad24bd7
TM
1040 child: Scaffold(
1041 appBar: AppBar(
1042 title: Text('Permissions'),
1043 //backgroundColor: Colors.transparent,
1044 elevation: 0.0,
1045 automaticallyImplyLeading: false,
1046 bottom: TabBar(isScrollable: true, tabs: [
1047 Tab(
1048 text: 'Users',
1049 icon: Icon(Icons.person),
1050 ),
b6823e19
TM
1051 Tab(
1052 text: 'API Tokens',
1053 icon: Icon(Icons.person_outline),
1054 ),
5ad24bd7
TM
1055 Tab(
1056 text: 'Groups',
1057 icon: Icon(Icons.group),
1058 ),
1059 Tab(
1060 text: 'Roles',
1061 icon: Icon(Icons.lock_open),
1062 ),
1063 Tab(
1064 text: 'Domains',
1065 icon: Icon(Icons.domain),
1066 )
1067 ]),
1068 ),
1069 body: ProxmoxStreamBuilder<PveAccessManagementBloc,
1070 PveAccessManagementState>(
1071 bloc: aBloc,
1072 builder: (context, aState) {
1073 return TabBarView(children: [
1074 ListView.builder(
1075 itemCount: aState.users.length,
1076 itemBuilder: (context, index) {
1077 final user = aState.users[index];
1078 return ListTile(
1079 title: Text(user.userid),
1080 subtitle: Text(user.email ?? ''),
1081 trailing: aState.apiUser == user.userid
1082 ? Icon(Icons.person_pin_circle)
1083 : null,
1084 );
1085 }),
b6823e19
TM
1086 ListView.builder(
1087 itemCount: aState.tokens.length,
1088 itemBuilder: (context, index) {
1089 final token = aState.tokens[index];
1090 var expireDate = 'infinite';
1091 if (token.expire != null) {
598a93b4 1092 expireDate = DateFormat.yMd().format(token.expire!);
b6823e19
TM
1093 }
1094
1095 return ListTile(
1096 title: Text('${token.userid} ${token.tokenid}'),
1097 subtitle: Text('Expires: $expireDate'),
1098 );
1099 }),
5ad24bd7
TM
1100 ListView.builder(
1101 itemCount: aState.groups.length,
1102 itemBuilder: (context, index) {
1103 final group = aState.groups[index];
598a93b4
TL
1104 final users = (group.users?.isNotEmpty ?? false)
1105 ? group.users!.split(',')
1106 : [];
5ad24bd7
TM
1107 return ListTile(
1108 title: Text(group.groupid),
1109 subtitle: Text(group.comment ?? ''),
1110 trailing: Icon(Icons.arrow_right),
1111 onTap: () => showModalBottomSheet(
1112 shape: RoundedRectangleBorder(
1113 borderRadius: BorderRadius.vertical(
1114 top: Radius.circular(10))),
1115 context: context,
1116 builder: (context) {
1117 return Container(
1118 height: MediaQuery.of(context).size.height * 0.5,
1119 child: Column(
1120 crossAxisAlignment: CrossAxisAlignment.start,
1121 children: <Widget>[
1122 Padding(
1123 padding: EdgeInsets.fromLTRB(0, 5, 0, 5),
1124 child: Align(
1125 alignment: Alignment.topCenter,
1126 child: Container(
1127 width: 40,
1128 height: 3,
1129 color: Colors.black,
1130 ),
1131 ),
1132 ),
1133 ListTile(
1134 title:
1135 Text('Group members (${users.length})'),
1136 ),
1137 Divider(),
1138 Expanded(
1139 child: Padding(
1140 padding: const EdgeInsets.all(14.0),
1141 child: ListView.builder(
1142 itemCount: users.length,
1143 itemBuilder: (context, index) =>
1144 ListTile(
1145 title: Text(users[index]),
1146 ),
1147 ),
1148 ),
1149 )
1150 ],
1151 ),
1152 );
1153 },
1154 ),
1155 );
1156 }),
1157 ListView.builder(
1158 itemCount: aState.roles.length,
1159 itemBuilder: (context, index) {
1160 final role = aState.roles[index];
1161 final perms = role.privs.split(',');
1162 return ListTile(
1163 title: Text(role.roleid),
598a93b4
TL
1164 subtitle: Text((role.special ?? false)
1165 ? 'Built in Role'
1166 : 'Custom'),
5ad24bd7
TM
1167 trailing: Icon(Icons.arrow_right),
1168 onTap: () => showModalBottomSheet(
1169 shape: RoundedRectangleBorder(
1170 borderRadius: BorderRadius.vertical(
1171 top: Radius.circular(10))),
1172 context: context,
1173 builder: (context) {
1174 return Container(
1175 height: MediaQuery.of(context).size.height * 0.5,
1176 child: Column(
1177 crossAxisAlignment: CrossAxisAlignment.start,
1178 children: <Widget>[
1179 Padding(
1180 padding: EdgeInsets.fromLTRB(0, 5, 0, 5),
1181 child: Align(
1182 alignment: Alignment.topCenter,
1183 child: Container(
1184 width: 40,
1185 height: 3,
1186 color: Colors.black,
1187 ),
1188 ),
1189 ),
1190 ListTile(
1191 title: Text('Privileges (${perms.length})'),
1192 ),
1193 Divider(),
1194 Expanded(
1195 child: Padding(
1196 padding: const EdgeInsets.all(14.0),
1197 child: ListView.builder(
1198 itemCount: perms.length,
1199 itemBuilder: (context, index) =>
1200 ListTile(
1201 title: Text(perms[index]),
1202 ),
1203 ),
1204 ),
1205 )
1206 ],
1207 ),
1208 );
1209 },
1210 ),
1211 );
1212 }),
1213 ListView.builder(
1214 itemCount: aState.domains.length,
1215 itemBuilder: (context, index) {
1216 final domain = aState.domains[index];
1217 return ListTile(
1218 title: Text(domain.realm),
1219 subtitle: Text(domain.comment ?? ''),
1220 trailing: domain.tfa?.isNotEmpty ?? false
1221 ? Icon(Icons.looks_two)
1222 : null,
1223 );
1224 }),
1225 ]);
1226 }),
1227 bottomNavigationBar: PveMobileBottomNavigationbar(),
1228 ),
1229 );
1230 }
1231}