From: Fabian Ebner Date: Wed, 23 Jun 2021 13:38:59 +0000 (+0200) Subject: add UI for APT repositories X-Git-Url: https://git.proxmox.com/?a=commitdiff_plain;h=24313a9d8001c549f5ea9ee0f358e16b81e629d7;p=proxmox-widget-toolkit.git add UI for APT repositories Signed-off-by: Fabian Ebner --- diff --git a/src/Makefile b/src/Makefile index 37da480..23f2360 100644 --- a/src/Makefile +++ b/src/Makefile @@ -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 index 0000000..30c31ec --- /dev/null +++ b/src/node/APTRepositories.js @@ -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 "" + 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) => ``; + + 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}
`; + } 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: '
' + + Ext.String.htmlEncode(warning_text) + '
', + 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) => "
" + gettext('Warning') + ': ' + msg + "
"; + + 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(); + }, +});