Ext.define('apt-repolist', { extend: 'Ext.data.Model', fields: [ 'Path', 'Index', 'Origin', 'FileType', 'Enabled', 'Comment', 'Types', 'URIs', 'Suites', 'Components', 'Options', ], }); Ext.define('Proxmox.window.APTRepositoryAdd', { extend: 'Proxmox.window.Edit', alias: 'widget.pmxAPTRepositoryAdd', isCreate: true, isAdd: true, subject: gettext('Repository'), width: 600, initComponent: function() { let me = this; if (!me.repoInfo || me.repoInfo.length === 0) { throw "repository information not initialized"; } let description = Ext.create('Ext.form.field.Display', { fieldLabel: gettext('Description'), name: 'description', }); let status = Ext.create('Ext.form.field.Display', { fieldLabel: gettext('Status'), name: 'status', renderer: function(value) { let statusText = gettext('Not yet configured'); if (value !== '') { statusText = Ext.String.format( '{0}: {1}', gettext('Configured'), value ? gettext('enabled') : gettext('disabled'), ); } return statusText; }, }); let repoSelector = Ext.create('Proxmox.form.KVComboBox', { fieldLabel: gettext('Repository'), xtype: 'proxmoxKVComboBox', name: 'handle', allowBlank: false, comboItems: me.repoInfo.map(info => [info.handle, info.name]), 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 (!valid || !handle) { return false; } const info = me.repoInfo.find(elem => elem.handle === handle); if (!info) { return false; } if (info.status) { return Ext.String.format(gettext('{0} is already configured'), renderedValue); } return valid; }, listeners: { change: function(f, value) { const info = me.repoInfo.find(elem => elem.handle === value); description.setValue(info.description); status.setValue(info.status); }, }, }); repoSelector.setValue(me.repoInfo[0].handle); Ext.apply(me, { items: [ repoSelector, description, status, ], repoSelector: repoSelector, }); me.callParent(); }, }); Ext.define('Proxmox.node.APTRepositoriesErrors', { extend: 'Ext.grid.GridPanel', xtype: 'proxmoxNodeAPTRepositoriesErrors', store: {}, scrollable: true, viewConfig: { stripeRows: false, getRowClass: (record) => { switch (record.data.status) { case 'warning': return 'proxmox-warning-row'; case 'critical': return 'proxmox-invalid-row'; default: return ''; } }, }, hideHeaders: true, columns: [ { dataIndex: 'status', renderer: (value) => ``, width: 50, }, { dataIndex: 'message', flex: 1, }, ], }); Ext.define('Proxmox.node.APTRepositoriesGrid', { extend: 'Ext.grid.GridPanel', xtype: 'proxmoxNodeAPTRepositoriesGrid', title: gettext('APT Repositories'), cls: 'proxmox-apt-repos', // to allow applying styling to general components with local effect border: false, tbar: [ { text: gettext('Reload'), iconCls: 'fa fa-refresh', handler: function() { let me = this; me.up('proxmoxNodeAPTRepositories').reload(); }, }, { text: gettext('Add'), id: 'addButton', disabled: true, repoInfo: undefined, handler: function(button, event, record) { Proxmox.Utils.checked_command(() => { let me = this; let panel = me.up('proxmoxNodeAPTRepositories'); let extraParams = {}; if (panel.digest !== undefined) { extraParams.digest = panel.digest; } Ext.create('Proxmox.window.APTRepositoryAdd', { repoInfo: me.repoInfo, url: `/api2/extjs/nodes/${panel.nodename}/apt/repositories`, method: 'PUT', extraRequestParams: extraParams, listeners: { destroy: function() { panel.reload(); }, }, }).show(); }); }, }, '-', { xtype: 'proxmoxButton', text: gettext('Enable'), defaultText: gettext('Enable'), altText: gettext('Disable'), id: 'repoEnableButton', disabled: true, bind: { text: '{enableButtonText}', }, handler: function(button, event, record) { let me = this; let panel = me.up('proxmoxNodeAPTRepositories'); let params = { path: record.data.Path, index: record.data.Index, enabled: record.data.Enabled ? 0 : 1, // invert }; if (panel.digest !== undefined) { params.digest = panel.digest; } Proxmox.Utils.API2Request({ url: `/nodes/${panel.nodename}/apt/repositories`, method: 'POST', params: params, failure: function(response, opts) { Ext.Msg.alert(gettext('Error'), response.htmlStatus); panel.reload(); }, success: function(response, opts) { panel.reload(); }, }); }, 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 }, 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, 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('
'))}"`; if (record.data.Enabled) { metaData.tdCls = 'proxmox-invalid-row'; err = ' '; } else { metaData.tdCls = 'proxmox-warning-row'; err = ' '; } } return suites.join(' ') + err; }, width: 130, }, { header: gettext('Components'), dataIndex: 'Components', renderer: function(components, metaData, record) { let err = ''; if (components.length === 1) { // FIXME: this should be a flag set to the actual repsotiories, i.e., a tristate // like production-ready = (Option) if (components[0].match(/\w+(-no-subscription|test)\s*$/i)) { metaData.tdCls = 'proxmox-warning-row'; err = ' '; 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, }, { 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('Origin'), dataIndex: 'Origin', width: 120, renderer: (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)) { cls = 'pmx-itype-icon pmx-itype-icon-proxmox-x'; } else if (value.match(/^\s*Debian\s*$/i)) { cls = 'pmx-itype-icon pmx-itype-icon-debian-swirl'; } return ` ${value}`; }, }, { header: gettext('Comment'), dataIndex: 'Comment', flex: 2, }, ], 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 groupingFeature = Ext.create('Ext.grid.feature.Grouping', { groupHeaderTpl: '{[ "File: " + values.name ]} ({rows.length} ' + 'repositor{[values.rows.length > 1 ? "ies" : "y"]})', enableGroupingMenu: false, }); Ext.apply(me, { store: store, features: [groupingFeature], }); me.callParent(); }, }); Ext.define('Proxmox.node.APTRepositories', { extend: 'Ext.panel.Panel', xtype: 'proxmoxNodeAPTRepositories', mixins: ['Proxmox.Mixin.CBind'], digest: undefined, product: 'Proxmox VE', // default 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(); }, updateState: function() { let me = this; let vm = me.getViewModel(); let store = vm.get('errorstore'); store.removeAll(); let errors = vm.get('errors'); errors.forEach((error) => { store.add({ status: 'critical', message: `${error.path} - ${error.error}`, }); }); let text = gettext('Repositories are configured in a recommended way'); let status = 'good'; let activeSubscription = vm.get('subscriptionActive'); let enterprise = vm.get('enterpriseRepo'); let nosubscription = vm.get('noSubscriptionRepo'); let test = vm.get('testRepo'); let wrongSuites = vm.get('suitesWarning'); let addGood = function(message) { store.add({ status: 'good', message, }); }; let addWarn = function(message) { status = 'warning'; text = message; store.add({ status, message, }); }; if (!enterprise && !nosubscription && !test) { addWarn(Ext.String.format(gettext('No {0} repository is enabled!'), vm.get('product'))); } 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 (wrongSuites) { addWarn(gettext('Some Suites are misconfigured')); } if (!activeSubscription && enterprise) { addWarn(gettext('The enterprise repository is enabled, but there is no active subscription!')); } if (nosubscription) { addWarn(gettext('The no-subscription repository is not recommended for production use!')); } if (test) { addWarn(gettext('The test repository is not recommended for production use!')); } if (errors.length > 0) { vm.set('state', { iconCls: Proxmox.Utils.get_health_icon('critical', true), text: gettext('Error parsing repositories'), }); return; } let iconCls = Proxmox.Utils.get_health_icon(status, true); vm.set('state', { iconCls, text, }); }, }, viewModel: { data: { product: 'Proxmox VE', // default errors: [], suitesWarning: false, subscriptionActive: '', noSubscriptionRepo: '', enterpriseRepo: '', testRepo: '', selectionenabled: false, state: {}, }, formulas: { enableButtonText: (get) => get('selectionenabled') ? gettext('Disable') : gettext('Enable'), }, stores: { errorstore: { fields: ['status', 'message'], }, }, }, scrollable: true, layout: { type: 'vbox', align: 'stretch', }, items: [ { xtype: 'panel', border: false, layout: { type: 'hbox', align: 'stretch', }, height: 200, title: gettext('Status'), items: [ { xtype: 'box', flex: 1, margin: 10, data: { iconCls: Proxmox.Utils.get_health_icon(undefined, true), text: '', }, bind: { data: '{state}', }, tpl: [ '
', '', '

', '{text}', '
', ], }, { xtype: 'proxmoxNodeAPTRepositoriesErrors', name: 'repositoriesErrors', flex: 2, margin: 10, bind: { store: '{errorstore}', }, }, ], }, { xtype: 'proxmoxNodeAPTRepositoriesGrid', name: 'repositoriesGrid', flex: 1, cbind: { nodename: '{nodename}', }, majorUpgradeAllowed: false, // TODO get release information from an API call? listeners: { selectionchange: 'selectionChange', }, }, ], check_subscription: function() { let me = this; let vm = me.getViewModel(); Proxmox.Utils.API2Request({ url: `/nodes/${me.nodename}/subscription`, method: 'GET', failure: (response, opts) => Ext.Msg.alert(gettext('Error'), response.htmlStatus), success: function(response, opts) { const res = response.result; const subscription = !(!res || !res.data || res.data.status.toLowerCase() !== 'active'); vm.set('subscriptionActive', subscription); me.getController().updateState(); }, }); }, updateStandardRepos: function(standardRepos) { let me = this; let vm = me.getViewModel(); let addButton = me.down('#addButton'); addButton.repoInfo = []; 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); } else if (handle === 'test') { vm.set('testRepo', status); } me.getController().updateState(); addButton.repoInfo.push(standardRepo); addButton.digest = me.digest; } addButton.setDisabled(false); }, reload: function() { let me = this; let vm = me.getViewModel(); let repoGrid = me.down('proxmoxNodeAPTRepositoriesGrid'); me.store.load(function(records, operation, success) { let gridData = []; let errors = []; let digest; let suitesWarning = false; if (success && records.length > 0) { let data = records[0].data; let files = data.files; 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: [], }; } if (info.kind === 'origin') { infos[path][idx].origin = info.message; } else if (info.kind === 'warning' || (info.kind === 'ignore-pre-upgrade-warning' && !repoGrid.majorUpgradeAllowed) ) { infos[path][idx].warnings.push(info); if (!suitesWarning && info.property === 'Suites') { suitesWarning = true; } } else { throw 'unknown info'; } } 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 || []; } gridData.push(repo); } }); repoGrid.store.loadData(gridData); me.updateStandardRepos(data['standard-repos']); } me.digest = digest; vm.set('errors', errors); vm.set('suitesWarning', suitesWarning); me.getController().updateState(); }); 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(); me.getViewModel().set('product', me.product); }, });