]>
Commit | Line | Data |
---|---|---|
3628aaea | 1 | import 'package:built_collection/built_collection.dart'; |
00a48038 | 2 | import 'package:flutter/material.dart'; |
3628aaea TM |
3 | import 'package:flutter/rendering.dart'; |
4 | import 'package:font_awesome_flutter/font_awesome_flutter.dart'; | |
b6823e19 | 5 | import 'package:intl/intl.dart'; |
3628aaea TM |
6 | import 'package:provider/provider.dart'; |
7 | import 'package:proxmox_dart_api_client/proxmox_dart_api_client.dart'; | |
5ad24bd7 | 8 | import 'package:pve_flutter_frontend/bloc/pve_access_management_bloc.dart'; |
3628aaea TM |
9 | import 'package:pve_flutter_frontend/bloc/pve_authentication_bloc.dart'; |
10 | import 'package:pve_flutter_frontend/bloc/pve_cluster_status_bloc.dart'; | |
11 | import 'package:pve_flutter_frontend/bloc/pve_file_selector_bloc.dart'; | |
12 | import 'package:pve_flutter_frontend/bloc/pve_resource_bloc.dart'; | |
13 | import 'package:pve_flutter_frontend/bloc/pve_storage_selector_bloc.dart'; | |
5ad24bd7 | 14 | import 'package:pve_flutter_frontend/states/pve_access_management_state.dart'; |
3628aaea TM |
15 | import 'package:pve_flutter_frontend/states/pve_cluster_status_state.dart'; |
16 | import 'package:pve_flutter_frontend/states/pve_file_selector_state.dart'; | |
17 | import 'package:pve_flutter_frontend/states/pve_resource_state.dart'; | |
18 | import 'package:pve_flutter_frontend/states/pve_storage_selector_state.dart'; | |
19 | import 'package:pve_flutter_frontend/utils/renderers.dart'; | |
3628aaea | 20 | import 'package:pve_flutter_frontend/widgets/proxmox_capacity_indicator.dart'; |
ee36e7ca | 21 | import 'package:pve_flutter_frontend/widgets/proxmox_custom_icon.dart'; |
bbc12f2a TM |
22 | import 'package:pve_flutter_frontend/widgets/proxmox_gauge_chart.dart'; |
23 | import 'package:pve_flutter_frontend/widgets/proxmox_heartbeat_indicator.dart'; | |
3628aaea TM |
24 | import 'package:pve_flutter_frontend/widgets/proxmox_stream_builder_widget.dart'; |
25 | import 'package:pve_flutter_frontend/widgets/pve_file_selector_widget.dart'; | |
5b5b3f73 | 26 | import 'package:pve_flutter_frontend/widgets/pve_guest_icon_widget.dart'; |
3628aaea | 27 | import 'package:pve_flutter_frontend/widgets/pve_help_icon_button_widget.dart'; |
c96ce032 | 28 | import 'package:pve_flutter_frontend/widgets/pve_resource_data_card_widget.dart'; |
3628aaea | 29 | import 'package:pve_flutter_frontend/widgets/pve_resource_status_chip_widget.dart'; |
bbc12f2a | 30 | import 'package:pve_flutter_frontend/widgets/pve_subscription_alert_dialog.dart'; |
9bb60c76 | 31 | import 'package:pve_flutter_frontend/utils/proxmox_colors.dart'; |
3628aaea | 32 | import 'package:rxdart/rxdart.dart'; |
00a48038 | 33 | |
3628aaea TM |
34 | class MainLayoutSlim extends StatefulWidget { |
35 | @override | |
36 | _MainLayoutSlimState createState() => _MainLayoutSlimState(); | |
37 | } | |
38 | ||
39 | class _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 | ||
113 | class 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 | ||
153 | class 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 | ||
549 | class 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 | ||
584 | class 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 | ||
677 | class 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 | ||
716 | class 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 | ||
776 | class 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 | ||
784 | class _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 | ||
838 | class _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 | ||
1010 | class 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 | |
1034 | class 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 | } |