]> git.proxmox.com Git - proxmox-widget-toolkit.git/commitdiff
add UI for APT repositories
authorFabian Ebner <f.ebner@proxmox.com>
Wed, 23 Jun 2021 13:38:59 +0000 (15:38 +0200)
committerThomas Lamprecht <t.lamprecht@proxmox.com>
Wed, 23 Jun 2021 14:01:06 +0000 (16:01 +0200)
Signed-off-by: Fabian Ebner <f.ebner@proxmox.com>
src/Makefile
src/node/APTRepositories.js [new file with mode: 0644]

index 37da480f698b9f73fd17d9060b3f0e9814589108..23f23601704564985d277d152732fec5ebc63f77 100644 (file)
@@ -72,6 +72,7 @@ JSSRC=                                        \
        window/ACMEDomains.js           \
        window/FileBrowser.js           \
        node/APT.js                     \
+       node/APTRepositories.js         \
        node/NetworkEdit.js             \
        node/NetworkView.js             \
        node/DNSEdit.js                 \
diff --git a/src/node/APTRepositories.js b/src/node/APTRepositories.js
new file mode 100644 (file)
index 0000000..30c31ec
--- /dev/null
@@ -0,0 +1,423 @@
+Ext.define('apt-repolist', {
+    extend: 'Ext.data.Model',
+    fields: [
+       'Path',
+       'Index',
+       'OfficialHost',
+       'FileType',
+       'Enabled',
+       'Comment',
+       'Types',
+       'URIs',
+       'Suites',
+       'Components',
+       'Options',
+    ],
+});
+
+Ext.define('Proxmox.node.APTRepositoriesErrors', {
+    extend: 'Ext.grid.GridPanel',
+
+    xtype: 'proxmoxNodeAPTRepositoriesErrors',
+
+    title: gettext('Errors'),
+
+    store: {},
+
+    viewConfig: {
+       stripeRows: false,
+       getRowClass: () => 'proxmox-invalid-row',
+    },
+
+    columns: [
+       {
+           header: gettext('File'),
+           dataIndex: 'path',
+           renderer: function(value, cell, record) {
+               return "<i class='pve-grid-fa fa fa-fw " +
+                   "fa-exclamation-triangle'></i>" + value;
+           },
+           width: 350,
+       },
+       {
+           header: gettext('Error'),
+           dataIndex: 'error',
+           flex: 1,
+       },
+    ],
+});
+
+Ext.define('Proxmox.node.APTRepositoriesGrid', {
+    extend: 'Ext.grid.GridPanel',
+
+    xtype: 'proxmoxNodeAPTRepositoriesGrid',
+
+    title: gettext('APT Repositories'),
+
+    tbar: [
+       {
+           text: gettext('Reload'),
+           iconCls: 'fa fa-refresh',
+           handler: function() {
+               let me = this;
+               me.up('proxmoxNodeAPTRepositories').reload();
+           },
+       },
+    ],
+
+    sortableColumns: false,
+
+    columns: [
+       {
+           header: gettext('Official'),
+           dataIndex: 'OfficialHost',
+           renderer: function(value, cell, record) {
+               let icon = (cls) => `<i class="fa fa-fw ${cls}"></i>`;
+
+               const enabled = record.data.Enabled;
+
+               if (value === undefined || value === null) {
+                   return icon('fa-question-circle-o');
+               }
+               if (!value) {
+                   return icon('fa-times ' + (enabled ? 'critical' : 'faded'));
+               }
+               return icon('fa-check ' + (enabled ? 'good' : 'faded'));
+           },
+           width: 70,
+       },
+       {
+           header: gettext('Enabled'),
+           dataIndex: 'Enabled',
+           renderer: Proxmox.Utils.format_enabled_toggle,
+           width: 90,
+       },
+       {
+           header: gettext('Types'),
+           dataIndex: 'Types',
+           renderer: function(types, cell, record) {
+               return types.join(' ');
+           },
+           width: 100,
+       },
+       {
+           header: gettext('URIs'),
+           dataIndex: 'URIs',
+           renderer: function(uris, cell, record) {
+               return uris.join(' ');
+           },
+           width: 350,
+       },
+       {
+           header: gettext('Suites'),
+           dataIndex: 'Suites',
+           renderer: function(suites, cell, record) {
+               return suites.join(' ');
+           },
+           width: 130,
+       },
+       {
+           header: gettext('Components'),
+           dataIndex: 'Components',
+           renderer: function(components, cell, record) {
+               return components.join(' ');
+           },
+           width: 170,
+       },
+       {
+           header: gettext('Options'),
+           dataIndex: 'Options',
+           renderer: function(options, cell, record) {
+               if (!options) {
+                   return '';
+               }
+
+               let filetype = record.data.FileType;
+               let text = '';
+
+               options.forEach(function(option) {
+                   let key = option.Key;
+                   if (filetype === 'list') {
+                       let values = option.Values.join(',');
+                       text += `${key}=${values} `;
+                   } else if (filetype === 'sources') {
+                       let values = option.Values.join(' ');
+                       text += `${key}: ${values}<br>`;
+                   } else {
+                       throw "unkown file type";
+                   }
+               });
+               return text;
+           },
+           flex: 1,
+       },
+       {
+           header: gettext('Comment'),
+           dataIndex: 'Comment',
+           flex: 2,
+       },
+    ],
+
+    addAdditionalInfos: function(gridData, infos) {
+       let me = this;
+
+       let warnings = {};
+       let officialHosts = {};
+
+       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 === 'badge' && info.message === 'official host name') {
+               officialHosts[key] = true;
+           }
+       }
+
+       gridData.forEach(function(record) {
+           const key = `${record.Path}:${record.Index}`;
+           record.OfficialHost = !!officialHosts[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];
+
+           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,
+           };
+       };
+    },
+
+    initComponent: function() {
+       let me = this;
+
+       if (!me.nodename) {
+           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();
+    },
+});
+
+Ext.define('Proxmox.node.APTRepositories', {
+    extend: 'Ext.panel.Panel',
+
+    xtype: 'proxmoxNodeAPTRepositories',
+    mixins: ['Proxmox.Mixin.CBind'],
+
+    digest: undefined,
+
+    viewModel: {
+       data: {
+           errorCount: 0,
+           subscriptionActive: '',
+           noSubscriptionRepo: '',
+           enterpriseRepo: '',
+       },
+       formulas: {
+           noErrors: (get) => get('errorCount') === 0,
+           mainWarning: function(get) {
+               // Not yet initialized
+               if (get('subscriptionActive') === '' ||
+                   get('enterpriseRepo') === '') {
+                   return '';
+               }
+
+               let withStyle = (msg) => "<div style='color:red;'><i class='fa fa-fw " +
+                   "fa-exclamation-triangle'></i>" + gettext('Warning') + ': ' + msg + "</div>";
+
+               if (!get('subscriptionActive') && get('enterpriseRepo')) {
+                   return withStyle(gettext('The enterprise repository is ' +
+                       'enabled, but there is no active subscription!'));
+               }
+
+               if (get('noSubscriptionRepo')) {
+                   return withStyle(gettext('The no-subscription repository is ' +
+                       'not recommended for production use!'));
+               }
+
+               if (!get('enterpriseRepo') && !get('noSubscriptionRepo')) {
+                   return withStyle(gettext('No Proxmox repository is enabled!'));
+               }
+
+               return '';
+           },
+       },
+    },
+
+    items: [
+       {
+           title: gettext('Warning'),
+           name: 'repositoriesMainWarning',
+           xtype: 'panel',
+           bind: {
+               title: '{mainWarning}',
+               hidden: '{!mainWarning}',
+           },
+       },
+       {
+           xtype: 'proxmoxNodeAPTRepositoriesErrors',
+           name: 'repositoriesErrors',
+           hidden: true,
+           bind: {
+               hidden: '{noErrors}',
+           },
+       },
+       {
+           xtype: 'proxmoxNodeAPTRepositoriesGrid',
+           name: 'repositoriesGrid',
+           cbind: {
+               nodename: '{nodename}',
+           },
+           majorUpgradeAllowed: false, // TODO get release information from an API call?
+       },
+    ],
+
+    check_subscription: function() {
+       let me = this;
+       let vm = me.getViewModel();
+
+       Proxmox.Utils.API2Request({
+           url: `/nodes/${me.nodename}/subscription`,
+           method: 'GET',
+           failure: function(response, opts) {
+               Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+           },
+           success: function(response, opts) {
+               const res = response.result;
+               const subscription = !(res === null || res === undefined ||
+                   !res || res.data.status.toLowerCase() !== 'active');
+               vm.set('subscriptionActive', subscription);
+           },
+       });
+    },
+
+    updateStandardRepos: function(standardRepos) {
+       let me = this;
+       let vm = me.getViewModel();
+
+       for (const standardRepo of standardRepos) {
+           const handle = standardRepo.handle;
+           const status = standardRepo.status;
+
+           if (handle === "enterprise") {
+               vm.set('enterpriseRepo', status);
+           } else if (handle === "no-subscription") {
+               vm.set('noSubscriptionRepo', status);
+           }
+       }
+    },
+
+    reload: function() {
+       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;
+
+           if (success && records.length > 0) {
+               let data = records[0].data;
+               let files = data.files;
+               errors = data.errors;
+               digest = data.digest;
+
+               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;
+                       gridData.push(repo);
+                   }
+               });
+
+               repoGrid.addAdditionalInfos(gridData, data.infos);
+               repoGrid.store.loadData(gridData);
+
+               me.updateStandardRepos(data['standard-repos']);
+           }
+
+           me.digest = digest;
+
+           vm.set('errorCount', errors.length);
+           errorGrid.store.loadData(errors);
+       });
+
+       me.check_subscription();
+    },
+
+    listeners: {
+       activate: function() {
+           let me = this;
+           me.reload();
+       },
+    },
+
+    initComponent: function() {
+       let me = this;
+
+       if (!me.nodename) {
+           throw "no node name specified";
+       }
+
+       let store = Ext.create('Ext.data.Store', {
+           proxy: {
+               type: 'proxmox',
+               url: `/api2/json/nodes/${me.nodename}/apt/repositories`,
+           },
+       });
+
+       Ext.apply(me, { store: store });
+
+       Proxmox.Utils.monStoreErrors(me, me.store, true);
+
+       me.callParent();
+    },
+});