Introduce HA rules and replace the existing HA groups with the new HA
node affinity rules in the web interface.

The HA rules components are designed to be extendible for other new rule
types and allow users to display the errors of contradictory HA rules,
if there are any, in addition to the other basic CRUD operations.

HA rule ids are automatically generated with a 13 character UUID string
in the web interface, as also done for other concepts already, e.g.,
backup jobs, because coming up with future-proof rule ids that cannot be
changed later is not that user friendly. The HA rule's comment field is
meant to store that information instead.

Signed-off-by: Daniel Kral <d.k...@proxmox.com>
---
 www/manager6/Makefile                         |   8 +-
 www/manager6/StateProvider.js                 |   2 +-
 www/manager6/dc/Config.js                     |   8 +-
 www/manager6/ha/GroupSelector.js              |  71 -------
 www/manager6/ha/Groups.js                     | 117 -----------
 www/manager6/ha/RuleEdit.js                   | 146 +++++++++++++
 www/manager6/ha/RuleErrorsModal.js            |  50 +++++
 www/manager6/ha/Rules.js                      | 196 ++++++++++++++++++
 .../NodeAffinityRuleEdit.js}                  | 105 ++--------
 www/manager6/ha/rules/NodeAffinityRules.js    |  36 ++++
 10 files changed, 455 insertions(+), 284 deletions(-)
 delete mode 100644 www/manager6/ha/GroupSelector.js
 delete mode 100644 www/manager6/ha/Groups.js
 create mode 100644 www/manager6/ha/RuleEdit.js
 create mode 100644 www/manager6/ha/RuleErrorsModal.js
 create mode 100644 www/manager6/ha/Rules.js
 rename www/manager6/ha/{GroupEdit.js => rules/NodeAffinityRuleEdit.js} (67%)
 create mode 100644 www/manager6/ha/rules/NodeAffinityRules.js

diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index 84a8b4d0..9bea169a 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -143,13 +143,15 @@ JSSRC=                                                    
\
        window/DirMapEdit.js                            \
        window/GuestImport.js                           \
        ha/Fencing.js                                   \
-       ha/GroupEdit.js                                 \
-       ha/GroupSelector.js                             \
-       ha/Groups.js                                    \
        ha/ResourceEdit.js                              \
        ha/Resources.js                                 \
+       ha/RuleEdit.js                                  \
+       ha/RuleErrorsModal.js                           \
+       ha/Rules.js                                     \
        ha/Status.js                                    \
        ha/StatusView.js                                \
+       ha/rules/NodeAffinityRuleEdit.js                \
+       ha/rules/NodeAffinityRules.js                   \
        dc/ACLView.js                                   \
        dc/ACMEClusterView.js                           \
        dc/AuthEditBase.js                              \
