]> git.proxmox.com Git - proxmox-widget-toolkit.git/blobdiff - src/node/APTRepositories.js
notification: matcher: move match-severity fields to panel
[proxmox-widget-toolkit.git] / src / node / APTRepositories.js
index bddcb314216ed6889d5c79aaf83d2580258aaccc..4e74da20b13e17a518bb20b79b44d9260af6c3e2 100644 (file)
@@ -23,6 +23,7 @@ Ext.define('Proxmox.window.APTRepositoryAdd', {
     isAdd: true,
 
     subject: gettext('Repository'),
+    width: 600,
 
     initComponent: function() {
        let me = this;
@@ -59,21 +60,24 @@ Ext.define('Proxmox.window.APTRepositoryAdd', {
            name: 'handle',
            allowBlank: false,
            comboItems: me.repoInfo.map(info => [info.handle, info.name]),
-           isValid: function() {
-               const handle = this.value;
+           validator: function(renderedValue) {
+               let handle = this.value;
+               // we cannot use this.callParent in instantiations
+               let valid = Proxmox.form.KVComboBox.prototype.validator.call(this, renderedValue);
 
-               if (!handle) {
+               if (!valid || !handle) {
                    return false;
                }
 
                const info = me.repoInfo.find(elem => elem.handle === handle);
-
                if (!info) {
                    return false;
                }
 
-               // not yet configured
-               return info.status === undefined || info.status === null;
+               if (info.status) {
+                   return Ext.String.format(gettext('{0} is already configured'), renderedValue);
+               }
+               return valid;
            },
            listeners: {
                change: function(f, value) {
@@ -86,14 +90,12 @@ Ext.define('Proxmox.window.APTRepositoryAdd', {
 
        repoSelector.setValue(me.repoInfo[0].handle);
 
-       let items = [
-           repoSelector,
-           description,
-           status,
-       ];
-
        Ext.apply(me, {
-           items: items,
+           items: [
+               repoSelector,
+               description,
+               status,
+           ],
            repoSelector: repoSelector,
        });
 
@@ -106,27 +108,31 @@ Ext.define('Proxmox.node.APTRepositoriesErrors', {
 
     xtype: 'proxmoxNodeAPTRepositoriesErrors',
 
-    title: gettext('Errors'),
-
     store: {},
 
-    border: false,
+    scrollable: true,
 
     viewConfig: {
        stripeRows: false,
-       getRowClass: () => 'proxmox-invalid-row',
+       getRowClass: (record) => {
+           switch (record.data.status) {
+               case 'warning': return 'proxmox-warning-row';
+               case 'critical': return 'proxmox-invalid-row';
+               default: return '';
+           }
+       },
     },
 
+    hideHeaders: true,
+
     columns: [
        {
-           header: gettext('File'),
-           dataIndex: 'path',
-           renderer: value => `<i class='pve-grid-fa fa fa-fw fa-exclamation-triangle'></i>${value}`,
-           width: 350,
+           dataIndex: 'status',
+           renderer: (value) => `<i class="fa fa-fw ${Proxmox.Utils.get_health_icon(value, true)}"></i>`,
+           width: 50,
        },
        {
-           header: gettext('Error'),
-           dataIndex: 'error',
+           dataIndex: 'message',
            flex: 1,
        },
     ],
@@ -135,6 +141,7 @@ Ext.define('Proxmox.node.APTRepositoriesErrors', {
 Ext.define('Proxmox.node.APTRepositoriesGrid', {
     extend: 'Ext.grid.GridPanel',
     xtype: 'proxmoxNodeAPTRepositoriesGrid',
+    mixins: ['Proxmox.Mixin.CBind'],
 
     title: gettext('APT Repositories'),
 
@@ -153,9 +160,12 @@ Ext.define('Proxmox.node.APTRepositoriesGrid', {
        },
        {
            text: gettext('Add'),
-           id: 'addButton',
+           name: 'addRepo',
            disabled: true,
            repoInfo: undefined,
+           cbind: {
+               onlineHelp: '{onlineHelp}',
+           },
            handler: function(button, event, record) {
                Proxmox.Utils.checked_command(() => {
                    let me = this;
@@ -168,9 +178,10 @@ Ext.define('Proxmox.node.APTRepositoriesGrid', {
 
                    Ext.create('Proxmox.window.APTRepositoryAdd', {
                        repoInfo: me.repoInfo,
-                       url: `/api2/json/nodes/${panel.nodename}/apt/repositories`,
+                       url: `/api2/extjs/nodes/${panel.nodename}/apt/repositories`,
                        method: 'PUT',
                        extraRequestParams: extraParams,
+                       onlineHelp: me.onlineHelp,
                        listeners: {
                            destroy: function() {
                                panel.reload();
@@ -182,12 +193,14 @@ Ext.define('Proxmox.node.APTRepositoriesGrid', {
        },
        '-',
        {
-           xtype: 'proxmoxButton',
-           text: gettext('Enable'),
+           xtype: 'proxmoxAltTextButton',
            defaultText: gettext('Enable'),
            altText: gettext('Disable'),
-           id: 'repoEnableButton',
+           name: 'repoEnable',
            disabled: true,
+           bind: {
+               text: '{enableButtonText}',
+           },
            handler: function(button, event, record) {
                let me = this;
                let panel = me.up('proxmoxNodeAPTRepositories');
@@ -215,31 +228,21 @@ Ext.define('Proxmox.node.APTRepositoriesGrid', {
                    },
                });
            },
-           listeners: {
-               render: function(btn) {
-                   // HACK: calculate the max button width on first render to avoid toolbar glitches
-                   let defSize = btn.getSize().width;
-
-                   btn.setText(btn.altText);
-                   let altSize = btn.getSize().width;
-
-                   btn.setText(btn.defaultText);
-                   btn.setSize({ width: altSize > defSize ? altSize : defSize });
-               },
-           },
        },
     ],
 
     sortableColumns: false,
+    viewConfig: {
+       stripeRows: false,
+       getRowClass: (record, index) => record.get('Enabled') ? '' : 'proxmox-disabled-row',
+    },
 
     columns: [
        {
-           xtype: 'checkcolumn',
            header: gettext('Enabled'),
            dataIndex: 'Enabled',
-           listeners: {
-               beforecheckchange: () => false, // veto, we don't want to allow inline change - to subtle
-           },
+           align: 'center',
+           renderer: Proxmox.Utils.renderEnabledIcon,
            width: 90,
        },
        {
@@ -261,16 +264,51 @@ Ext.define('Proxmox.node.APTRepositoriesGrid', {
        {
            header: gettext('Suites'),
            dataIndex: 'Suites',
-           renderer: function(suites, cell, record) {
-               return suites.join(' ');
+           renderer: function(suites, metaData, record) {
+               let err = '';
+               if (record.data.warnings && record.data.warnings.length > 0) {
+                   let txt = [gettext('Warning')];
+                   record.data.warnings.forEach((warning) => {
+                       if (warning.property === 'Suites') {
+                           txt.push(warning.message);
+                       }
+                   });
+                   metaData.tdAttr = `data-qtip="${Ext.htmlEncode(txt.join('<br>'))}"`;
+                   if (record.data.Enabled) {
+                       metaData.tdCls = 'proxmox-invalid-row';
+                       err = '<i class="fa fa-fw critical fa-exclamation-circle"></i> ';
+                   } else {
+                       metaData.tdCls = 'proxmox-warning-row';
+                       err = '<i class="fa fa-fw warning fa-exclamation-circle"></i> ';
+                   }
+               }
+               return suites.join(' ') + err;
            },
            width: 130,
        },
        {
            header: gettext('Components'),
            dataIndex: 'Components',
-           renderer: function(components, cell, record) {
-               return components.join(' ');
+           renderer: function(components, metaData, record) {
+               if (components === undefined) {
+                   return '';
+               }
+               let err = '';
+               if (components.length === 1) {
+                   // FIXME: this should be a flag set to the actual repsotiories, i.e., a tristate
+                   // like production-ready = <yes|no|other> (Option<bool>)
+                   if (components[0].match(/\w+(-no-subscription|test)\s*$/i)) {
+                       metaData.tdCls = 'proxmox-warning-row';
+                       err = '<i class="fa fa-fw warning fa-exclamation-circle"></i> ';
+
+                       let qtip = components[0].match(/no-subscription/)
+                           ? gettext('The no-subscription repository is NOT production-ready')
+                           : gettext('The test repository may contain unstable updates')
+                           ;
+                           metaData.tdAttr = `data-qtip="${Ext.htmlEncode(qtip)}"`;
+                   }
+               }
+               return components.join(' ') + err;
            },
            width: 170,
        },
@@ -294,7 +332,7 @@ Ext.define('Proxmox.node.APTRepositoriesGrid', {
                        let values = option.Values.join(' ');
                        text += `${key}: ${values}<br>`;
                    } else {
-                       throw "unkown file type";
+                       throw "unknown file type";
                    }
                });
                return text;
@@ -305,11 +343,15 @@ Ext.define('Proxmox.node.APTRepositoriesGrid', {
            header: gettext('Origin'),
            dataIndex: 'Origin',
            width: 120,
-           renderer: (value, meta, rec) => {
+           renderer: function(value, meta, rec) {
+               if (typeof value !== 'string' || value.length === 0) {
+                   value = gettext('Other');
+               }
                let cls = 'fa fa-fw fa-question-circle-o';
-               if (value.match(/^\s*Proxmox\s*$/i)) {
+               let originType = this.up('proxmoxNodeAPTRepositories').classifyOrigin(value);
+               if (originType === 'Proxmox') {
                    cls = 'pmx-itype-icon pmx-itype-icon-proxmox-x';
-               } else if (value.match(/^\s*Debian\s*$/i)) {
+               } else if (originType === 'Debian') {
                    cls = 'pmx-itype-icon pmx-itype-icon-debian-swirl';
                }
                return `<i class='${cls}'></i> ${value}`;
@@ -319,54 +361,27 @@ Ext.define('Proxmox.node.APTRepositoriesGrid', {
            header: gettext('Comment'),
            dataIndex: 'Comment',
            flex: 2,
+           renderer: Ext.String.htmlEncode,
        },
     ],
 
-    addAdditionalInfos: function(gridData, infos) {
-       let me = this;
-
-       let warnings = {};
-       let origins = {};
-
-       let addLine = function(obj, key, line) {
-           if (obj[key]) {
-               obj[key] += "\n";
-               obj[key] += line;
-           } else {
-               obj[key] = line;
-           }
-       };
-
-       for (const info of infos) {
-           const key = `${info.path}:${info.index}`;
-           if (info.kind === 'warning' ||
-               (info.kind === 'ignore-pre-upgrade-warning' && !me.majorUpgradeAllowed)
-           ) {
-               addLine(warnings, key, gettext('Warning') + ": " + info.message);
-           } else if (info.kind === 'origin') {
-               origins[key] = info.message;
-           }
-       }
-
-       gridData.forEach(function(record) {
-           const key = `${record.Path}:${record.Index}`;
-           record.Origin = origins[key];
-       });
-
-       me.rowBodyFeature.getAdditionalData = function(innerData, rowIndex, record, orig) {
-           let headerCt = this.view.headerCt;
-           let colspan = headerCt.getColumnCount();
-
-           const key = `${innerData.Path}:${innerData.Index}`;
-           const warning_text = warnings[key];
+    features: [
+       {
+           ftype: 'grouping',
+           groupHeaderTpl: '{[ "File: " + values.name ]} ({rows.length} repositor{[values.rows.length > 1 ? "ies" : "y"]})',
+           enableGroupingMenu: false,
+       },
+    ],
 
-           return {
-               rowBody: '<div style="color: red; white-space: pre-line">' +
-                   Ext.String.htmlEncode(warning_text) + '</div>',
-               rowBodyCls: warning_text ? '' : Ext.baseCSSPrefix + 'grid-row-body-hidden',
-               rowBodyColspan: colspan,
-           };
-       };
+    store: {
+       model: 'apt-repolist',
+       groupField: 'Path',
+       sorters: [
+           {
+               property: 'Index',
+               direction: 'ASC',
+           },
+       ],
     },
 
     initComponent: function() {
@@ -376,49 +391,8 @@ Ext.define('Proxmox.node.APTRepositoriesGrid', {
            throw "no node name specified";
        }
 
-       let store = Ext.create('Ext.data.Store', {
-           model: 'apt-repolist',
-           groupField: 'Path',
-           sorters: [
-               {
-                   property: 'Index',
-                   direction: 'ASC',
-               },
-           ],
-       });
-
-       let rowBodyFeature = Ext.create('Ext.grid.feature.RowBody', {});
-
-       let groupingFeature = Ext.create('Ext.grid.feature.Grouping', {
-           groupHeaderTpl: '{[ "File: " + values.name ]} ({rows.length} ' +
-               'repositor{[values.rows.length > 1 ? "ies" : "y"]})',
-           enableGroupingMenu: false,
-       });
-
-       let sm = Ext.create('Ext.selection.RowModel', {});
-
-       Ext.apply(me, {
-           store: store,
-           selModel: sm,
-           rowBodyFeature: rowBodyFeature,
-           features: [groupingFeature, rowBodyFeature],
-       });
-
        me.callParent();
     },
-
-    listeners: {
-       selectionchange: function() {
-           let me = this;
-
-           if (me.onSelectionChange) {
-               let sm = me.getSelectionModel();
-               let rec = sm.getSelection()[0];
-
-               me.onSelectionChange(rec, sm);
-           }
-       },
-    },
 });
 
 Ext.define('Proxmox.node.APTRepositories', {
@@ -428,84 +402,218 @@ Ext.define('Proxmox.node.APTRepositories', {
 
     digest: undefined,
 
+    onlineHelp: undefined,
+
     product: 'Proxmox VE', // default
 
-    viewModel: {
-       data: {
-           product: 'Proxmox VE', // default
-           errorCount: 0,
-           subscriptionActive: '',
-           noSubscriptionRepo: '',
-           enterpriseRepo: '',
+    classifyOrigin: function(origin) {
+       origin ||= '';
+       if (origin.match(/^\s*Proxmox\s*$/i)) {
+           return 'Proxmox';
+       } else if (origin.match(/^\s*Debian\s*(:?Backports)?$/i)) {
+           return 'Debian';
+       }
+       return 'Other';
+    },
+
+    controller: {
+       xclass: 'Ext.app.ViewController',
+
+       selectionChange: function(grid, selection) {
+           let me = this;
+           if (!selection || selection.length < 1) {
+               return;
+           }
+           let rec = selection[0];
+           let vm = me.getViewModel();
+           vm.set('selectionenabled', rec.get('Enabled'));
+           vm.notify();
        },
-       formulas: {
-           noErrors: (get) => get('errorCount') === 0,
-           mainWarning: function(get) {
-               // Not yet initialized
-               if (get('subscriptionActive') === '' ||
-                   get('enterpriseRepo') === '') {
-                   return '';
+
+       updateState: function() {
+           let me = this;
+           let vm = me.getViewModel();
+
+           let store = vm.get('errorstore');
+           store.removeAll();
+
+           let status = 'good'; // start with best, the helper below will downgrade if needed
+           let text = gettext('All OK, you have production-ready repositories configured!');
+
+           let addGood = message => store.add({ status: 'good', message });
+           let addWarn = (message, important) => {
+               if (status !== 'critical') {
+                   status = 'warning';
+                   text = important ? message : gettext('Warning');
                }
+               store.add({ status: 'warning', message });
+           };
+           let addCritical = (message, important) => {
+               status = 'critical';
+               text = important ? message : gettext('Error');
+               store.add({ status: 'critical', message });
+           };
 
-               let icon = `<i class='fa fa-fw fa-exclamation-triangle critical'></i>`;
-               let fmt = (msg) => `<div class="black">${icon}${gettext('Warning')}: ${msg}</div>`;
+           let errors = vm.get('errors');
+           errors.forEach(error => addCritical(`${error.path} - ${error.error}`));
+
+           let activeSubscription = vm.get('subscriptionActive');
+           let enterprise = vm.get('enterpriseRepo');
+           let nosubscription = vm.get('noSubscriptionRepo');
+           let test = vm.get('testRepo');
+           let cephRepos = {
+               enterprise: vm.get('cephEnterpriseRepo'),
+               nosubscription: vm.get('cephNoSubscriptionRepo'),
+               test: vm.get('cephTestRepo'),
+           };
+           let wrongSuites = vm.get('suitesWarning');
+           let mixedSuites = vm.get('mixedSuites');
+
+           if (!enterprise && !nosubscription && !test) {
+               addCritical(
+                   Ext.String.format(gettext('No {0} repository is enabled, you do not get any updates!'), vm.get('product')),
+               );
+           } else if (errors.length > 0) {
+               // nothing extra, just avoid that we show "get updates"
+           } else if (enterprise && !nosubscription && !test && activeSubscription) {
+               addGood(Ext.String.format(gettext('You get supported updates for {0}'), vm.get('product')));
+           } else if (nosubscription || test) {
+               addGood(Ext.String.format(gettext('You get updates for {0}'), vm.get('product')));
+           }
 
-               if (!get('subscriptionActive') && get('enterpriseRepo')) {
-                   return fmt(gettext('The enterprise repository is enabled, but there is no active subscription!'));
+           if (wrongSuites) {
+               addWarn(gettext('Some suites are misconfigured'));
+           }
+
+           if (mixedSuites) {
+               addWarn(gettext('Detected mixed suites before upgrade'));
+           }
+
+           let productionReadyCheck = (repos, type, noSubAlternateName) => {
+               if (!activeSubscription && repos.enterprise) {
+                   addWarn(Ext.String.format(
+                       gettext('The {0}enterprise repository is enabled, but there is no active subscription!'),
+                       type,
+                   ));
                }
 
-               if (get('noSubscriptionRepo')) {
-                   return fmt(gettext('The no-subscription repository is not recommended for production use!'));
+               if (repos.nosubscription) {
+                   addWarn(Ext.String.format(
+                       gettext('The {0}no-subscription{1} repository is not recommended for production use!'),
+                       type,
+                       noSubAlternateName,
+                   ));
                }
 
-               if (!get('enterpriseRepo') && !get('noSubscriptionRepo')) {
-                   let msg = Ext.String.format(gettext('No {0} repository is enabled!'), get('product'));
-                   return fmt(msg);
+               if (repos.test) {
+                   addWarn(Ext.String.format(
+                       gettext('The {0}test repository may pull in unstable updates and is not recommended for production use!'),
+                       type,
+                   ));
                }
+           };
 
-               return '';
-           },
+           productionReadyCheck({ enterprise, nosubscription, test }, '', '');
+           // TODO drop alternate 'main' name when no longer relevant
+           productionReadyCheck(cephRepos, 'Ceph ', '/main');
+
+           if (errors.length > 0) {
+               text = gettext('Fatal parsing error for at least one repository');
+           }
+
+           let iconCls = Proxmox.Utils.get_health_icon(status, true);
+
+           vm.set('state', {
+               iconCls,
+               text,
+           });
        },
     },
 
-    items: [
-       {
-           xtype: 'header',
-           baseCls: 'x-panel-header',
-           bind: {
-               hidden: '{!mainWarning}',
-               title: '{mainWarning}',
-           },
+    viewModel: {
+       data: {
+           product: 'Proxmox VE', // default
+           errors: [],
+           suitesWarning: false,
+           mixedSuites: false, // used before major upgrade
+           subscriptionActive: '',
+           noSubscriptionRepo: '',
+           enterpriseRepo: '',
+           testRepo: '',
+           cephEnterpriseRepo: '',
+           cephNoSubscriptionRepo: '',
+           cephTestRepo: '',
+           selectionenabled: false,
+           state: {},
        },
-       {
-           xtype: 'box',
-           bind: {
-               hidden: '{!mainWarning}',
+       formulas: {
+           enableButtonText: (get) => get('selectionenabled')
+               ? gettext('Disable') : gettext('Enable'),
+       },
+       stores: {
+           errorstore: {
+               fields: ['status', 'message'],
            },
-           height: 5,
        },
+    },
+
+    scrollable: true,
+    layout: {
+       type: 'vbox',
+       align: 'stretch',
+    },
+
+    items: [
        {
-           xtype: 'proxmoxNodeAPTRepositoriesErrors',
-           name: 'repositoriesErrors',
-           hidden: true,
-           padding: '0 0 5 0',
-           bind: {
-               hidden: '{noErrors}',
+           xtype: 'panel',
+           border: false,
+           layout: {
+               type: 'hbox',
+               align: 'stretch',
            },
+           height: 200,
+           title: gettext('Status'),
+           items: [
+               {
+                   xtype: 'box',
+                   flex: 2,
+                   margin: 10,
+                   data: {
+                       iconCls: Proxmox.Utils.get_health_icon(undefined, true),
+                       text: '',
+                   },
+                   bind: {
+                       data: '{state}',
+                   },
+                   tpl: [
+                       '<center class="centered-flex-column" style="font-size:15px;line-height: 25px;">',
+                       '<i class="fa fa-4x {iconCls}"></i>',
+                       '{text}',
+                       '</center>',
+                   ],
+               },
+               {
+                   xtype: 'proxmoxNodeAPTRepositoriesErrors',
+                   name: 'repositoriesErrors',
+                   flex: 7,
+                   margin: 10,
+                   bind: {
+                       store: '{errorstore}',
+                   },
+               },
+           ],
        },
        {
            xtype: 'proxmoxNodeAPTRepositoriesGrid',
            name: 'repositoriesGrid',
+           flex: 1,
            cbind: {
                nodename: '{nodename}',
+               onlineHelp: '{onlineHelp}',
            },
            majorUpgradeAllowed: false, // TODO get release information from an API call?
-           onSelectionChange: function(rec, sm) {
-               let me = this;
-               if (rec) {
-                   let btn = me.up('proxmoxNodeAPTRepositories').down('#repoEnableButton');
-                   btn.setText(rec.get('Enabled') ? gettext('Disable') : gettext('Enable'));
-               }
+           listeners: {
+               selectionchange: 'selectionChange',
            },
        },
     ],
@@ -522,6 +630,7 @@ Ext.define('Proxmox.node.APTRepositories', {
                const res = response.result;
                const subscription = !(!res || !res.data || res.data.status.toLowerCase() !== 'active');
                vm.set('subscriptionActive', subscription);
+               me.getController().updateState();
            },
        });
     },
@@ -530,9 +639,9 @@ Ext.define('Proxmox.node.APTRepositories', {
        let me = this;
        let vm = me.getViewModel();
 
-       let addButton = me.down('#addButton');
-       addButton.repoInfo = [];
+       let addButton = me.down('button[name=addRepo]');
 
+       addButton.repoInfo = [];
        for (const standardRepo of standardRepos) {
            const handle = standardRepo.handle;
            const status = standardRepo.status;
@@ -541,7 +650,16 @@ Ext.define('Proxmox.node.APTRepositories', {
                vm.set('enterpriseRepo', status);
            } else if (handle === "no-subscription") {
                vm.set('noSubscriptionRepo', status);
+           } else if (handle === 'test') {
+               vm.set('testRepo', status);
+           } else if (handle.match(/^ceph-[a-zA-Z]+-enterprise$/)) {
+               vm.set('cephEnterpriseRepo', status);
+           } else if (handle.match(/^ceph-[a-zA-Z]+-no-subscription$/)) {
+               vm.set('cephNoSubscriptionRepo', status);
+           } else if (handle.match(/^ceph-[a-zA-Z]+-test$/)) {
+               vm.set('cephTestRepo', status);
            }
+           me.getController().updateState();
 
            addButton.repoInfo.push(standardRepo);
            addButton.digest = me.digest;
@@ -554,12 +672,17 @@ Ext.define('Proxmox.node.APTRepositories', {
        let me = this;
        let vm = me.getViewModel();
        let repoGrid = me.down('proxmoxNodeAPTRepositoriesGrid');
-       let errorGrid = me.down('proxmoxNodeAPTRepositoriesErrors');
 
        me.store.load(function(records, operation, success) {
            let gridData = [];
            let errors = [];
            let digest;
+           let suitesWarning = false;
+
+           // Usually different suites will give errors anyways, but before a major upgrade the
+           // current and the next suite are allowed, so it makes sense to check for mixed suites.
+           let checkMixedSuites = false;
+           let mixedSuites = false;
 
            if (success && records.length > 0) {
                let data = records[0].data;
@@ -567,16 +690,69 @@ Ext.define('Proxmox.node.APTRepositories', {
                errors = data.errors;
                digest = data.digest;
 
+               let infos = {};
+               for (const info of data.infos) {
+                   let path = info.path;
+                   let idx = info.index;
+
+                   if (!infos[path]) {
+                       infos[path] = {};
+                   }
+                   if (!infos[path][idx]) {
+                       infos[path][idx] = {
+                           origin: '',
+                           warnings: [],
+                           // Used as a heuristic to detect mixed repositories pre-upgrade. The
+                           // warning is set on all repositories that do configure the next suite.
+                           gotIgnorePreUpgradeWarning: false,
+                       };
+                   }
+
+                   if (info.kind === 'origin') {
+                       infos[path][idx].origin = info.message;
+                   } else if (info.kind === 'warning') {
+                       infos[path][idx].warnings.push(info);
+                   } else if (info.kind === 'ignore-pre-upgrade-warning') {
+                       infos[path][idx].gotIgnorePreUpgradeWarning = true;
+                       if (!repoGrid.majorUpgradeAllowed) {
+                           infos[path][idx].warnings.push(info);
+                       } else {
+                           checkMixedSuites = true;
+                       }
+                   }
+               }
+
+
                files.forEach(function(file) {
                    for (let n = 0; n < file.repositories.length; n++) {
                        let repo = file.repositories[n];
                        repo.Path = file.path;
                        repo.Index = n;
+                       if (infos[file.path] && infos[file.path][n]) {
+                           repo.Origin = infos[file.path][n].origin || Proxmox.Utils.unknownText;
+                           repo.warnings = infos[file.path][n].warnings || [];
+
+                           if (repo.Enabled) {
+                               if (repo.warnings.some(w => w.property === 'Suites')) {
+                                   suitesWarning = true;
+                               }
+
+                               let originType = me.classifyOrigin(repo.Origin);
+                               // Only Proxmox and Debian repositories checked here, because the
+                               // warning can be missing for others for a different reason (e.g.
+                               // using 'stable' or non-Debian code names).
+                               if (checkMixedSuites && repo.Types.includes('deb') &&
+                                   (originType === 'Proxmox' || originType === 'Debian') &&
+                                   !infos[file.path][n].gotIgnorePreUpgradeWarning
+                               ) {
+                                   mixedSuites = true;
+                               }
+                           }
+                       }
                        gridData.push(repo);
                    }
                });
 
-               repoGrid.addAdditionalInfos(gridData, data.infos);
                repoGrid.store.loadData(gridData);
 
                me.updateStandardRepos(data['standard-repos']);
@@ -584,8 +760,10 @@ Ext.define('Proxmox.node.APTRepositories', {
 
            me.digest = digest;
 
-           vm.set('errorCount', errors.length);
-           errorGrid.store.loadData(errors);
+           vm.set('errors', errors);
+           vm.set('suitesWarning', suitesWarning);
+           vm.set('mixedSuites', mixedSuites);
+           me.getController().updateState();
        });
 
        me.check_subscription();