[pve-devel] [PATCH v7 proxmox-widget-toolkit 1/2] add UI for APT repositories

Fabian Ebner f.ebner at proxmox.com
Wed Jun 23 15:38:59 CEST 2021


Signed-off-by: Fabian Ebner <f.ebner at proxmox.com>
---

Changes from v6:
    * adapt to new API
    * squashed patch adding warnings/checks into this one
    * say 'enabled' instead of 'configured' in warnings
    * move tbar to grid component (selection model is needed for future buttons)
    * use greyed-out icon if repository is disabled

 src/Makefile                |   1 +
 src/node/APTRepositories.js | 423 ++++++++++++++++++++++++++++++++++++
 2 files changed, 424 insertions(+)
 create mode 100644 src/node/APTRepositories.js

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 "<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();
+    },
+});
-- 
2.30.2






More information about the pve-devel mailing list