From: Dominik Csapak <d.csapak@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [pve-devel] [PATCH v5 12/15] ui: add ResourceMapTree
Date: Tue, 6 Jun 2023 15:52:19 +0200 [thread overview]
Message-ID: <20230606135222.984747-21-d.csapak@proxmox.com> (raw)
In-Reply-To: <20230606135222.984747-1-d.csapak@proxmox.com>
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@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
next prev parent reply other threads:[~2023-06-06 13:53 UTC|newest]
Thread overview: 30+ messages / expand[flat|nested] mbox.gz Atom feed top
2023-06-06 13:51 [pve-devel] [PATCH access-control/guest-common/qemu-server/manager v5] cluster mapping Dominik Csapak
2023-06-06 13:52 ` [pve-devel] [PATCH access-control v5 1/1] add privileges and paths for cluster resource mapping Dominik Csapak
2023-06-07 17:03 ` [pve-devel] applied: " Thomas Lamprecht
2023-06-06 13:52 ` [pve-devel] [PATCH guest-common v5 1/1] add PCI/USB Mapping configs Dominik Csapak
2023-06-07 17:17 ` [pve-devel] applied: " Thomas Lamprecht
2023-06-06 13:52 ` [pve-devel] [PATCH qemu-server v5 1/6] enable cluster mapped USB devices for guests Dominik Csapak
2023-06-13 12:23 ` Wolfgang Bumiller
2023-06-06 13:52 ` [pve-devel] [PATCH qemu-server v5 2/6] enable cluster mapped PCI " Dominik Csapak
2023-06-06 13:52 ` [pve-devel] [PATCH qemu-server v5 3/6] check_local_resources: extend for mapped resources Dominik Csapak
2023-06-13 12:43 ` Wolfgang Bumiller
2023-06-06 13:52 ` [pve-devel] [PATCH qemu-server v5 4/6] api: migrate preconditions: use new check_local_resources info Dominik Csapak
2023-06-13 12:46 ` Wolfgang Bumiller
2023-06-06 13:52 ` [pve-devel] [PATCH qemu-server v5 5/6] migration: check for mapped resources Dominik Csapak
2023-06-06 13:52 ` [pve-devel] [PATCH qemu-server v5 6/6] add test for mapped pci devices Dominik Csapak
2023-06-06 13:52 ` [pve-devel] [PATCH v5 01/15] pvesh: fix parameters for proxyto_callback Dominik Csapak
2023-06-06 13:52 ` [pve-devel] [PATCH v5 02/15] api: add resource map api endpoints for PCI and USB Dominik Csapak
2023-06-13 11:26 ` Wolfgang Bumiller
2023-06-06 13:52 ` [pve-devel] [PATCH v5 03/15] ui: parser: add helpers for lists of property strings Dominik Csapak
2023-06-06 13:52 ` [pve-devel] [PATCH v5 04/15] ui: form/USBSelector: make it more flexible with nodename Dominik Csapak
2023-06-06 13:52 ` [pve-devel] [PATCH v5 05/15] ui: form: add PCIMapSelector Dominik Csapak
2023-06-06 13:52 ` [pve-devel] [PATCH v5 06/15] ui: form: add USBMapSelector Dominik Csapak
2023-06-06 13:52 ` [pve-devel] [PATCH v5 07/15] ui: qemu/PCIEdit: rework panel to add a mapped configuration Dominik Csapak
2023-06-06 13:52 ` [pve-devel] [PATCH v5 08/15] ui: qemu/USBEdit: add 'mapped' device case Dominik Csapak
2023-06-06 13:52 ` [pve-devel] [PATCH v5 09/15] ui: form: add MultiPCISelector Dominik Csapak
2023-06-06 13:52 ` [pve-devel] [PATCH v5 10/15] ui: add edit window for pci mappings Dominik Csapak
2023-06-06 13:52 ` [pve-devel] [PATCH v5 11/15] ui: add edit window for usb mappings Dominik Csapak
2023-06-06 13:52 ` Dominik Csapak [this message]
2023-06-06 13:52 ` [pve-devel] [PATCH v5 13/15] ui: allow configuring pci and usb mapping Dominik Csapak
2023-06-06 13:52 ` [pve-devel] [PATCH v5 14/15] ui: window/Migrate: allow mapped devices Dominik Csapak
2023-06-06 13:52 ` [pve-devel] [PATCH v5 15/15] ui: improve permission handling for hardware Dominik Csapak
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20230606135222.984747-21-d.csapak@proxmox.com \
--to=d.csapak@proxmox.com \
--cc=pve-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox