public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [pve-devel] [PATCH pve_flutter_frontend v3] feat: ui: add lock/unlock button in guests options page
@ 2025-09-04  9:25 Shan Shaji
  0 siblings, 0 replies; only message in thread
From: Shan Shaji @ 2025-09-04  9:25 UTC (permalink / raw)
  To: pve-devel; +Cc: Thomas Lamprecht

On the options page for VMs and CTs it was easy to change the
configs by mistake. To avoid that, added a lock/unlock button
on top of the screen. The toggle buttons will only be enabled
if the button is clicked.

Suggested-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
Signed-off-by: Shan Shaji <s.shaji@proxmox.com>
---
changes since v2:
 - Remove edit icon and text, instead used lock/unlock icon.
 - Allow users to lock the options again without closing the page. 
 - Update commit message.
 - Removed unintended formatting changes and rename `isLockOptions` to
   `isOptionsLocked` 

 changes since v1:
 - Rebase with master.  
 
lib/bloc/pve_lxc_overview_bloc.dart          | 11 ++++++
 lib/bloc/pve_qemu_overview_bloc.dart         | 11 ++++++
 lib/states/pve_lxc_overview_state.dart       | 21 ++++++-----
 lib/states/pve_qemu_overview_state.dart      | 21 ++++++-----
 lib/widgets/pve_config_switch_list_tile.dart |  4 ++-
 lib/widgets/pve_icon_button_widget.dart      | 38 ++++++++++++++++++++
 lib/widgets/pve_lxc_options_widget.dart      | 16 ++++++++-
 lib/widgets/pve_lxc_overview.dart            | 20 ++++++-----
 lib/widgets/pve_qemu_options_widget.dart     | 17 +++++++++
 lib/widgets/pve_qemu_overview.dart           |  2 +-
 10 files changed, 132 insertions(+), 29 deletions(-)
 create mode 100644 lib/widgets/pve_icon_button_widget.dart

diff --git a/lib/bloc/pve_lxc_overview_bloc.dart b/lib/bloc/pve_lxc_overview_bloc.dart
index e287f97..b339c08 100644
--- a/lib/bloc/pve_lxc_overview_bloc.dart
+++ b/lib/bloc/pve_lxc_overview_bloc.dart
@@ -89,6 +89,11 @@ class PveLxcOverviewBloc
         yield latestState.rebuild((b) => b..errorMessage = '');
       }
     }
+
+    if (event is LockLxcOptions) {
+      yield latestState
+          .rebuild((b) => b..isOptionsLocked = event.isOptionsLocked);
+    }
   }
 
   Future<List<PveGuestRRDdataModel>> _preProcessRRDdata() async {
@@ -131,3 +136,9 @@ class RevertPendingLxcConfig extends PveLxcOverviewEvent {
 
   RevertPendingLxcConfig(this.cField);
 }
+
+class LockLxcOptions extends PveLxcOverviewEvent {
+  final bool isOptionsLocked;
+
+  LockLxcOptions(this.isOptionsLocked);
+}
diff --git a/lib/bloc/pve_qemu_overview_bloc.dart b/lib/bloc/pve_qemu_overview_bloc.dart
index 3d0fd0e..57a4c93 100644
--- a/lib/bloc/pve_qemu_overview_bloc.dart
+++ b/lib/bloc/pve_qemu_overview_bloc.dart
@@ -94,6 +94,11 @@ class PveQemuOverviewBloc
         yield latestState.rebuild((b) => b..errorMessage = '');
       }
     }
+
+    if (event is LockQemuOptions) {
+      yield latestState
+          .rebuild((b) => b..isOptionsLocked = event.isOptionsLocked);
+    }
   }
 
   Future<List<PveGuestRRDdataModel>> _preProcessRRDdata() async {
@@ -136,3 +141,9 @@ class RevertPendingQemuConfig extends PveQemuOverviewEvent {
 
   RevertPendingQemuConfig(this.cField);
 }
+
+class LockQemuOptions extends PveQemuOverviewEvent {
+  final bool isOptionsLocked;
+
+  LockQemuOptions(this.isOptionsLocked);
+}
\ No newline at end of file
diff --git a/lib/states/pve_lxc_overview_state.dart b/lib/states/pve_lxc_overview_state.dart
index c10c2e7..a162121 100644
--- a/lib/states/pve_lxc_overview_state.dart
+++ b/lib/states/pve_lxc_overview_state.dart
@@ -10,6 +10,7 @@ abstract class PveLxcOverviewState
     implements Built<PveLxcOverviewState, PveLxcOverviewStateBuilder> {
   // Fields
   String get nodeID;
+  bool get isOptionsLocked;
   PveNodesLxcStatusModel? get currentStatus;
   BuiltList<PveGuestRRDdataModel>? get rrdData;
   PveNodesLxcConfigModel? get config;
@@ -20,13 +21,15 @@ abstract class PveLxcOverviewState
           [void Function(PveLxcOverviewStateBuilder) updates]) =
       _$PveLxcOverviewState;
 
-  factory PveLxcOverviewState.init(String nodeID) =>
-      PveLxcOverviewState((b) => b
-        //base
-        ..errorMessage = ''
-        ..isBlank = true
-        ..isLoading = false
-        ..isSuccess = false
-        //class
-        ..nodeID = nodeID);
+  factory PveLxcOverviewState.init(String nodeID) => PveLxcOverviewState(
+        (b) => b
+          //base
+          ..errorMessage = ''
+          ..isBlank = true
+          ..isLoading = false
+          ..isSuccess = false
+          //class
+          ..nodeID = nodeID
+          ..isOptionsLocked = true,
+      );
 }
