/* * Left Treepanel, containing all the resources we manage in this datacenter: server nodes, server storages, VMs and Containers */ Ext.define('PVE.tree.ResourceTree', { extend: 'Ext.tree.TreePanel', alias: ['widget.pveResourceTree'], userCls: 'proxmox-tags-circle', statics: { typeDefaults: { node: { iconCls: 'fa fa-building', text: gettext('Nodes'), }, pool: { iconCls: 'fa fa-tags', text: gettext('Resource Pool'), }, storage: { iconCls: 'fa fa-database', text: gettext('Storage'), }, sdn: { iconCls: 'fa fa-th', text: gettext('SDN'), }, qemu: { iconCls: 'fa fa-desktop', text: gettext('Virtual Machine'), }, lxc: { //iconCls: 'x-tree-node-lxc', iconCls: 'fa fa-cube', text: gettext('LXC Container'), }, template: { iconCls: 'fa fa-file-o', }, }, }, useArrows: true, // private nodeSortFn: function(node1, node2) { let me = this; let n1 = node1.data, n2 = node2.data; if (!n1.groupbyid === !n2.groupbyid) { let n1IsGuest = n1.type === 'qemu' || n1.type === 'lxc'; let n2IsGuest = n2.type === 'qemu' || n2.type === 'lxc'; if (me['group-guest-types'] || !n1IsGuest || !n2IsGuest) { // first sort (group) by type if (n1.type > n2.type) { return 1; } else if (n1.type < n2.type) { return -1; } } // then sort (group) by ID if (n1IsGuest) { if (me['group-templates'] && (!n1.template !== !n2.template)) { return n1.template ? 1 : -1; // sort templates after regular VMs } if (me['sort-field'] === 'vmid') { if (n1.vmid > n2.vmid) { // prefer VMID as metric for guests return 1; } else if (n1.vmid < n2.vmid) { return -1; } } else { return n1.name.localeCompare(n2.name); } } // same types but not a guest return n1.id > n2.id ? 1 : n1.id < n2.id ? -1 : 0; } else if (n1.groupbyid) { return -1; } else if (n2.groupbyid) { return 1; } return 0; // should not happen }, // private: fast binary search findInsertIndex: function(node, child, start, end) { let me = this; let diff = end - start; if (diff <= 0) { return start; } let mid = start + (diff >> 1); let res = me.nodeSortFn(child, node.childNodes[mid]); if (res <= 0) { return me.findInsertIndex(node, child, start, mid); } else { return me.findInsertIndex(node, child, mid + 1, end); } }, setIconCls: function(info) { let cls = PVE.Utils.get_object_icon_class(info.type, info); if (cls !== '') { info.iconCls = cls; } }, // add additional elements to text. Currently only the usage indicator for storages setText: function(info) { let me = this; let status = ''; if (info.type === 'storage') { let usage = info.disk / info.maxdisk; if (usage >= 0.0 && usage <= 1.0) { let barHeight = (usage * 100).toFixed(0); let remainingHeight = (100 - barHeight).toFixed(0); status = '
'; status += `
`; status += `
`; status += '
'; } } if (Ext.isNumeric(info.vmid) && info.vmid > 0) { if (PVE.UIOptions.getTreeSortingValue('sort-field') !== 'vmid') { info.text = `${info.name} (${String(info.vmid)})`; } } info.text += PVE.Utils.renderTags(info.tags, PVE.UIOptions.tagOverrides); info.text = status + info.text; }, setToolTip: function(info) { if (info.type === 'pool' || info.groupbyid !== undefined) { return; } let qtips = [gettext('Status') + ': ' + (info.qmpstatus || info.status)]; if (info.lock) { qtips.push(Ext.String.format(gettext('Config locked ({0})'), info.lock)); } if (info.hastate !== 'unmanaged') { qtips.push(gettext('HA State') + ": " + info.hastate); } info.qtip = qtips.join(', '); }, // private addChildSorted: function(node, info) { let me = this; me.setIconCls(info); me.setText(info); me.setToolTip(info); if (info.groupbyid) { info.text = info.groupbyid; if (info.type === 'type') { let defaults = PVE.tree.ResourceTree.typeDefaults[info.groupbyid]; if (defaults && defaults.text) { info.text = defaults.text; } } } let child = Ext.create('PVETree', info); if (node.childNodes) { let pos = me.findInsertIndex(node, child, 0, node.childNodes.length); node.insertBefore(child, node.childNodes[pos]); } else { node.insertBefore(child); } return child; }, // private groupChild: function(node, info, groups, level) { let me = this; let groupBy = groups[level]; let v = info[groupBy]; if (v) { let group = node.findChild('groupbyid', v); if (!group) { let groupinfo; if (info.type === groupBy) { groupinfo = info; } else { groupinfo = { type: groupBy, id: groupBy + "/" + v, }; if (groupBy !== 'type') { groupinfo[groupBy] = v; } } groupinfo.leaf = false; groupinfo.groupbyid = v; group = me.addChildSorted(node, groupinfo); } if (info.type === groupBy) { return group; } if (group) { return me.groupChild(group, info, groups, level + 1); } } return me.addChildSorted(node, info); }, saveSortingOptions: function() { let me = this; let changed = false; for (const key of ['sort-field', 'group-templates', 'group-guest-types']) { let newValue = PVE.UIOptions.getTreeSortingValue(key); if (me[key] !== newValue) { me[key] = newValue; changed = true; } } return changed; }, initComponent: function() { let me = this; me.saveSortingOptions(); let rstore = PVE.data.ResourceStore; let sp = Ext.state.Manager.getProvider(); if (!me.viewFilter) { me.viewFilter = {}; } let pdata = { dataIndex: {}, updateCount: 0, }; let store = Ext.create('Ext.data.TreeStore', { model: 'PVETree', root: { expanded: true, id: 'root', text: gettext('Datacenter'), iconCls: 'fa fa-server', }, }); let stateid = 'rid'; const changedFields = [ 'text', 'running', 'template', 'status', 'qmpstatus', 'hastate', 'lock', 'tags', ]; let updateTree = function() { store.suspendEvents(); let rootnode = me.store.getRootNode(); // remember selected node (and all parents) let sm = me.getSelectionModel(); let lastsel = sm.getSelection()[0]; let parents = []; let sorting_changed = me.saveSortingOptions(); for (let node = lastsel; node; node = node.parentNode) { parents.push(node); } let groups = me.viewFilter.groups || []; // explicitly check for node/template, as those are not always grouping attributes // also check for name for when the tree is sorted by name let moveCheckAttrs = groups.concat(['node', 'template', 'name']); let filterfn = me.viewFilter.filterfn; let reselect = false; // for disappeared nodes let index = pdata.dataIndex; // remove vanished or moved items and update changed items in-place for (const [key, olditem] of Object.entries(index)) { // getById() use find(), which is slow (ExtJS4 DP5) let item = rstore.data.get(olditem.data.id); let changed = sorting_changed, moved = sorting_changed; if (item) { // test if any grouping attributes changed, catches migrated tree-nodes in server view too for (const attr of moveCheckAttrs) { if (item.data[attr] !== olditem.data[attr]) { moved = true; break; } } // tree item has been updated for (const field of changedFields) { if (item.data[field] !== olditem.data[field]) { changed = true; break; } } // FIXME: also test filterfn()? } if (changed) { olditem.beginEdit(); let info = olditem.data; Ext.apply(info, item.data); me.setIconCls(info); me.setText(info); me.setToolTip(info); olditem.commit(); } if ((!item || moved) && olditem.isLeaf()) { delete index[key]; let parentNode = olditem.parentNode; // a selected item moved (migration) or disappeared (destroyed), so deselect that // node now and try to reselect the moved (or its parent) node later if (lastsel && olditem.data.id === lastsel.data.id) { reselect = true; sm.deselect(olditem); } // store events are suspended, so remove the item manually store.remove(olditem); parentNode.removeChild(olditem, true); } } rstore.each(function(item) { // add new items let olditem = index[item.data.id]; if (olditem) { return; } if (filterfn && !filterfn(item)) { return; } let info = Ext.apply({ leaf: true }, item.data); let child = me.groupChild(rootnode, info, groups, 0); if (child) { index[item.data.id] = child; } }); store.resumeEvents(); store.fireEvent('refresh', store); // select parent node if original selected node vanished if (lastsel && !rootnode.findChild('id', lastsel.data.id, true)) { lastsel = rootnode; for (const node of parents) { if (rootnode.findChild('id', node.data.id, true)) { lastsel = node; break; } } me.selectById(lastsel.data.id); } else if (lastsel && reselect) { me.selectById(lastsel.data.id); } // on first tree load set the selection from the stateful provider if (!pdata.updateCount) { rootnode.expand(); me.applyState(sp.get(stateid)); } pdata.updateCount++; }; sp.on('statechange', (_sp, key, value) => { if (key === stateid) { me.applyState(value); } }); Ext.apply(me, { allowSelection: true, store: store, viewConfig: { animate: false, // note: animate cause problems with applyState }, listeners: { itemcontextmenu: PVE.Utils.createCmdMenu, destroy: function() { rstore.un("load", updateTree); }, beforecellmousedown: function(tree, td, cellIndex, record, tr, rowIndex, ev) { let sm = me.getSelectionModel(); // disable selection when right clicking except if the record is already selected me.allowSelection = ev.button !== 2 || sm.isSelected(record); }, beforeselect: function(tree, record, index, eopts) { let allow = me.allowSelection; me.allowSelection = true; return allow; }, itemdblclick: PVE.Utils.openTreeConsole, }, setViewFilter: function(view) { me.viewFilter = view; me.clearTree(); updateTree(); }, setDatacenterText: function(clustername) { let rootnode = me.store.getRootNode(); let rnodeText = gettext('Datacenter'); if (clustername !== undefined) { rnodeText += ' (' + clustername + ')'; } rootnode.beginEdit(); rootnode.data.text = rnodeText; rootnode.commit(); }, clearTree: function() { pdata.updateCount = 0; let rootnode = me.store.getRootNode(); rootnode.collapse(); rootnode.removeAll(); pdata.dataIndex = {}; me.getSelectionModel().deselectAll(); }, selectExpand: function(node) { let sm = me.getSelectionModel(); if (!sm.isSelected(node)) { sm.select(node); for (let iter = node; iter; iter = iter.parentNode) { if (!iter.isExpanded()) { iter.expand(); } } me.getView().focusRow(node); } }, selectById: function(nodeid) { let rootnode = me.store.getRootNode(); let node; if (nodeid === 'root') { node = rootnode; } else { node = rootnode.findChild('id', nodeid, true); } if (node) { me.selectExpand(node); } return node; }, applyState: function(state) { if (state && state.value) { me.selectById(state.value); } else { me.getSelectionModel().deselectAll(); } }, }); me.callParent(); me.getSelectionModel().on('select', (_sm, n) => sp.set(stateid, { value: n.data.id })); rstore.on("load", updateTree); rstore.startUpdate(); }, });