From: Shan Shaji <s.shaji@proxmox.com>
To: pve-devel@lists.proxmox.com
Cc: Thomas Lamprecht <t.lamprecht@proxmox.com>
Subject: [pve-devel] [PATCH pve_flutter_frontend v3] feat: ui: add lock/unlock button in guests options page
Date: Thu, 4 Sep 2025 11:25:48 +0200 [thread overview]
Message-ID: <20250904092548.59064-1-s.shaji@proxmox.com> (raw)
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
reply other threads:[~2025-09-04 9:25 UTC|newest]
Thread overview: [no followups] expand[flat|nested] mbox.gz Atom feed
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20250904092548.59064-1-s.shaji@proxmox.com \
--to=s.shaji@proxmox.com \
--cc=pve-devel@lists.proxmox.com \
--cc=t.lamprecht@proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox