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