From: Daniel Kral <d.kral@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH manager v2 06/12] ui: ha: node affinity: move node priority selector into separate component
Date: Tue, 2 Jun 2026 12:01:10 +0200 [thread overview]
Message-ID: <20260602100226.180071-7-d.kral@proxmox.com> (raw)
In-Reply-To: <20260602100226.180071-1-d.kral@proxmox.com>
PVE.form.NodePrioritySelector is a reduced adaption of
PVE.form.VMSelector, which adds error highlights to the node priority
selector and fetches the node list from the API directly.
Signed-off-by: Daniel Kral <d.kral@proxmox.com>
---
changes since v1:
- new
www/manager6/Makefile | 1 +
www/manager6/ha/NodePrioritySelector.js | 169 ++++++++++++++++++
www/manager6/ha/rules/NodeAffinityRuleEdit.js | 136 +-------------
3 files changed, 177 insertions(+), 129 deletions(-)
create mode 100644 www/manager6/ha/NodePrioritySelector.js
diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index d4dd3f35..eb0e9d9c 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -151,6 +151,7 @@ JSSRC= \
window/DirMapEdit.js \
window/GuestImport.js \
ha/Fencing.js \
+ ha/NodePrioritySelector.js \
ha/ResourceEdit.js \
ha/Resources.js \
ha/RuleEdit.js \
diff --git a/www/manager6/ha/NodePrioritySelector.js b/www/manager6/ha/NodePrioritySelector.js
new file mode 100644
index 00000000..ec6ac02a
--- /dev/null
+++ b/www/manager6/ha/NodePrioritySelector.js
@@ -0,0 +1,169 @@
+Ext.define('PVE.forms.NodePrioritySelector', {
+ extend: 'Ext.grid.Panel',
+ alias: 'widget.pveNodePrioritySelector',
+
+ mixins: {
+ field: 'Ext.form.field.Field',
+ },
+
+ allowBlank: true,
+ selectAll: false,
+ isFormField: true,
+
+ store: {
+ autoLoad: true,
+ fields: ['node', 'cpu', 'mem', 'priority'],
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json/nodes',
+ },
+ sorters: [
+ {
+ property: 'node',
+ direction: 'ASC',
+ },
+ ],
+ },
+
+ columns: [
+ {
+ header: gettext('Node'),
+ flex: 1,
+ dataIndex: 'node',
+ },
+ {
+ header: gettext('Memory usage') + ' %',
+ renderer: PVE.Utils.render_mem_usage_percent,
+ sortable: true,
+ width: 150,
+ dataIndex: 'mem',
+ },
+ {
+ header: gettext('CPU usage'),
+ renderer: Proxmox.Utils.render_cpu,
+ sortable: true,
+ width: 150,
+ dataIndex: 'cpu',
+ },
+ {
+ header: gettext('Priority'),
+ xtype: 'widgetcolumn',
+ dataIndex: 'priority',
+ sortable: true,
+ stopSelection: true,
+ widget: {
+ xtype: 'proxmoxintegerfield',
+ minValue: 0,
+ maxValue: 1000,
+ isFormField: false,
+ },
+ },
+ ],
+
+ selModel: {
+ selType: 'checkboxmodel',
+ mode: 'SIMPLE',
+ },
+
+ checkChangeEvents: ['selectionchange', 'change'],
+
+ listeners: {
+ selectionchange: function () {
+ // to trigger validity and error checks
+ this.checkChange();
+ },
+ },
+
+ getSubmitData: function () {
+ let me = this;
+ let res = {};
+ res[me.name] = me.getValue();
+ return res;
+ },
+
+ getValue: function () {
+ let me = this;
+
+ if (me.savedValue !== undefined) {
+ return me.savedValue;
+ }
+
+ let sm = me.getSelectionModel();
+ let selectedNodeModels = sm.getSelection() ?? [];
+ let nodes = selectedNodeModels
+ .map(({ data }) => data.node + (data.priority ? `:${data.priority}` : ''))
+ .join(',');
+
+ return nodes;
+ },
+
+ setValueSelection: function (value) {
+ let me = this;
+
+ let store = me.getStore();
+ let nodes = value.split(',').map((item) => {
+ let [node, priority] = item.split(':');
+
+ let record = store.findRecord('node', node, 0, false, true, true);
+ if (record) {
+ record.set('priority', priority);
+ record.commit();
+ } else {
+ let addedRecords = store.add({ node, priority });
+ record = addedRecords[0];
+ }
+
+ return record;
+ });
+
+ let sm = me.getSelectionModel();
+ if (nodes.length) {
+ sm.select(nodes);
+ } else {
+ sm.deselectAll();
+ }
+
+ me.getErrors();
+ },
+
+ setValue: function (value) {
+ let me = this;
+
+ let store = me.getStore();
+ if (!store.isLoaded()) {
+ me.savedValue = value;
+ store.on(
+ 'load',
+ function () {
+ me.setValueSelection(value);
+ delete me.savedValue;
+ },
+ { single: true },
+ );
+ } else {
+ me.setValueSelection(value);
+ }
+
+ return me.mixins.field.setValue.call(me, value);
+ },
+
+ getErrors: function (value) {
+ let me = this;
+
+ if (!me.isDisabled() && me.allowBlank === false && me.getValue().length === 0) {
+ me.addBodyCls(['x-form-trigger-wrap-default', 'x-form-trigger-wrap-invalid']);
+ return [gettext('No nodes selected')];
+ }
+
+ me.removeBodyCls(['x-form-trigger-wrap-default', 'x-form-trigger-wrap-invalid']);
+
+ return [];
+ },
+
+ initComponent: function () {
+ let me = this;
+
+ me.callParent();
+ me.initField();
+ },
+});
diff --git a/www/manager6/ha/rules/NodeAffinityRuleEdit.js b/www/manager6/ha/rules/NodeAffinityRuleEdit.js
index b4b6a13c..77da18b1 100644
--- a/www/manager6/ha/rules/NodeAffinityRuleEdit.js
+++ b/www/manager6/ha/rules/NodeAffinityRuleEdit.js
@@ -4,133 +4,6 @@ Ext.define('PVE.ha.rules.NodeAffinityInputPanel', {
initComponent: function () {
let me = this;
- /* TODO Node selector should be factored out in its own component */
- let update_nodefield, update_node_selection;
-
- let sm = Ext.create('Ext.selection.CheckboxModel', {
- mode: 'SIMPLE',
- listeners: {
- selectionchange: function (model, selected) {
- update_nodefield(selected);
- },
- },
- });
-
- let store = Ext.create('Ext.data.Store', {
- fields: ['node', 'mem', 'cpu', 'priority'],
- data: PVE.data.ResourceStore.getNodes(), // use already cached data to avoid an API call
- proxy: {
- type: 'memory',
- reader: { type: 'json' },
- },
- sorters: [
- {
- property: 'node',
- direction: 'ASC',
- },
- ],
- });
-
- var nodegrid = Ext.createWidget('grid', {
- store: store,
- border: true,
- height: 300,
- selModel: sm,
- columns: [
- {
- header: gettext('Node'),
- flex: 1,
- dataIndex: 'node',
- },
- {
- header: gettext('Memory usage') + ' %',
- renderer: PVE.Utils.render_mem_usage_percent,
- sortable: true,
- width: 150,
- dataIndex: 'mem',
- },
- {
- header: gettext('CPU usage'),
- renderer: Proxmox.Utils.render_cpu,
- sortable: true,
- width: 150,
- dataIndex: 'cpu',
- },
- {
- header: gettext('Priority'),
- xtype: 'widgetcolumn',
- dataIndex: 'priority',
- sortable: true,
- stopSelection: true,
- widget: {
- xtype: 'proxmoxintegerfield',
- minValue: 0,
- maxValue: 1000,
- isFormField: false,
- listeners: {
- change: function (numberfield, value, old_value) {
- let record = numberfield.getWidgetRecord();
- record.set('priority', value);
- update_nodefield(sm.getSelection());
- record.commit();
- },
- },
- },
- },
- ],
- });
-
- let nodefield = Ext.create('Ext.form.field.Hidden', {
- name: 'nodes',
- value: '',
- listeners: {
- change: function (field, value) {
- update_node_selection(value);
- },
- },
- isValid: function () {
- let value = this.getValue();
- return value && value.length !== 0;
- },
- });
-
- update_node_selection = function (string) {
- let nodes = string.split(',').map((item) => {
- let [node, priority] = item.split(':');
-
- let record = store.findRecord('node', node, 0, false, true, true);
- if (record) {
- record.set('priority', priority);
- record.commit();
- } else {
- let addedRecords = store.add({ node, priority });
- record = addedRecords[0];
- }
-
- return record;
- });
-
- if (nodes.length) {
- sm.select(nodes);
- } else {
- sm.deselectAll();
- }
-
- nodegrid.reconfigure(store);
- };
-
- update_nodefield = function (selected) {
- let nodes = selected
- .map(({ data }) => data.node + (data.priority ? `:${data.priority}` : ''))
- .join(',');
-
- // nodefield change listener calls us again, which results in a
- // endless recursion, suspend the event temporary to avoid this
- nodefield.suspendEvent('change');
- nodefield.setValue(nodes);
- nodefield.resumeEvent('change');
- };
-
me.column2 = [
{
xtype: 'proxmoxcheckbox',
@@ -145,10 +18,15 @@ Ext.define('PVE.ha.rules.NodeAffinityInputPanel', {
uncheckedValue: 0,
defaultValue: 0,
},
- nodefield,
];
- me.columnB = [nodegrid];
+ me.columnB = [
+ {
+ xtype: 'pveNodePrioritySelector',
+ name: 'nodes',
+ allowBlank: false,
+ },
+ ];
me.callParent();
},
--
2.47.3
next prev parent reply other threads:[~2026-06-02 10:03 UTC|newest]
Thread overview: 13+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-06-02 10:01 [PATCH-SERIES docs/ha-manager/manager v2 00/12] Negative Node Affinity Rules Daniel Kral
2026-06-02 10:01 ` [PATCH ha-manager v2 01/12] rules: node affinity: add affinity property to node affinity rules Daniel Kral
2026-06-02 10:01 ` [PATCH ha-manager v2 02/12] rules: rename ambiguous argument nodes to cluster nodes Daniel Kral
2026-06-02 10:01 ` [PATCH ha-manager v2 03/12] rules: node affinity: implement negative node affinity rules Daniel Kral
2026-06-02 10:01 ` [PATCH manager v2 04/12] ui: ha: node affinity: handle non-existent nodes Daniel Kral
2026-06-02 10:01 ` [PATCH manager v2 05/12] ui: ha: node affinity: do update node selection all at once Daniel Kral
2026-06-02 10:01 ` Daniel Kral [this message]
2026-06-02 10:01 ` [PATCH manager v2 07/12] ui: ha: node affinity: allow setting affinity for node affinity rules Daniel Kral
2026-06-02 10:01 ` [PATCH manager v2 08/12] ui: ha: node affinity: do not send default node affinity rule values Daniel Kral
2026-06-02 10:01 ` [PATCH docs v2 09/12] ha-manager: rules: use the correct article for terms starting with HA Daniel Kral
2026-06-02 10:01 ` [PATCH docs v2 10/12] ha-manager: rules: improve resource affinity rule short description Daniel Kral
2026-06-02 10:01 ` [PATCH docs v2 11/12] ha-manager: rules: adapt rule configuration examples Daniel Kral
2026-06-02 10:01 ` [PATCH docs v2 12/12] ha-manager: rules: add negative node affinity rule descriptions Daniel Kral
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=20260602100226.180071-7-d.kral@proxmox.com \
--to=d.kral@proxmox.com \
--cc=pve-devel@lists.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