]>
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, | |
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 | ||
549 | class 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 | ||
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 | }, | |
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 | ||
676 | class 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 | ||
715 | class 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 | ||
775 | class 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 | ||
783 | class _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 | ||
837 | class _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 | ||
1009 | class 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 | |
1033 | class 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 | } |