diff --git a/lib/states/pve_qemu_overview_state.dart b/lib/states/pve_qemu_overview_state.dart
index 43201bc..8d8dd96 100644
--- a/lib/states/pve_qemu_overview_state.dart
+++ b/lib/states/pve_qemu_overview_state.dart
@@ -8,6 +8,7 @@ abstract class PveQemuOverviewState
     with PveBaseState
     implements Built<PveQemuOverviewState, PveQemuOverviewStateBuilder> {
   String get nodeID;
+  bool get isOptionsLocked;
   PveQemuStatusModel? get currentStatus;
   BuiltList<PveGuestRRDdataModel>? get rrdData;
   PveNodesQemuConfigModel? get config;
@@ -18,13 +19,15 @@ abstract class PveQemuOverviewState
           [void Function(PveQemuOverviewStateBuilder) updates]) =
       _$PveQemuOverviewState;
 
-  factory PveQemuOverviewState.init(String nodeID) =>
-      PveQemuOverviewState((b) => b
-        //base
-        ..errorMessage = ''
-        ..isBlank = true
-        ..isLoading = false
-        ..isSuccess = false
-        //class
-        ..nodeID = nodeID);
+  factory PveQemuOverviewState.init(String nodeID) => PveQemuOverviewState(
+        (b) => b
+          //base
+          ..errorMessage = ''
+          ..isBlank = true
+          ..isLoading = false
+          ..isSuccess = false
+          //class
+          ..nodeID = nodeID
+          ..isOptionsLocked = true,
+      );
 }
