[pve-devel] [PATCH pve-flutter-frontend v2] node overview: add power settings menu

Dominik Csapak d.csapak at proxmox.com
Wed Apr 17 10:33:05 CEST 2024


On 4/17/24 10:19, Folke Gleumes wrote:
> On Wed, 2024-04-17 at 08:45 +0200, Dominik Csapak wrote:
>> similar to how it works for qemu, but add a confirmation dialog
>> so one does not accidentally shutdown or reboot a node.
>>
>> Signed-off-by: Dominik Csapak <d.csapak at proxmox.com>
>> ---
>> changes from v1:
>> * add an AlertDialog as confirmation before executing the action
>>
>>   lib/bloc/pve_node_overview_bloc.dart          | 11 +++
>>   lib/widgets/pve_node_overview.dart            | 24 ++++++
>>   .../pve_node_power_settings_widget.dart       | 84
>> +++++++++++++++++++
>>   3 files changed, 119 insertions(+)
>>   create mode 100644 lib/widgets/pve_node_power_settings_widget.dart
>>
>> diff --git a/lib/bloc/pve_node_overview_bloc.dart
>> b/lib/bloc/pve_node_overview_bloc.dart
>> index d14ff79..19d6563 100644
>> --- a/lib/bloc/pve_node_overview_bloc.dart
>> +++ b/lib/bloc/pve_node_overview_bloc.dart
>> @@ -57,9 +57,20 @@ class PveNodeOverviewBloc
>>         final disks = await apiClient.getNodeDisksList(nodeID);
>>         yield latestState.rebuild((b) => b..disks.replace(disks));
>>       }
>> +    if (event is PerformNodeAction) {
>> +      await apiClient.doResourceAction(nodeID, '', 'node',
>> event.action,
>> +          parameters: <String, String>{});
>> +      yield latestState;
>> +    }
>>     }
>>   }
>>   
>>   abstract class PveNodeOverviewEvent {}
>>   
>>   class UpdateNodeStatus extends PveNodeOverviewEvent {}
>> +
>> +class PerformNodeAction extends PveNodeOverviewEvent {
>> +  final PveClusterResourceAction action;
>> +
>> +  PerformNodeAction(this.action);
>> +}
>> diff --git a/lib/widgets/pve_node_overview.dart
>> b/lib/widgets/pve_node_overview.dart
>> index 7b65c0e..ad9a3b2 100644
>> --- a/lib/widgets/pve_node_overview.dart
>> +++ b/lib/widgets/pve_node_overview.dart
>> @@ -8,6 +8,7 @@ import
>> 'package:pve_flutter_frontend/states/pve_node_overview_state.dart';
>>   import
>> 'package:pve_flutter_frontend/states/pve_task_log_state.dart';
>>   import 'package:pve_flutter_frontend/utils/renderers.dart';
>>   import 'package:pve_flutter_frontend/utils/utils.dart';
>> +import
>> 'package:pve_flutter_frontend/widgets/pve_node_power_settings_widget.
>> dart';
>>   import
>> 'package:pve_flutter_frontend/widgets/proxmox_capacity_indicator.dart
>> ';
>>   import
>> 'package:pve_flutter_frontend/widgets/proxmox_stream_builder_widget.d
>> art';
>>   import
>> 'package:pve_flutter_frontend/widgets/pve_action_card_widget.dart';
>> @@ -189,6 +190,16 @@ class PveNodeOverview extends StatelessWidget {
>>                         child: Row(
>>                           mainAxisAlignment:
>> MainAxisAlignment.spaceEvenly,
>>                           children: <Widget>[
>> +                          ActionCard(
>> +                            icon: const Icon(
>> +                              Icons.power_settings_new,
>> +                              size: 55,
>> +                              color: Colors.white24,
>> +                            ),
>> +                            title: 'Power Settings',
>> +                            onTap: () =>
>> +                                showPowerMenuBottomSheet(context,
>> nBloc),
>> +                          ),
>>                             ActionCard(
>>                               icon: const Icon(
>>                                 Icons.queue_play_next,
>> @@ -443,4 +454,17 @@ class PveNodeOverview extends StatelessWidget {
>>         },
>>       );
>>     }
>> +
>> +  Future<T?> showPowerMenuBottomSheet<T>(
>> +      BuildContext context, PveNodeOverviewBloc nodeBloc) async {
>> +    return showModalBottomSheet(
>> +      shape: const RoundedRectangleBorder(
>> +          borderRadius: BorderRadius.vertical(top:
>> Radius.circular(10))),
>> +      context: context,
>> +      builder: (context) => Provider.value(
>> +        value: nodeBloc,
>> +        child: const PveNodePowerSettings(),
>> +      ),
>> +    );
>> +  }
>>   }
>> diff --git a/lib/widgets/pve_node_power_settings_widget.dart
>> b/lib/widgets/pve_node_power_settings_widget.dart
>> new file mode 100644
>> index 0000000..621ac68
>> --- /dev/null
>> +++ b/lib/widgets/pve_node_power_settings_widget.dart
>> @@ -0,0 +1,84 @@
>> +import 'package:flutter/material.dart';
>> +import 'package:provider/provider.dart';
>> +import
>> 'package:proxmox_dart_api_client/proxmox_dart_api_client.dart';
>> +import
>> 'package:pve_flutter_frontend/bloc/pve_node_overview_bloc.dart';
>> +import
>> 'package:pve_flutter_frontend/states/pve_node_overview_state.dart';
>> +import
>> 'package:pve_flutter_frontend/widgets/proxmox_stream_builder_widget.d
>> art';
>> +
>> +class PveNodePowerSettings extends StatelessWidget {
>> +  const PveNodePowerSettings({
>> +    super.key,
>> +  });
>> +  @override
>> +  Widget build(BuildContext context) {
>> +    final bloc = Provider.of<PveNodeOverviewBloc>(context);
>> +    return ProxmoxStreamBuilder<PveNodeOverviewBloc,
>> PveNodeOverviewState>(
>> +        bloc: bloc,
>> +        builder: (context, state) {
>> +          return SafeArea(
>> +            child: SingleChildScrollView(
>> +              child: Container(
>> +                constraints: BoxConstraints(
>> +                    minHeight: MediaQuery.of(context).size.height /
>> 3),
>> +                child: Column(
>> +                  mainAxisSize: MainAxisSize.min,
>> +                  children: <Widget>[
>> +                    ListTile(
>> +                      leading: const Icon(Icons.autorenew),
>> +                      title: const Text(
>> +                        "Reboot",
>> +                        style: TextStyle(fontWeight:
>> FontWeight.bold),
>> +                      ),
>> +                      subtitle: const Text("Reboot Node"),
>> +                      onTap: () => action(context,
>> +                          PveClusterResourceAction.reboot, bloc,
>> "reboot"),
>> +                    ),
>> +                    ListTile(
>> +                      leading: const Icon(Icons.power_settings_new),
>> +                      title: const Text(
>> +                        "Shutdown",
>> +                        style: TextStyle(fontWeight:
>> FontWeight.bold),
>> +                      ),
>> +                      subtitle: const Text("Shutdown Node"),
>> +                      onTap: () => action(context,
>> +                          PveClusterResourceAction.shutdown, bloc,
>> "shutdown"),
>> +                    ),
>> +                  ],
>> +                ),
>> +              ),
>> +            ),
>> +          );
>> +        });
>> +  }
>> +
>> +  void action(BuildContext context, PveClusterResourceAction action,
>> +      PveNodeOverviewBloc bloc, String actionText) async {
>> +    if (await showDialog(
>> +        context: context,
>> +        builder: (context) {
>> +          return AlertDialog(
>> +            contentPadding: const EdgeInsets.fromLTRB(24.0, 12.0,
>> 24.0, 16.0),
>> +            title: const Row(
>> +              mainAxisAlignment: MainAxisAlignment.spaceBetween,
>> +              children: <Widget>[
>> +                Text('Confirm'),
>> +                Icon(Icons.warning),
>> +              ],
>> +            ),
>> +            content: Text(
>> +                "Are you sure you want to $actionText node
>> '${bloc.nodeID}'?"),
>> +            actions: [
>> +              TextButton(
>> +                  onPressed: () => Navigator.of(context).pop(true),
>> +                  child: const Text("Yes")),
>> +              TextButton(
>> +                  onPressed: () => Navigator.of(context).pop(false),
>> +                  child: const Text("No"))
>> +            ],
> 
> The order of buttons probably should be reversed. According to material
> guidelines a dismissive action has to be placed to the left of
> confirming actions [0]. In practice, I accidentally shut down my test
> VM because I intuitively went for the leftmost button to abort. To make
> the actions even more obvious, they could be phrased as Cancel and
> Shutdown/Reboot.

fair enough, i used the same order as we do for the desktop ui
(left yes, right no), but that is probably also not super ideal?

i'll send a v3

> 
>> +          );
>> +        })) {
>> +      bloc.events.add(PerformNodeAction(action));
>> +      if (context.mounted) Navigator.of(context).pop();
>> +    }
>> +  }
>> +}
> 
> Besides the button ordering, everything worked well, so if those are
> changed, consider this:
> 
> Tested-by: Folke Gleumes <f.gleumes at proxmox.com>
> 
> [0] https://m2.material.io/components/dialogs#actions
> _______________________________________________
> pve-devel mailing list
> pve-devel at lists.proxmox.com
> https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel





More information about the pve-devel mailing list