[pve-devel] [PATCH v5 12/15] ui: add ResourceMapTree
Dominik Csapak
d.csapak at proxmox.com
Tue Jun 6 15:52:19 CEST 2023
this will be the base class for trees for the individual mapping types,
e.g. pci and usb mapping.
there are a few things to configure, but the overall code sharing is
still significant, and should work out fine for future mapping types
Signed-off-by: Dominik Csapak <d.csapak at proxmox.com>
---
www/manager6/Makefile | 1 +
www/manager6/tree/ResourceMapTree.js | 316 +++++++++++++++++++++++++++
2 files changed, 317 insertions(+)
create mode 100644 www/manager6/tree/ResourceMapTree.js
diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index c94aee64..938ec9f1 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -101,6 +101,7 @@ JSSRC= \
panel/MultiDiskEdit.js \
tree/ResourceTree.js \
tree/SnapshotTree.js \
+ tree/ResourceMapTree.js \
window/Backup.js \
window/BackupConfig.js \
window/BulkAction.js \
diff --git a/www/manager6/tree/ResourceMapTree.js b/www/manager6/tree/ResourceMapTree.js
new file mode 100644
index 00000000..df50b63a
--- /dev/null
+++ b/www/manager6/tree/ResourceMapTree.js
@@ -0,0 +1,316 @@
+Ext.define('PVE.tree.ResourceMapTree', {
+ extend: 'Ext.tree.Panel',
+ alias: 'widget.pveResourceMapTree',
+ mixins: ['Proxmox.Mixin.CBind'],
+
+ rootVisible: false,
+
+ emptyText: gettext('No Mapping found'),
+
+ // will be opened on edit
+ editWindowClass: undefined,
+
+ // The base url of the resource
+ baseUrl: undefined,
+
+ // icon class to show on the entries
+ mapIconCls: undefined,
+
+ // if given, should be a function that takes a nodename and returns
+ // the url for getting the data to check the status
+ getStatusCheckUrl: undefined,
+
+ // the result of above api call and the nodename is passed and can set the status
+ checkValidity: undefined,
+
+ // the property that denotes a single map entry for a node
+ entryIdProperty: undefined,
+
+ cbindData: function(initialConfig) {
+ let me = this;
+ const caps = Ext.state.Manager.get('GuiCap');
+ me.canConfigure = !!caps.mapping['Mapping.Modify'];
+
+ return {};
+ },
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ addMapping: function() {
+ let me = this;
+ let view = me.getView();
+ Ext.create(view.editWindowClass, {
+ url: view.baseUrl,
+ autoShow: true,
+ listeners: {
+ destroy: () => me.load(),
+ },
+ });
+ },
+
+ addHost: function() {
+ let me = this;
+ me.edit(false);
+ },
+
+ edit: function(includeNodename = true) {
+ let me = this;
+ let view = me.getView();
+ let selection = view.getSelection();
+ if (!selection || !selection.length) {
+ return;
+ }
+ let rec = selection[0];
+ if (!view.canConfigure || (rec.data.type === 'entry' && includeNodename)) {
+ return;
+ }
+
+ Ext.create(view.editWindowClass, {
+ url: `${view.baseUrl}/${rec.data.name}`,
+ autoShow: true,
+ autoLoad: true,
+ nodename: includeNodename ? rec.data.node : undefined,
+ name: rec.data.name,
+ listeners: {
+ destroy: () => me.load(),
+ },
+ });
+ },
+
+ remove: function() {
+ let me = this;
+ let view = me.getView();
+ let selection = view.getSelection();
+ if (!selection || !selection.length) {
+ return;
+ }
+
+ let data = selection[0].data;
+ let url = `${view.baseUrl}/${data.name}`;
+ let method = 'PUT';
+ let params = {
+ digest: me.lookup[data.name].digest,
+ };
+ let map = me.lookup[data.name].map;
+ switch (data.type) {
+ case 'entry':
+ method = 'DELETE';
+ params = undefined;
+ break;
+ case 'node':
+ params.map = PVE.Parser.filterPropertyStringList(map, (e) => e.node !== data.node);
+ break;
+ case 'map':
+ params.map = PVE.Parser.filterPropertyStringList(map, (e) =>
+ Object.entries(e).some(([key, value]) => data[key] !== value));
+ break;
+ default:
+ throw "invalid type";
+ }
+ if (!params?.map.length) {
+ method = 'DELETE';
+ params = undefined;
+ }
+ Proxmox.Utils.API2Request({
+ url,
+ method,
+ params,
+ success: function() {
+ me.load();
+ },
+ });
+ },
+
+ load: function() {
+ let me = this;
+ let view = me.getView();
+ Proxmox.Utils.API2Request({
+ url: view.baseUrl,
+ method: 'GET',
+ failure: response => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
+ success: function({ result: { data } }) {
+ let lookup = {};
+ data.forEach((entry) => {
+ lookup[entry.id] = Ext.apply({}, entry);
+ entry.iconCls = 'fa fa-fw fa-folder-o';
+ entry.name = entry.id;
+ entry.text = entry.id;
+ entry.type = 'entry';
+
+ let nodes = {};
+ for (const map of entry.map) {
+ let parsed = PVE.Parser.parsePropertyString(map);
+ parsed.iconCls = view.mapIconCls;
+ parsed.leaf = true;
+ parsed.name = entry.id;
+ parsed.text = parsed[view.entryIdProperty];
+ parsed.type = 'map';
+
+ if (nodes[parsed.node] === undefined) {
+ nodes[parsed.node] = {
+ children: [],
+ expanded: true,
+ iconCls: 'fa fa-fw fa-building-o',
+ leaf: false,
+ name: entry.id,
+ node: parsed.node,
+ text: parsed.node,
+ type: 'node',
+ };
+ }
+ nodes[parsed.node].children.push(parsed);
+ }
+ delete entry.id;
+ entry.children = Object.values(nodes);
+ entry.leaf = entry.children.length === 0;
+ });
+ me.lookup = lookup;
+ if (view.getStatusCheckUrl !== undefined && view.checkValidity !== undefined) {
+ me.loadStatusData();
+ }
+ view.setRootNode({
+ children: data,
+ });
+ let root = view.getRootNode();
+ root.expand();
+ root.childNodes.forEach(node => node.expand());
+ },
+ });
+ },
+
+ nodeLoadingState: {},
+
+ loadStatusData: function() {
+ let me = this;
+ let view = me.getView();
+ PVE.data.ResourceStore.getNodes().forEach(({ node }) => {
+ me.nodeLoadingState[node] = true;
+ let url = view.getStatusCheckUrl(node);
+ Proxmox.Utils.API2Request({
+ url,
+ method: 'GET',
+ failure: function(response) {
+ me.nodeLoadingState[node] = false;
+ view.getRootNode()?.cascade(function(rec) {
+ if (rec.data.node !== node) {
+ return;
+ }
+
+ rec.set('valid', 0);
+ rec.set('errmsg', response.htmlStatus);
+ rec.commit();
+ });
+ },
+ success: function({ result: { data } }) {
+ me.nodeLoadingState[node] = false;
+ view.checkValidity(data, node);
+ },
+ });
+ });
+ },
+
+ renderStatus: function(value, _metadata, record) {
+ let me = this;
+ if (record.data.type !== 'map') {
+ return '';
+ }
+ let iconCls;
+ let status;
+ if (value === undefined) {
+ if (me.nodeLoadingState[record.data.node]) {
+ iconCls = 'fa-spinner fa-spin';
+ status = gettext('Loading...');
+ } else {
+ iconCls = 'fa-question-circle';
+ status = gettext('Unknown Node');
+ }
+ } else {
+ let state = value ? 'good' : 'critical';
+ iconCls = PVE.Utils.get_health_icon(state, true);
+ status = value ? gettext("OK") : record.data.errmsg || Proxmox.Utils.unknownText;
+ }
+ return `<i class="fa ${iconCls}"></i> ${status}`;
+ },
+
+ init: function(view) {
+ let me = this;
+
+ ['editWindowClass', 'baseUrl', 'mapIconCls', 'entryIdProperty'].forEach((property) => {
+ if (view[property] === undefined) {
+ throw `No ${property} defined`;
+ }
+ });
+
+ me.load();
+ },
+ },
+
+ store: {
+ sorters: 'text',
+ data: {},
+ },
+
+
+ tbar: [
+ {
+ text: gettext('Add mapping'),
+ handler: 'addMapping',
+ cbind: {
+ disabled: '{!canConfigure}',
+ },
+ },
+ {
+ xtype: 'proxmoxButton',
+ text: gettext('New Host mapping'),
+ disabled: true,
+ parentXType: 'treepanel',
+ enableFn: function(_rec) {
+ return this.up('treepanel').canConfigure;
+ },
+ handler: 'addHost',
+ },
+ {
+ xtype: 'proxmoxButton',
+ text: gettext('Edit'),
+ disabled: true,
+ parentXType: 'treepanel',
+ enableFn: function(rec) {
+ return rec && rec.data.type !== 'entry' && this.up('treepanel').canConfigure;
+ },
+ handler: 'edit',
+ },
+ {
+ xtype: 'proxmoxButton',
+ parentXType: 'treepanel',
+ handler: 'remove',
+ disabled: true,
+ text: gettext('Remove'),
+ enableFn: function(rec) {
+ return rec && this.up('treepanel').canConfigure;
+ },
+ confirmMsg: function(rec) {
+ let msg, id;
+ let view = this.up('treepanel');
+ switch (rec.data.type) {
+ case 'entry':
+ msg = gettext("Are you sure you want to remove '{0}'");
+ return Ext.String.format(msg, rec.data.name);
+ case 'node':
+ msg = gettext("Are you sure you want to remove '{0}' entries for '{1}'");
+ return Ext.String.format(msg, rec.data.node, rec.data.name);
+ case 'map':
+ msg = gettext("Are you sure you want to remove '{0}' on '{1}' for '{2}'");
+ id = rec.data[view.entryIdProperty];
+ return Ext.String.format(msg, id, rec.data.node, rec.data.name);
+ default:
+ throw "invalid type";
+ }
+ },
+ },
+ ],
+
+ listeners: {
+ itemdblclick: 'edit',
+ },
+});
--
2.30.2
More information about the pve-devel
mailing list