[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