* [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