From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) by lore.proxmox.com (Postfix) with ESMTPS id 4ABFE1FF189 for ; Thu, 4 Sep 2025 11:25:54 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 6E37029205; Thu, 4 Sep 2025 11:26:08 +0200 (CEST) From: Shan Shaji To: pve-devel@lists.proxmox.com Date: Thu, 4 Sep 2025 11:25:48 +0200 Message-ID: <20250904092548.59064-1-s.shaji@proxmox.com> X-Mailer: git-send-email 2.47.2 MIME-Version: 1.0 X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1756977946971 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.006 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment POISEN_SPAM_PILL 0.1 Meta: its spam POISEN_SPAM_PILL_2 0.1 random spam to be learned in bayes POISEN_SPAM_PILL_4 0.1 random spam to be learned in bayes RCVD_IN_VALIDITY_CERTIFIED_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. RCVD_IN_VALIDITY_RPBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. RCVD_IN_VALIDITY_SAFE_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Subject: [pve-devel] [PATCH pve_flutter_frontend v3] feat: ui: add lock/unlock button in guests options page X-BeenThere: pve-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox VE development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Reply-To: Proxmox VE development discussion Cc: Thomas Lamprecht Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Errors-To: pve-devel-bounces@lists.proxmox.com Sender: "pve-devel" 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 Signed-off-by: Shan Shaji --- 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> _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> _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 { // Fields String get nodeID; + bool get isOptionsLocked; PveNodesLxcStatusModel? get currentStatus; BuiltList? 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 { String get nodeID; + bool get isOptionsLocked; PveQemuStatusModel? get currentStatus; BuiltList? 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? 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: [ @@ -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