diff --git a/www/manager6/form/NodeSelector.js b/www/manager6/form/NodeSelector.js new file mode 100644 index 0000000..37d3ee5 --- /dev/null +++ b/www/manager6/form/NodeSelector.js @@ -0,0 +1,108 @@ +Ext.define('PVE.form.NodeSelector', { + extend: 'Proxmox.form.ComboGrid', + alias: ['widget.pveNodeSelector'], + + // invalidate nodes which are offline + onlineValidator: false, + + selectCurNode: false, + + // do not allow those nodes (array) + disallowedNodes: undefined, + + // only allow those nodes (array) + allowedNodes: undefined, + + valueField: 'node', + displayField: 'node', + store: { + fields: ['node', 'cpu', 'maxcpu', 'mem', 'maxmem', 'uptime'], + proxy: { + type: 'proxmox', + url: '/api2/json/nodes', + }, + sorters: [ + { + property: 'node', + direction: 'ASC', + }, + { + property: 'mem', + direction: 'DESC', + }, + ], + }, + + listConfig: { + columns: [ + { + header: gettext('Node'), + dataIndex: 'node', + sortable: true, + hideable: false, + flex: 1, + }, + { + header: gettext('Memory usage') + ' %', + renderer: PVE.Utils.render_mem_usage_percent, + sortable: true, + width: 100, + dataIndex: 'mem', + }, + { + header: gettext('CPU usage'), + renderer: Proxmox.Utils.render_cpu, + sortable: true, + width: 100, + dataIndex: 'cpu', + }, + ], + }, + + validator: function (value) { + let me = this; + if (!me.onlineValidator || (me.allowBlank && !value)) { + return true; + } + + let offline = [], + notAllowed = []; + Ext.Array.each(value.split(/\s*,\s*/), function (node) { + let rec = me.store.findRecord(me.valueField, node, 0, false, true, true); + if (!(rec && rec.data) || rec.data.status !== 'online') { + offline.push(node); + } else if (me.allowedNodes && !Ext.Array.contains(me.allowedNodes, node)) { + notAllowed.push(node); + } + }); + + if (value && notAllowed.length !== 0) { + return 'Node ' + notAllowed.join(', ') + ' is not allowed for this action!'; + } + if (value && offline.length !== 0) { + return 'Node ' + offline.join(', ') + ' seems to be offline!'; + } + return true; + }, + + initComponent: function () { + var me = this; + + if (me.selectCurNode && PVE.curSelectedNode && PVE.curSelectedNode.data.node) { + me.preferredValue = PVE.curSelectedNode.data.node; + } + + me.callParent(); + me.getStore().load(); + + me.getStore().addFilter( + new Ext.util.Filter({ + // filter out disallowed nodes + filterFn: (item) => + !(me.disallowedNodes && me.disallowedNodes.includes(item.data.node)), + }), + ); + + me.mon(me.getStore(), 'load', () => me.isValid()); + }, +}); diff --git a/www/manager6/ha/NodePrioritySelector.js b/www/manager6/ha/NodePrioritySelector.js new file mode 100644 index 0000000..635374f --- /dev/null +++ b/www/manager6/ha/NodePrioritySelector.js @@ -0,0 +1,230 @@ +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'], + 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: 'mem', + }, + { + 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(); + }, +});