]>
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 | 121 | items: [ |
5d2e7931 | 122 | const BottomNavigationBarItem( |
9bb60c76 TL |
123 | icon: Icon(Icons.dashboard), |
124 | label: "Dashboard", | |
125 | ), | |
5d2e7931 | 126 | const BottomNavigationBarItem( |
9bb60c76 | 127 | icon: Icon(Icons.developer_board), |
453d9410 | 128 | label: "Resources", |
5ad24bd7 | 129 | ), |
5d2e7931 | 130 | const BottomNavigationBarItem( |
9bb60c76 | 131 | icon: Icon(Icons.supervised_user_circle), |
453d9410 | 132 | label: "Access", |
5ad24bd7 | 133 | ), |
5d2e7931 | 134 | const 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( |
5d2e7931 | 160 | title: const Column( |
e6238281 TM |
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, | |
5d2e7931 | 178 | leading: const Icon( |
ee36e7ca TM |
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( | |
5d2e7931 | 204 | title: const Text( |
bbc12f2a TM |
205 | "Status", |
206 | style: TextStyle( | |
207 | fontWeight: FontWeight.bold, | |
208 | color: Colors.white, | |
209 | ), | |
210 | ), | |
211 | subtitle: Text( | |
212 | cState.cluster?.name ?? "Datacenter", | |
5d2e7931 | 213 | style: const TextStyle( |
bbc12f2a TM |
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( | |
5d2e7931 | 231 | padding: const EdgeInsets.symmetric(horizontal: 8), |
bbc12f2a TM |
232 | shrinkWrap: true, |
233 | scrollDirection: Axis.horizontal, | |
234 | children: [ | |
235 | if (cState.missingSubscription) | |
236 | ActionChip( | |
9bb60c76 | 237 | backgroundColor: |
cfb55e5a | 238 | Theme.of(context).colorScheme.primaryContainer, |
5d2e7931 TL |
239 | avatar: const Icon(Icons.report, color: Colors.red), |
240 | label: const Text( | |
bbc12f2a TM |
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( | |
5d2e7931 | 254 | padding: const EdgeInsets.symmetric( |
9bb60c76 TL |
255 | vertical: 4.0, horizontal: 8.0), |
256 | backgroundColor: | |
cfb55e5a | 257 | Theme.of(context).colorScheme.primaryContainer, |
bbc12f2a TM |
258 | avatar: Icon( |
259 | Renderers.getDefaultResourceIcon('qemu'), | |
9bb60c76 | 260 | color: Theme.of(context).colorScheme.onPrimary, |
bbc12f2a TM |
261 | size: 20, |
262 | ), | |
5d2e7931 | 263 | label: const Text( |
bbc12f2a TM |
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( | |
5d2e7931 | 284 | padding: const EdgeInsets.symmetric( |
9bb60c76 TL |
285 | vertical: 4.0, horizontal: 8.0), |
286 | backgroundColor: | |
cfb55e5a | 287 | Theme.of(context).colorScheme.primaryContainer, |
bbc12f2a TM |
288 | avatar: Icon( |
289 | Renderers.getDefaultResourceIcon('lxc'), | |
9bb60c76 | 290 | color: Theme.of(context).colorScheme.onPrimary, |
bbc12f2a TM |
291 | size: 20, |
292 | ), | |
5d2e7931 | 293 | label: const Text( |
bbc12f2a TM |
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( | |
5d2e7931 | 334 | title: const Text( |
bbc12f2a TM |
335 | 'Analytics', |
336 | style: TextStyle( | |
337 | fontWeight: FontWeight.bold, | |
338 | fontSize: 20, | |
339 | ), | |
340 | ), | |
5d2e7931 | 341 | subtitle: const Text('Usage across all online nodes'), |
bbc12f2a | 342 | children: [ |
35e7d210 | 343 | Padding( |
bbc12f2a TM |
344 | padding: const EdgeInsets.symmetric(vertical: 16.0), |
345 | child: ProxmoxGaugeChartListTile( | |
5d2e7931 | 346 | title: const Text('CPU'), |
bbc12f2a TM |
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( | |
5d2e7931 | 357 | title: const Text('Memory'), |
bbc12f2a TM |
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 | 368 | PveResourceDataCardWidget( |
5d2e7931 | 369 | title: const Text( |
bbc12f2a TM |
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 | 399 | return PveResourceDataCardWidget( |
5d2e7931 | 400 | title: const Text( |
bbc12f2a TM |
401 | 'Guests', |
402 | style: TextStyle( | |
403 | fontWeight: FontWeight.bold, | |
404 | fontSize: 20, | |
405 | ), | |
406 | ), | |
407 | children: <Widget>[ | |
408 | ListTile( | |
5d2e7931 | 409 | title: const 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 | 426 | dense: true, |
5d2e7931 | 427 | title: const Text( |
d48d8e9d TM |
428 | "Online", |
429 | style: TextStyle(fontSize: 14), | |
430 | ), | |
5d2e7931 | 431 | leading: const Icon(Icons.play_circle_outline, |
d48d8e9d TM |
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, | |
5d2e7931 | 449 | title: const Text( |
bbc12f2a | 450 | "Offline", |
f5aa4476 TM |
451 | style: TextStyle( |
452 | fontSize: 14, | |
453 | ), | |
bbc12f2a | 454 | ), |
5d2e7931 | 455 | leading: const Icon(Icons.stop), |
bbc12f2a | 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 | ), |
5d2e7931 | 471 | const Divider( |
d84177c2 TM |
472 | indent: 10, |
473 | ), | |
bbc12f2a | 474 | ListTile( |
5d2e7931 | 475 | title: const 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, | |
5d2e7931 | 493 | title: const Text( |
bbc12f2a TM |
494 | "Online", |
495 | style: TextStyle(fontSize: 14), | |
496 | ), | |
5d2e7931 | 497 | leading: const Icon(Icons.play_circle_outline, |
bbc12f2a TM |
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, | |
5d2e7931 | 516 | title: const Text( |
bbc12f2a | 517 | "Offline", |
c96ce032 | 518 | style: TextStyle( |
f5aa4476 TM |
519 | fontSize: 14, |
520 | ), | |
35e7d210 | 521 | ), |
5d2e7931 | 522 | leading: const Icon(Icons.stop), |
bbc12f2a | 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, | |
5d2e7931 | 611 | separatorBuilder: (context, index) => const Divider(), |
4e30c09d TM |
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(), | |
5d2e7931 | 641 | style: const TextStyle( |
4e30c09d TM |
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 | |
5d2e7931 | 655 | return const ListTile( |
4e30c09d TM |
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, | |
5d2e7931 TL |
821 | contentPadding: const EdgeInsets.fromLTRB(20, 5, 8, 5), |
822 | prefixIconConstraints: const BoxConstraints(minHeight: 32, minWidth: 32), | |
823 | suffixIconConstraints: const BoxConstraints(maxHeight: 32, maxWidth: 32), | |
9bb60c76 TL |
824 | //fillColor: Color(0xFFF1F2F4), |
825 | fillColor: Theme.of(context).colorScheme.surface, | |
826 | filled: true, | |
827 | isDense: true, | |
828 | enabledBorder: | |
5d2e7931 | 829 | const OutlineInputBorder(borderSide: BorderSide(color: Colors.white)), |
9bb60c76 | 830 | ), |
5d2e7931 | 831 | style: const 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( | |
5d2e7931 | 853 | title: const Text( |
57d2bbd2 | 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 | 868 | ), |
5d2e7931 | 869 | const Divider( |
57d2bbd2 TM |
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: [ | |
5d2e7931 | 877 | const ListTile( |
57d2bbd2 TM |
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: [ | |
5d2e7931 | 951 | const ListTile( |
d48d8e9d TM |
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 | |
5d2e7931 | 1020 | ? const Icon( |
d48d8e9d TM |
1021 | FontAwesomeIcons.filter, |
1022 | color: Colors.black, | |
1023 | ) | |
5d2e7931 | 1024 | : const Icon( |
d48d8e9d TM |
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( | |
5d2e7931 | 1042 | title: const Text('Permissions'), |
5ad24bd7 TM |
1043 | //backgroundColor: Colors.transparent, |
1044 | elevation: 0.0, | |
1045 | automaticallyImplyLeading: false, | |
b876878b TL |
1046 | bottom: TabBar( |
1047 | isScrollable: true, | |
1048 | labelStyle: | |
1049 | TextStyle(color: Theme.of(context).colorScheme.onPrimary), | |
1050 | tabs: [ | |
5d2e7931 | 1051 | const Tab( |
b876878b TL |
1052 | text: 'Users', |
1053 | icon: Icon(Icons.person), | |
1054 | ), | |
5d2e7931 | 1055 | const Tab( |
b876878b TL |
1056 | text: 'API Tokens', |
1057 | icon: Icon(Icons.person_outline), | |
1058 | ), | |
5d2e7931 | 1059 | const Tab( |
b876878b TL |
1060 | text: 'Groups', |
1061 | icon: Icon(Icons.group), | |
1062 | ), | |
5d2e7931 | 1063 | const Tab( |
b876878b TL |
1064 | text: 'Roles', |
1065 | icon: Icon(Icons.lock_open), | |
1066 | ), | |
5d2e7931 | 1067 | const Tab( |
b876878b TL |
1068 | text: 'Domains', |
1069 | icon: Icon(Icons.domain), | |
1070 | ) | |
1071 | ]), | |
5ad24bd7 TM |
1072 | ), |
1073 | body: ProxmoxStreamBuilder<PveAccessManagementBloc, | |
1074 | PveAccessManagementState>( | |
1075 | bloc: aBloc, | |
1076 | builder: (context, aState) { | |
1077 | return TabBarView(children: [ | |
1078 | ListView.builder( | |
1079 | itemCount: aState.users.length, | |
1080 | itemBuilder: (context, index) { | |
1081 | final user = aState.users[index]; | |
1082 | return ListTile( | |
1083 | title: Text(user.userid), | |
1084 | subtitle: Text(user.email ?? ''), | |
1085 | trailing: aState.apiUser == user.userid | |
5d2e7931 | 1086 | ? const Icon(Icons.person_pin_circle) |
5ad24bd7 TM |
1087 | : null, |
1088 | ); | |
1089 | }), | |
b6823e19 TM |
1090 | ListView.builder( |
1091 | itemCount: aState.tokens.length, | |
1092 | itemBuilder: (context, index) { | |
1093 | final token = aState.tokens[index]; | |
1094 | var expireDate = 'infinite'; | |
1095 | if (token.expire != null) { | |
598a93b4 | 1096 | expireDate = DateFormat.yMd().format(token.expire!); |
b6823e19 TM |
1097 | } |
1098 | ||
1099 | return ListTile( | |
1100 | title: Text('${token.userid} ${token.tokenid}'), | |
1101 | subtitle: Text('Expires: $expireDate'), | |
1102 | ); | |
1103 | }), | |
5ad24bd7 TM |
1104 | ListView.builder( |
1105 | itemCount: aState.groups.length, | |
1106 | itemBuilder: (context, index) { | |
1107 | final group = aState.groups[index]; | |
598a93b4 TL |
1108 | final users = (group.users?.isNotEmpty ?? false) |
1109 | ? group.users!.split(',') | |
1110 | : []; | |
5ad24bd7 TM |
1111 | return ListTile( |
1112 | title: Text(group.groupid), | |
1113 | subtitle: Text(group.comment ?? ''), | |
5d2e7931 | 1114 | trailing: const Icon(Icons.arrow_right), |
5ad24bd7 | 1115 | onTap: () => showModalBottomSheet( |
5d2e7931 | 1116 | shape: const RoundedRectangleBorder( |
5ad24bd7 TM |
1117 | borderRadius: BorderRadius.vertical( |
1118 | top: Radius.circular(10))), | |
1119 | context: context, | |
1120 | builder: (context) { | |
1121 | return Container( | |
1122 | height: MediaQuery.of(context).size.height * 0.5, | |
1123 | child: Column( | |
1124 | crossAxisAlignment: CrossAxisAlignment.start, | |
1125 | children: <Widget>[ | |
1126 | Padding( | |
5d2e7931 | 1127 | padding: const EdgeInsets.fromLTRB(0, 5, 0, 5), |
5ad24bd7 TM |
1128 | child: Align( |
1129 | alignment: Alignment.topCenter, | |
1130 | child: Container( | |
1131 | width: 40, | |
1132 | height: 3, | |
1133 | color: Colors.black, | |
1134 | ), | |
1135 | ), | |
1136 | ), | |
1137 | ListTile( | |
1138 | title: | |
1139 | Text('Group members (${users.length})'), | |
1140 | ), | |
5d2e7931 | 1141 | const Divider(), |
5ad24bd7 TM |
1142 | Expanded( |
1143 | child: Padding( | |
1144 | padding: const EdgeInsets.all(14.0), | |
1145 | child: ListView.builder( | |
1146 | itemCount: users.length, | |
1147 | itemBuilder: (context, index) => | |
1148 | ListTile( | |
1149 | title: Text(users[index]), | |
1150 | ), | |
1151 | ), | |
1152 | ), | |
1153 | ) | |
1154 | ], | |
1155 | ), | |
1156 | ); | |
1157 | }, | |
1158 | ), | |
1159 | ); | |
1160 | }), | |
1161 | ListView.builder( | |
1162 | itemCount: aState.roles.length, | |
1163 | itemBuilder: (context, index) { | |
1164 | final role = aState.roles[index]; | |
1165 | final perms = role.privs.split(','); | |
1166 | return ListTile( | |
1167 | title: Text(role.roleid), | |
598a93b4 TL |
1168 | subtitle: Text((role.special ?? false) |
1169 | ? 'Built in Role' | |
1170 | : 'Custom'), | |
5d2e7931 | 1171 | trailing: const Icon(Icons.arrow_right), |
5ad24bd7 | 1172 | onTap: () => showModalBottomSheet( |
5d2e7931 | 1173 | shape: const RoundedRectangleBorder( |
5ad24bd7 TM |
1174 | borderRadius: BorderRadius.vertical( |
1175 | top: Radius.circular(10))), | |
1176 | context: context, | |
1177 | builder: (context) { | |
1178 | return Container( | |
1179 | height: MediaQuery.of(context).size.height * 0.5, | |
1180 | child: Column( | |
1181 | crossAxisAlignment: CrossAxisAlignment.start, | |
1182 | children: <Widget>[ | |
1183 | Padding( | |
5d2e7931 | 1184 | padding: const EdgeInsets.fromLTRB(0, 5, 0, 5), |
5ad24bd7 TM |
1185 | child: Align( |
1186 | alignment: Alignment.topCenter, | |
1187 | child: Container( | |
1188 | width: 40, | |
1189 | height: 3, | |
1190 | color: Colors.black, | |
1191 | ), | |
1192 | ), | |
1193 | ), | |
1194 | ListTile( | |
1195 | title: Text('Privileges (${perms.length})'), | |
1196 | ), | |
5d2e7931 | 1197 | const Divider(), |
5ad24bd7 TM |
1198 | Expanded( |
1199 | child: Padding( | |
1200 | padding: const EdgeInsets.all(14.0), | |
1201 | child: ListView.builder( | |
1202 | itemCount: perms.length, | |
1203 | itemBuilder: (context, index) => | |
1204 | ListTile( | |
1205 | title: Text(perms[index]), | |
1206 | ), | |
1207 | ), | |
1208 | ), | |
1209 | ) | |
1210 | ], | |
1211 | ), | |
1212 | ); | |
1213 | }, | |
1214 | ), | |
1215 | ); | |
1216 | }), | |
1217 | ListView.builder( | |
1218 | itemCount: aState.domains.length, | |
1219 | itemBuilder: (context, index) { | |
1220 | final domain = aState.domains[index]; | |
1221 | return ListTile( | |
1222 | title: Text(domain.realm), | |
1223 | subtitle: Text(domain.comment ?? ''), | |
1224 | trailing: domain.tfa?.isNotEmpty ?? false | |
5d2e7931 | 1225 | ? const Icon(Icons.looks_two) |
5ad24bd7 TM |
1226 | : null, |
1227 | ); | |
1228 | }), | |
1229 | ]); | |
1230 | }), | |
1231 | bottomNavigationBar: PveMobileBottomNavigationbar(), | |
1232 | ), | |
1233 | ); | |
1234 | } | |
1235 | } |