diff --git a/www/manager6/StateProvider.js b/www/manager6/StateProvider.js
index 5137ee55..889f198b 100644
--- a/www/manager6/StateProvider.js
+++ b/www/manager6/StateProvider.js
@@ -54,7 +54,7 @@ Ext.define('PVE.StateProvider', {
         system: 50,
         monitor: 49,
         'ha-fencing': 48,
-        'ha-groups': 47,
+        'ha-rules': 47,
         'ha-resources': 46,
         'ceph-log': 45,
         'ceph-crushmap': 44,
diff --git a/www/manager6/dc/Config.js b/www/manager6/dc/Config.js
index 76c9a6ca..0de67c1b 100644
--- a/www/manager6/dc/Config.js
+++ b/www/manager6/dc/Config.js
@@ -170,11 +170,11 @@ Ext.define('PVE.dc.Config', {
                     itemId: 'ha',
                 },
                 {
-                    title: gettext('Groups'),
+                    title: gettext('Rules'),
                     groups: ['ha'],
-                    xtype: 'pveHAGroupsView',
-                    iconCls: 'fa fa-object-group',
-                    itemId: 'ha-groups',
+                    xtype: 'pveHARulesView',
+                    iconCls: 'fa fa-gears',
+                    itemId: 'ha-rules',
                 },
                 {
                     title: gettext('Fencing'),
diff --git a/www/manager6/ha/GroupSelector.js b/www/manager6/ha/GroupSelector.js
deleted file mode 100644
index 9dc1f4bb..00000000
--- a/www/manager6/ha/GroupSelector.js
+++ /dev/null
@@ -1,71 +0,0 @@
-Ext.define(
-    'PVE.ha.GroupSelector',
-    {
-        extend: 'Proxmox.form.ComboGrid',
-        alias: ['widget.pveHAGroupSelector'],
-
-        autoSelect: false,
-        valueField: 'group',
-        displayField: 'group',
-        listConfig: {
-            columns: [
-                {
-                    header: gettext('Group'),
-                    width: 100,
-                    sortable: true,
-                    dataIndex: 'group',
-                },
-                {
-                    header: gettext('Nodes'),
-                    width: 100,
-                    sortable: false,
-                    dataIndex: 'nodes',
-                },
-                {
-                    header: gettext('Comment'),
-                    flex: 1,
-                    dataIndex: 'comment',
-                    renderer: Ext.String.htmlEncode,
-                },
-            ],
-        },
-        store: {
-            model: 'pve-ha-groups',
-            sorters: {
-                property: 'group',
-                direction: 'ASC',
-            },
-        },
-
-        initComponent: function () {
-            var me = this;
-            me.callParent();
-            me.getStore().load();
-        },
-    },
-    function () {
-        Ext.define('pve-ha-groups', {
-            extend: 'Ext.data.Model',
-            fields: [
-                'group',
-                'type',
-                'digest',
-                'nodes',
-                'comment',
-                {
-                    name: 'restricted',
-                    type: 'boolean',
-                },
-                {
-                    name: 'nofailback',
-                    type: 'boolean',
-                },
-            ],
-            proxy: {
-                type: 'proxmox',
-                url: '/api2/json/cluster/ha/groups',
-            },
-            idProperty: 'group',
-        });
-    },
-);
diff --git a/www/manager6/ha/Groups.js b/www/manager6/ha/Groups.js
deleted file mode 100644
index 6b4958f0..00000000
--- a/www/manager6/ha/Groups.js
+++ /dev/null
@@ -1,117 +0,0 @@
-Ext.define('PVE.ha.GroupsView', {
-    extend: 'Ext.grid.GridPanel',
-    alias: ['widget.pveHAGroupsView'],
-
-    onlineHelp: 'ha_manager_groups',
-
-    stateful: true,
-    stateId: 'grid-ha-groups',
-
-    initComponent: function () {
-        var me = this;
-
-        var caps = Ext.state.Manager.get('GuiCap');
-
-        var store = new Ext.data.Store({
-            model: 'pve-ha-groups',
-            sorters: {
-                property: 'group',
-                direction: 'ASC',
-            },
-        });
-
-        var reload = function () {
-            store.load();
-        };
-
-        var sm = Ext.create('Ext.selection.RowModel', {});
-
-        let run_editor = function () {
-            let rec = sm.getSelection()[0];
-            Ext.create('PVE.ha.GroupEdit', {
-                groupId: rec.data.group,
-                listeners: {
-                    destroy: () => store.load(),
-                },
-                autoShow: true,
-            });
-        };
-
-        let remove_btn = Ext.create('Proxmox.button.StdRemoveButton', {
-            selModel: sm,
-            baseurl: '/cluster/ha/groups/',
-            callback: () => store.load(),
-        });
-        let edit_btn = new Proxmox.button.Button({
-            text: gettext('Edit'),
-            disabled: true,
-            selModel: sm,
-            handler: run_editor,
-        });
-
-        Ext.apply(me, {
-            store: store,
-            selModel: sm,
-            viewConfig: {
-                trackOver: false,
-            },
-            tbar: [
-                {
-                    text: gettext('Create'),
-                    disabled: !caps.nodes['Sys.Console'],
-                    handler: function () {
-                        Ext.create('PVE.ha.GroupEdit', {
-                            listeners: {
-                                destroy: () => store.load(),
-                            },
-                            autoShow: true,
-                        });
-                    },
-                },
-                edit_btn,
-                remove_btn,
-            ],
-            columns: [
-                {
-                    header: gettext('Group'),
-                    width: 150,
-                    sortable: true,
-                    dataIndex: 'group',
-                },
-                {
-                    header: 'restricted',
-                    width: 100,
-                    sortable: true,
-                    renderer: Proxmox.Utils.format_boolean,
-                    dataIndex: 'restricted',
-                },
-                {
-                    header: 'nofailback',
-                    width: 100,
-                    sortable: true,
-                    renderer: Proxmox.Utils.format_boolean,
-                    dataIndex: 'nofailback',
-                },
-                {
-                    header: gettext('Nodes'),
-                    flex: 1,
-                    sortable: false,
-                    dataIndex: 'nodes',
-                },
-                {
-                    header: gettext('Comment'),
-                    flex: 1,
-                    renderer: Ext.String.htmlEncode,
-                    dataIndex: 'comment',
-                },
-            ],
-            listeners: {
-                activate: reload,
-                beforeselect: (grid, record, index, eOpts) => 
caps.nodes['Sys.Console'],
-                itemdblclick: run_editor,
-            },
-        });
-
-        me.callParent();
-    },
-});
diff --git a/www/manager6/ha/RuleEdit.js b/www/manager6/ha/RuleEdit.js
new file mode 100644
index 00000000..9ecebd6d
--- /dev/null
+++ b/www/manager6/ha/RuleEdit.js
@@ -0,0 +1,146 @@
+Ext.define('PVE.ha.RuleInputPanel', {
+    extend: 'Proxmox.panel.InputPanel',
+
+    onlineHelp: 'ha_manager_rules',
+
+    formatResourceListString: function (resources) {
+        let me = this;
+
+        return resources.map((vmid) => {
+            if (me.resourcesStore.getById(`qemu/${vmid}`)) {
+                return `vm:${vmid}`;
+            } else if (me.resourcesStore.getById(`lxc/${vmid}`)) {
+                return `ct:${vmid}`;
+            } else {
+                Ext.Msg.alert(gettext('Error'), `Could not find resource type 
for ${vmid}`);
+                throw `Unknown resource type: ${vmid}`;
+            }
+        });
+    },
+
+    onGetValues: function (values) {
+        let me = this;
+
+        values.type = me.ruleType;
+
+        if (me.isCreate) {
+            values.rule = 'ha-rule-' + 
Ext.data.identifier.Uuid.Global.generate().slice(0, 13);
+        }
+
+        values.disable = values.enable ? 0 : 1;
+        delete values.enable;
+
+        values.resources = me.formatResourceListString(values.resources);
+
+        return values;
+    },
+
+    initComponent: function () {
+        let me = this;
+
+        let resourcesStore = Ext.create('Ext.data.Store', {
+            model: 'PVEResources',
+            autoLoad: true,
+            sorters: 'vmid',
+            filters: [
+                {
+                    property: 'type',
+                    value: /lxc|qemu/,
+                },
+                {
+                    property: 'hastate',
+                    operator: '!=',
+                    value: 'unmanaged',
+                },
+            ],
+        });
+
+        Ext.apply(me, {
+            resourcesStore: resourcesStore,
+        });
+
+        me.column1 = me.column1 ?? [];
+        me.column1.unshift(
+            {
+                xtype: 'proxmoxcheckbox',
+                name: 'enable',
+                fieldLabel: gettext('Enable'),
+                uncheckedValue: 0,
+                defaultValue: 1,
+                checked: true,
+            },
+            {
+                xtype: 'vmComboSelector',
+                name: 'resources',
+                fieldLabel: gettext('HA Resources'),
+                store: me.resourcesStore,
+                allowBlank: false,
+                autoSelect: false,
+                multiSelect: true,
+                validateExists: true,
+            },
+        );
+
+        me.column2 = me.column2 ?? [];
+
+        me.columnB = me.columnB ?? [];
+        me.columnB.unshift({
+            xtype: 'textfield',
+            name: 'comment',
+            fieldLabel: gettext('Comment'),
+            allowBlank: true,
+        });
+
+        me.callParent();
+    },
+});
+
+Ext.define('PVE.ha.RuleEdit', {
+    extend: 'Proxmox.window.Edit',
+
+    defaultFocus: undefined, // prevent the vmComboSelector to be expanded 
when focusing the window
+
+    initComponent: function () {
+        let me = this;
+
+        me.isCreate = !me.ruleId;
+
+        if (me.isCreate) {
+            me.url = '/api2/extjs/cluster/ha/rules';
+            me.method = 'POST';
+        } else {
+            me.url = `/api2/extjs/cluster/ha/rules/${me.ruleId}`;
+            me.method = 'PUT';
+        }
+
+        let inputPanel = Ext.create(me.panelType, {
+            ruleId: me.ruleId,
+            ruleType: me.ruleType,
+            isCreate: me.isCreate,
+        });
+
+        Ext.apply(me, {
+            subject: me.panelName,
+            isAdd: true,
+            items: [inputPanel],
+        });
+
+        me.callParent();
+
+        if (!me.isCreate) {
+            me.load({
+                success: (response, options) => {
+                    let values = response.result.data;
+
+                    values.resources = values.resources
+                        .split(',')
+                        .map((resource) => resource.split(':')[1]);
+
+                    values.enable = values.disable ? 0 : 1;
+
+                    inputPanel.setValues(values);
+                },
+            });
+        }
+    },
+});
diff --git a/www/manager6/ha/RuleErrorsModal.js 
b/www/manager6/ha/RuleErrorsModal.js
new file mode 100644
index 00000000..ebd909fc
--- /dev/null
+++ b/www/manager6/ha/RuleErrorsModal.js
@@ -0,0 +1,50 @@
+Ext.define('PVE.ha.RuleErrorsModal', {
+    extend: 'Ext.window.Window',
+    alias: ['widget.pveHARulesErrorsModal'],
+    mixins: ['Proxmox.Mixin.CBind'],
+
+    modal: true,
+    scrollable: true,
+    resizable: false,
+
+    title: gettext('HA rule errors'),
+
+    initComponent: function () {
+        let me = this;
+
+        let renderHARuleErrors = (errors) => {
+            if (!errors) {
+                return gettext('The HA rule has no errors.');
+            }
+
+            let errorListItemsHtml = '';
+
+            for (let [opt, messages] of Object.entries(errors)) {
+                errorListItemsHtml += messages
+                    .map((message) => `<li>${Ext.htmlEncode(`${opt}: 
${message}`)}</li>`)
+                    .join('');
+            }
+
+            return `<div>
+                   <p>${gettext('The HA rule has the following errors:')}</p>
+                   <ul>${errorListItemsHtml}</ul>
+               </div>`;
+        };
+
+        Ext.apply(me, {
+            modal: true,
+            border: false,
+            layout: 'fit',
+            items: [
+                {
+                    xtype: 'displayfield',
+                    padding: 20,
+                    scrollable: true,
+                    value: renderHARuleErrors(me.errors),
+                },
+            ],
+        });
+
+        me.callParent();
+    },
+});
diff --git a/www/manager6/ha/Rules.js b/www/manager6/ha/Rules.js
new file mode 100644
index 00000000..8f487465
--- /dev/null
+++ b/www/manager6/ha/Rules.js
@@ -0,0 +1,196 @@
+Ext.define('PVE.ha.RulesBaseView', {
+    extend: 'Ext.grid.GridPanel',
+
+    initComponent: function () {
+        let me = this;
+
+        if (!me.ruleType) {
+            throw 'no rule type given';
+        }
+
+        let store = new Ext.data.Store({
+            model: 'pve-ha-rules',
+            autoLoad: true,
+            filters: [
+                {
+                    property: 'type',
+                    value: me.ruleType,
+                },
+            ],
+        });
+
+        let reloadStore = () => store.load();
+
+        let sm = Ext.create('Ext.selection.RowModel', {});
+
+        let createRuleEditWindow = (ruleId) => {
+            if (!me.inputPanel) {
+                throw `no editor registered for ha rule type: ${me.ruleType}`;
+            }
+
+            Ext.create('PVE.ha.RuleEdit', {
+                panelType: `PVE.ha.rules.${me.inputPanel}`,
+                panelName: me.ruleTitle,
+                ruleType: me.ruleType,
+                ruleId: ruleId,
+                autoShow: true,
+                listeners: {
+                    destroy: reloadStore,
+                },
+            });
+        };
+
+        let runEditor = () => {
+            let rec = sm.getSelection()[0];
+            if (!rec) {
+                return;
+            }
+            let { rule } = rec.data;
+            createRuleEditWindow(rule);
+        };
+
+        let editButton = Ext.create('Proxmox.button.Button', {
+            text: gettext('Edit'),
+            disabled: true,
+            selModel: sm,
+            handler: runEditor,
+        });
+
+        let removeButton = Ext.create('Proxmox.button.StdRemoveButton', {
+            selModel: sm,
+            baseurl: '/cluster/ha/rules/',
+            callback: reloadStore,
+        });
+
+        Ext.apply(me, {
+            store: store,
+            selModel: sm,
+            viewConfig: {
+                trackOver: false,
+            },
+            emptyText: Ext.String.format(gettext('No {0} rules configured.'), 
me.ruleTitle),
+            tbar: [
+                {
+                    text: gettext('Add'),
+                    handler: () => createRuleEditWindow(),
+                },
+                editButton,
+                removeButton,
+            ],
+            listeners: {
+                activate: reloadStore,
+                itemdblclick: runEditor,
+            },
+        });
+
+        me.columns.unshift(
+            {
+                header: gettext('Enabled'),
+                width: 80,
+                dataIndex: 'disable',
+                align: 'center',
+                renderer: function (value) {
+                    return Proxmox.Utils.renderEnabledIcon(!value);
+                },
+                sortable: true,
+            },
+            {
+                header: gettext('State'),
+                xtype: 'actioncolumn',
+                width: 65,
+                align: 'center',
+                dataIndex: 'errors',
+                items: [
+                    {
+                        handler: (table, rowIndex, colIndex, item, event, { 
data }) => {
+                            let errors = Object.keys(data.errors ?? {});
+                            if (!errors.length) {
+                                return;
+                            }
+
+                            Ext.create('PVE.ha.RuleErrorsModal', {
+                                autoShow: true,
+                                errors: data.errors ?? {},
+                            });
+                        },
+                        getTip: (value) => {
+                            let errors = Object.keys(value ?? {});
+                            if (errors.length) {
+                                return gettext('HA Rule has conflicts and/or 
errors.');
+                            } else {
+                                return gettext('HA Rule is OK.');
+                            }
+                        },
+                        getClass: (value) => {
+                            let iconName = 'check';
+
+                            let errors = Object.keys(value ?? {});
+                            if (errors.length) {
+                                iconName = 'exclamation-triangle';
+                            }
+
+                            return `fa fa-${iconName}`;
+                        },
+                    },
+                ],
+            },
+        );
+
+        me.columns.push({
+            header: gettext('Comment'),
+            flex: 1,
+            renderer: Ext.String.htmlEncode,
+            dataIndex: 'comment',
+        });
+
+        me.callParent();
+    },
+});
+
+Ext.define(
+    'PVE.ha.RulesView',
+    {
+        extend: 'Ext.panel.Panel',
+        alias: 'widget.pveHARulesView',
+
+        onlineHelp: 'ha_manager_rules',
+
+        layout: {
+            type: 'vbox',
+            align: 'stretch',
+        },
+
+        items: [
+            {
+                title: gettext('HA Node Affinity Rules'),
+                xtype: 'pveHANodeAffinityRulesView',
+                flex: 1,
+                border: 0,
+            },
+        ],
+    },
+    function () {
+        Ext.define('pve-ha-rules', {
+            extend: 'Ext.data.Model',
+            fields: [
+                'rule',
+                'type',
+                'nodes',
+                'digest',
+                'errors',
+                'disable',
+                'comment',
+                'resources',
+                {
+                    name: 'strict',
+                    type: 'boolean',
+                },
+            ],
+            proxy: {
+                type: 'proxmox',
+                url: '/api2/json/cluster/ha/rules',
+            },
+            idProperty: 'rule',
+        });
+    },
+);
diff --git a/www/manager6/ha/GroupEdit.js 
b/www/manager6/ha/rules/NodeAffinityRuleEdit.js
similarity index 67%
rename from www/manager6/ha/GroupEdit.js
rename to www/manager6/ha/rules/NodeAffinityRuleEdit.js
index f7eed22e..4574d9ef 100644
--- a/www/manager6/ha/GroupEdit.js
+++ b/www/manager6/ha/rules/NodeAffinityRuleEdit.js
@@ -1,22 +1,10 @@
-Ext.define('PVE.ha.GroupInputPanel', {
-    extend: 'Proxmox.panel.InputPanel',
-    onlineHelp: 'ha_manager_groups',
-
-    groupId: undefined,
-
-    onGetValues: function (values) {
-        var me = this;
-
-        if (me.isCreate) {
-            values.type = 'group';
-        }
-
-        return values;
-    },
+Ext.define('PVE.ha.rules.NodeAffinityInputPanel', {
+    extend: 'PVE.ha.RuleInputPanel',
 
     initComponent: function () {
-        var me = this;
+        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', {
@@ -134,84 +122,25 @@ Ext.define('PVE.ha.GroupInputPanel', {
             nodefield.resumeEvent('change');
         };
 
-        me.column1 = [
+        me.column2 = [
             {
-                xtype: me.isCreate ? 'textfield' : 'displayfield',
-                name: 'group',
-                value: me.groupId || '',
-                fieldLabel: 'ID',
-                vtype: 'StorageId',
-                allowBlank: false,
+                xtype: 'proxmoxcheckbox',
+                name: 'strict',
+                fieldLabel: gettext('Strict'),
+                autoEl: {
+                    tag: 'div',
+                    'data-qtip': gettext(
+                        'Enable if the HA Resources must be restricted to the 
nodes.',
+                    ),
+                },
+                uncheckedValue: 0,
+                defaultValue: 0,
             },
             nodefield,
         ];
 
-        me.column2 = [
-            {
-                xtype: 'proxmoxcheckbox',
-                name: 'restricted',
-                uncheckedValue: 0,
-                fieldLabel: 'restricted',
-            },
-            {
-                xtype: 'proxmoxcheckbox',
-                name: 'nofailback',
-                uncheckedValue: 0,
-                fieldLabel: 'nofailback',
-            },
-        ];
-
-        me.columnB = [
-            {
-                xtype: 'textfield',
-                name: 'comment',
-                fieldLabel: gettext('Comment'),
-            },
-            nodegrid,
-        ];
+        me.columnB = [nodegrid];
 
         me.callParent();
     },
 });
-
-Ext.define('PVE.ha.GroupEdit', {
-    extend: 'Proxmox.window.Edit',
-
-    groupId: undefined,
-
-    initComponent: function () {
-        var me = this;
-
-        me.isCreate = !me.groupId;
-
-        if (me.isCreate) {
-            me.url = '/api2/extjs/cluster/ha/groups';
-            me.method = 'POST';
-        } else {
-            me.url = '/api2/extjs/cluster/ha/groups/' + me.groupId;
-            me.method = 'PUT';
-        }
-
-        var ipanel = Ext.create('PVE.ha.GroupInputPanel', {
-            isCreate: me.isCreate,
-            groupId: me.groupId,
-        });
-
-        Ext.apply(me, {
-            subject: gettext('HA Group'),
-            items: [ipanel],
-        });
-
-        me.callParent();
-
-        if (!me.isCreate) {
-            me.load({
-                success: function (response, options) {
-                    var values = response.result.data;
-
-                    ipanel.setValues(values);
-                },
-            });
-        }
-    },
-});
diff --git a/www/manager6/ha/rules/NodeAffinityRules.js 
b/www/manager6/ha/rules/NodeAffinityRules.js
new file mode 100644
index 00000000..b6143acd
--- /dev/null
+++ b/www/manager6/ha/rules/NodeAffinityRules.js
@@ -0,0 +1,36 @@
+Ext.define('PVE.ha.NodeAffinityRulesView', {
+    extend: 'PVE.ha.RulesBaseView',
+    alias: 'widget.pveHANodeAffinityRulesView',
+
+    ruleType: 'node-affinity',
+    ruleTitle: gettext('HA Node Affinity'),
+    inputPanel: 'NodeAffinityInputPanel',
+    faIcon: 'map-pin',
+
+    stateful: true,
+    stateId: 'grid-ha-node-affinity-rules',
+
+    initComponent: function () {
+        let me = this;
+
+        me.columns = [
+            {
+                header: gettext('Strict'),
+                width: 75,
+                dataIndex: 'strict',
+            },
+            {
+                header: gettext('HA Resources'),
+                flex: 1,
+                dataIndex: 'resources',
+            },
+            {
+                header: gettext('Nodes'),
+                flex: 1,
+                dataIndex: 'nodes',
+            },
+        ];
+
+        me.callParent();
+    },
+});
-- 
2.47.2



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel

Reply via email to