Files
dualfroz 85bc6aeedb fix #6874: ui: node selectors: sort memory usage by percentage
The memory usage column of the node selector, used e.g. in the
migration dialog, and of the HA node priority selector shows a
percentage, but uses the raw 'mem' bytes as data index. Sorting by that
column thus sorts by used bytes, which does not match the displayed
values as soon as the nodes have a different amount of memory.

Add a calculated 'memuse' field, like the resource store already has,
and use it for the column. This also makes the percentage renderer
receive a ratio here, like everywhere else, which allows dropping its
bytes fallback in a follow-up.

Signed-off-by: Michal Fox <me@dualfroz.com>
2026-09-25 07:15:08 +02:00

241 lines
5.8 KiB
JavaScript

Ext.define('PVE.forms.NodePrioritySelector', {
extend: 'Ext.grid.Panel',
alias: 'widget.pveNodePrioritySelector',
mixins: {
field: 'Ext.form.field.Field',
},
allowBlank: true,
isFormField: true,
config: {
useNodePriority: null,
},
publishes: ['useNodePriority'],
viewModel: {
data: {
showNodePriority: null,
},
},
store: {
autoLoad: true,
fields: [
'node',
'cpu',
'mem',
'priority',
{
name: 'memuse',
type: 'number',
calculate: PVE.Utils.calculate_mem_usage,
},
],
proxy: {
type: 'proxmox',
url: '/api2/json/nodes',
},
sorters: [
{
property: 'node',
direction: 'ASC',
},
],
},
columns: [
{
header: gettext('Node'),
width: 150,
dataIndex: 'node',
},
{
header: gettext('Memory usage') + ' %',
renderer: PVE.Utils.render_mem_usage_percent,
sortable: true,
width: 150,
dataIndex: 'memuse',
},
{
header: gettext('CPU usage'),
renderer: Proxmox.Utils.render_cpu,
sortable: true,
flex: 1,
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 (field, value) {
let record = field.getWidgetRecord();
record.set('priority', value);
record.commit();
},
},
bind: {
hidden: '{!showNodePriority}',
disabled: '{!showNodePriority}',
},
},
bind: {
hidden: '{!showNodePriority}',
disabled: '{!showNodePriority}',
},
},
],
selModel: {
selType: 'checkboxmodel',
mode: 'SIMPLE',
},
checkChangeEvents: ['selectionchange'],
listeners: {
selectionchange: function () {
// to trigger validity and error checks
this.checkChange();
},
},
invertCheckboxSelection: function () {
let me = this;
let sm = me.getSelectionModel();
let allNodeModels = sm.getStore().getRange();
let selectedNodeModels = new Set(sm.getSelection());
if (!allNodeModels.length || !selectedNodeModels.size) {
return;
}
sm.deselectAll();
sm.select(allNodeModels.filter((node) => !selectedNodeModels.has(node)));
},
applyUseNodePriority: function (newValue) {
let me = this;
me.getViewModel().set('showNodePriority', newValue);
return newValue;
},
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 }) => {
let nodeEntry = data.node;
if (me.useNodePriority && data.priority) {
nodeEntry += `:${data.priority}`;
}
return nodeEntry;
})
.join(',');
return nodes;
},
setValueSelection: function (value) {
let me = this;
let store = me.getStore();
store.beginUpdate();
let nodenames = value.length ? value.split(',') : [];
let nodes = nodenames.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;
});
store.endUpdate();
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();
},
});