diff --git a/lib/widgets/pve_config_switch_list_tile.dart b/lib/widgets/pve_config_switch_list_tile.dart
index c209fbe..19ae13c 100644
--- a/lib/widgets/pve_config_switch_list_tile.dart
+++ b/lib/widgets/pve_config_switch_list_tile.dart
@@ -7,6 +7,7 @@ class PveConfigSwitchListTile extends StatelessWidget {
   final Widget? title;
   final ValueChanged<bool>? onChanged;
   final VoidCallback? onDeleted;
+  final bool disable;
 
   const PveConfigSwitchListTile({
     super.key,
@@ -16,6 +17,7 @@ class PveConfigSwitchListTile extends StatelessWidget {
     this.title,
     this.onChanged,
     this.onDeleted,
+    this.disable = false,
   });
   @override
   Widget build(BuildContext context) {
@@ -26,7 +28,7 @@ class PveConfigSwitchListTile extends StatelessWidget {
     return SwitchListTile(
       title: _getTitle(),
       value: pBool ?? value ?? defaultValue!,
-      onChanged: pending != null ? null : onChanged,
+      onChanged: disable || pending != null ? null : onChanged,
     );
   }
 
diff --git a/lib/widgets/pve_icon_button_widget.dart b/lib/widgets/pve_icon_button_widget.dart
new file mode 100644
index 0000000..8066cff
--- /dev/null
+++ b/lib/widgets/pve_icon_button_widget.dart
@@ -0,0 +1,38 @@
+import 'package:flutter/material.dart';
+
+class PveIconButton extends StatelessWidget {
+  final IconData icon;
+  final String label;
+  final Color? color;
+  final VoidCallback? onPressed;
+
+  const PveIconButton({
+    super.key,
+    required this.icon,
+    required this.label,
+    this.color,
+    this.onPressed,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return TextButton(
+      onPressed: onPressed,
+      child: Row(
+        spacing: 2,
+        children: [
+          Icon(
+            icon,
+            color: color ?? Theme.of(context).colorScheme.onPrimary,
+          ),
+          Text(
+            label,
+            style: TextStyle(
+              color: color ?? Theme.of(context).colorScheme.onPrimary,
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+}
diff --git a/lib/widgets/pve_lxc_options_widget.dart b/lib/widgets/pve_lxc_options_widget.dart
index 7ad0224..32a0da9 100644
--- a/lib/widgets/pve_lxc_options_widget.dart
+++ b/lib/widgets/pve_lxc_options_widget.dart
@@ -3,6 +3,7 @@ import 'package:pve_flutter_frontend/bloc/pve_lxc_overview_bloc.dart';
 import 'package:pve_flutter_frontend/states/pve_lxc_overview_state.dart';
 import 'package:pve_flutter_frontend/widgets/proxmox_stream_builder_widget.dart';
 import 'package:pve_flutter_frontend/widgets/pve_config_switch_list_tile.dart';
+import 'package:pve_flutter_frontend/widgets/pve_icon_button_widget.dart';
 
 class PveLxcOptions extends StatelessWidget {
   final PveLxcOverviewBloc? lxcBloc;
@@ -16,7 +17,17 @@ class PveLxcOptions extends StatelessWidget {
           final config = state.config;
           if (config != null) {
             return Scaffold(
-                appBar: AppBar(),
+                appBar: AppBar(
+                  actions: [
+                    PveIconButton(
+                      onPressed: () => lxcBloc!.events.add(
+                        LockLxcOptions(!state.isOptionsLocked),
+                      ),
+                      icon: state.isOptionsLocked ? Icons.lock_outline : Icons.lock_open_outlined,
+                      label: state.isOptionsLocked ? 'Unlock' : 'Lock',
+                    )
+                  ],
+                ),
                 body: SingleChildScrollView(
                   child: Column(
                     children: <Widget>[
@@ -25,6 +36,7 @@ class PveLxcOptions extends StatelessWidget {
                         subtitle: Text(config.hostname ?? 'undefined'),
                       ),
                       PveConfigSwitchListTile(
+                        disable: state.isOptionsLocked,
                         title: const Text("Start on boot"),
                         value: config.onboot,
                         defaultValue: false,
@@ -47,6 +59,7 @@ class PveLxcOptions extends StatelessWidget {
                         subtitle: Text("${config.arch}"),
                       ),
                       PveConfigSwitchListTile(
+                        disable: state.isOptionsLocked,
                         title: const Text("/dev/console"),
                         value: config.console,
                         defaultValue: true,
@@ -65,6 +78,7 @@ class PveLxcOptions extends StatelessWidget {
                         subtitle: Text(config.cmode?.name ?? 'tty'),
                       ),
                       PveConfigSwitchListTile(
+                        disable: state.isOptionsLocked,
                         title: const Text("Protection"),
                         value: config.protection,
                         defaultValue: false,
diff --git a/lib/widgets/pve_lxc_overview.dart b/lib/widgets/pve_lxc_overview.dart
index fe43a26..6ad2fa6 100644
--- a/lib/widgets/pve_lxc_overview.dart
+++ b/lib/widgets/pve_lxc_overview.dart
@@ -151,14 +151,18 @@ class PveLxcOverview extends StatelessWidget {
                                         state.nodeID,
                                         'lxc')),
                               createActionCard(
-                                  'Options',
-                                  Icons.settings,
-                                  () => Navigator.of(context)
-                                      .push(MaterialPageRoute(
-                                          builder: (context) => PveLxcOptions(
-                                                lxcBloc: lxcBloc,
-                                              ),
-                                          fullscreenDialog: true))),
+                                'Options',
+                                Icons.settings,
+                                () => Navigator.of(context).push(
+                                    MaterialPageRoute(
+                                      builder: (context) => PveLxcOptions(
+                                        lxcBloc: lxcBloc
+                                          ..events.add(LockLxcOptions(true)),
+                                      ),
+                                      fullscreenDialog: true,
+                                    ),
+                                  )
+                              ),
                               if (!resourceBloc.latestState.isStandalone)
                                 createActionCard(
                                     'Migrate',
diff --git a/lib/widgets/pve_qemu_options_widget.dart b/lib/widgets/pve_qemu_options_widget.dart
index 7ed0a3e..a7ddb0e 100644
--- a/lib/widgets/pve_qemu_options_widget.dart
+++ b/lib/widgets/pve_qemu_options_widget.dart
@@ -4,6 +4,7 @@ import 'package:pve_flutter_frontend/bloc/pve_qemu_overview_bloc.dart';
 import 'package:pve_flutter_frontend/states/pve_qemu_overview_state.dart';
 import 'package:pve_flutter_frontend/widgets/proxmox_stream_builder_widget.dart';
 import 'package:pve_flutter_frontend/widgets/pve_config_switch_list_tile.dart';
+import 'package:pve_flutter_frontend/widgets/pve_icon_button_widget.dart';
 
 class PveQemuOptions extends StatelessWidget {
   final String guestID;
@@ -24,6 +25,15 @@ class PveQemuOptions extends StatelessWidget {
                     icon: const Icon(Icons.close),
                     onPressed: () => Navigator.of(context).pop(),
                   ),
+                  actions: [
+                    PveIconButton(
+                      onPressed: () => bloc.events.add(
+                        LockQemuOptions(!state.isOptionsLocked),
+                      ),
+                      icon: state.isOptionsLocked ? Icons.lock_outline : Icons.lock_open_outlined,
+                      label: state.isOptionsLocked ? 'Unlock' : 'Lock',
+                    )
+                  ],
                 ),
                 body: SingleChildScrollView(
                   child: Form(
@@ -36,6 +46,7 @@ class PveQemuOptions extends StatelessWidget {
                           subtitle: Text(config.name ?? 'VM$guestID'),
                         ),
                         PveConfigSwitchListTile(
+                          disable: state.isOptionsLocked,
                           title: const Text("Start on boot"),
                           value: config.onboot,
                           defaultValue: false,
@@ -61,6 +72,7 @@ class PveQemuOptions extends StatelessWidget {
                           subtitle: Text(config.boot ?? 'Disk, Network, USB'),
                         ),
                         PveConfigSwitchListTile(
+                          disable: state.isOptionsLocked,
                           title: const Text("Use tablet for pointer"),
                           value: config.tablet,
                           defaultValue: true,
@@ -75,6 +87,7 @@ class PveQemuOptions extends StatelessWidget {
                           subtitle: Text(config.hotplug ?? 'disk,network,usb'),
                         ),
                         PveConfigSwitchListTile(
+                          disable: state.isOptionsLocked,
                           title: const Text("ACPI support"),
                           value: config.acpi,
                           defaultValue: true,
@@ -85,6 +98,7 @@ class PveQemuOptions extends StatelessWidget {
                               bloc.events.add(RevertPendingQemuConfig('acpi')),
                         ),
                         PveConfigSwitchListTile(
+                          disable: state.isOptionsLocked,
                           title: const Text("KVM hardware virtualization"),
                           value: config.kvm,
                           defaultValue: true,
@@ -95,6 +109,7 @@ class PveQemuOptions extends StatelessWidget {
                               bloc.events.add(RevertPendingQemuConfig('kvm')),
                         ),
                         PveConfigSwitchListTile(
+                          disable: state.isOptionsLocked,
                           title: const Text("Freeze CPU on startup"),
                           value: config.freeze,
                           defaultValue: false,
@@ -105,6 +120,7 @@ class PveQemuOptions extends StatelessWidget {
                               .add(RevertPendingQemuConfig('freeze')),
                         ),
                         PveConfigSwitchListTile(
+                          disable: state.isOptionsLocked,
                           title: const Text("Use local time for RTC"),
                           value: config.localtime,
                           defaultValue: false,
@@ -128,6 +144,7 @@ class PveQemuOptions extends StatelessWidget {
                           subtitle: Text(config.agent ?? 'Default (disabled)'),
                         ),
                         PveConfigSwitchListTile(
+                          disable: state.isOptionsLocked,
                           title: const Text("Protection"),
                           value: config.protection,
                           defaultValue: false,
diff --git a/lib/widgets/pve_qemu_overview.dart b/lib/widgets/pve_qemu_overview.dart
index 50f7382..473731c 100644
--- a/lib/widgets/pve_qemu_overview.dart
+++ b/lib/widgets/pve_qemu_overview.dart
@@ -257,7 +257,7 @@ class PveQemuOverview extends StatelessWidget {
   Route _createOptionsRoute(PveQemuOverviewBloc bloc) {
     return PageRouteBuilder(
       pageBuilder: (context, animation, secondaryAnimation) => Provider.value(
-        value: bloc,
+        value: bloc..events.add(LockQemuOptions(true)),
         child: PveQemuOptions(
           guestID: guestID,
         ),
-- 
2.47.2



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] only message in thread

only message in thread, other threads:[~2025-09-04  9:25 UTC | newest]

Thread overview: (only message) (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2025-09-04  9:25 [pve-devel] [PATCH pve_flutter_frontend v3] feat: ui: add lock/unlock button in guests options page Shan Shaji

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal