]> git.proxmox.com Git - pve-manager.git/blame - www/manager6/tree/ResourceTree.js
ui: resource tree: fix extra space inconsistency in some node elements
[pve-manager.git] / www / manager6 / tree / ResourceTree.js
CommitLineData
5289a1b8 1/*
ec505260 2 * Left Treepanel, containing all the resources we manage in this datacenter: server nodes, server storages, VMs and Containers
5289a1b8 3 */
83726b9d
DM
4Ext.define('PVE.tree.ResourceTree', {
5 extend: 'Ext.tree.TreePanel',
6 alias: ['widget.pveResourceTree'],
7
366558a7
DC
8 userCls: 'proxmox-tags-circle',
9
83726b9d
DM
10 statics: {
11 typeDefaults: {
2a4971d8 12 node: {
4dbc64a7 13 iconCls: 'fa fa-building',
f6710aac 14 text: gettext('Nodes'),
83726b9d 15 },
2a4971d8 16 pool: {
4dbc64a7 17 iconCls: 'fa fa-tags',
f6710aac 18 text: gettext('Resource Pool'),
83726b9d
DM
19 },
20 storage: {
4dbc64a7 21 iconCls: 'fa fa-database',
f6710aac 22 text: gettext('Storage'),
83726b9d 23 },
9233148b 24 sdn: {
90f7cb68 25 iconCls: 'fa fa-th',
f6710aac 26 text: gettext('SDN'),
9233148b 27 },
83726b9d 28 qemu: {
4dbc64a7 29 iconCls: 'fa fa-desktop',
f6710aac 30 text: gettext('Virtual Machine'),
83726b9d
DM
31 },
32 lxc: {
b1d8e73d 33 //iconCls: 'x-tree-node-lxc',
4dbc64a7 34 iconCls: 'fa fa-cube',
f6710aac 35 text: gettext('LXC Container'),
b1d8e73d
DC
36 },
37 template: {
f6710aac
TL
38 iconCls: 'fa fa-file-o',
39 },
40 },
83726b9d
DM
41 },
42
b1d8e73d
DC
43 useArrows: true,
44
83726b9d
DM
45 // private
46 nodeSortFn: function(node1, node2) {
8e9267f3 47 let me = this;
ff0777c5
TL
48 let n1 = node1.data, n2 = node2.data;
49
50 if (!n1.groupbyid === !n2.groupbyid) {
8e9267f3
DC
51 let n1IsGuest = n1.type === 'qemu' || n1.type === 'lxc';
52 let n2IsGuest = n2.type === 'qemu' || n2.type === 'lxc';
53 if (me['group-guest-types'] || !n1IsGuest || !n2IsGuest) {
54 // first sort (group) by type
55 if (n1.type > n2.type) {
56 return 1;
57 } else if (n1.type < n2.type) {
58 return -1;
59 }
83726b9d 60 }
8e9267f3 61
ff0777c5 62 // then sort (group) by ID
8e9267f3
DC
63 if (n1IsGuest) {
64 if (me['group-templates'] && (!n1.template !== !n2.template)) {
ff0777c5
TL
65 return n1.template ? 1 : -1; // sort templates after regular VMs
66 }
8e9267f3
DC
67 if (me['sort-field'] === 'vmid') {
68 if (n1.vmid > n2.vmid) { // prefer VMID as metric for guests
69 return 1;
70 } else if (n1.vmid < n2.vmid) {
71 return -1;
72 }
73 } else {
74 return n1.name.localeCompare(n2.name);
83726b9d 75 }
83726b9d 76 }
ff0777c5 77 // same types but not a guest
53e3ea84 78 return n1.id > n2.id ? 1 : n1.id < n2.id ? -1 : 0;
83726b9d
DM
79 } else if (n1.groupbyid) {
80 return -1;
81 } else if (n2.groupbyid) {
82 return 1;
83 }
ff0777c5 84 return 0; // should not happen
83726b9d
DM
85 },
86
87 // private: fast binary search
88 findInsertIndex: function(node, child, start, end) {
ff0777c5 89 let me = this;
83726b9d 90
ff0777c5 91 let diff = end - start;
83726b9d
DM
92 if (diff <= 0) {
93 return start;
94 }
ff0777c5 95 let mid = start + (diff >> 1);
83726b9d 96
ff0777c5 97 let res = me.nodeSortFn(child, node.childNodes[mid]);
83726b9d
DM
98 if (res <= 0) {
99 return me.findInsertIndex(node, child, start, mid);
100 } else {
101 return me.findInsertIndex(node, child, mid + 1, end);
102 }
103 },
104
105 setIconCls: function(info) {
ff0777c5 106 let cls = PVE.Utils.get_object_icon_class(info.type, info);
4dbc64a7
DC
107 if (cls !== '') {
108 info.iconCls = cls;
83726b9d
DM
109 }
110 },
111
ff0777c5 112 // add additional elements to text. Currently only the usage indicator for storages
a19652db 113 setText: function(info) {
ff0777c5 114 let me = this;
a19652db 115
ff0777c5 116 let status = '';
a19652db 117 if (info.type === 'storage') {
ff0777c5
TL
118 let usage = info.disk / info.maxdisk;
119 if (usage >= 0.0 && usage <= 1.0) {
120 let barHeight = (usage * 100).toFixed(0);
121 let remainingHeight = (100 - barHeight).toFixed(0);
a19652db 122 status = '<div class="usage-wrapper">';
ff0777c5
TL
123 status += `<div class="usage-negative" style="height: ${remainingHeight}%"></div>`;
124 status += `<div class="usage" style="height: ${barHeight}%"></div>`;
fa17629b 125 status += '</div> ';
a19652db
DC
126 }
127 }
8e9267f3
DC
128 if (Ext.isNumeric(info.vmid) && info.vmid > 0) {
129 if (PVE.UIOptions.getTreeSortingValue('sort-field') !== 'vmid') {
130 info.text = `${info.name} (${String(info.vmid)})`;
131 }
132 }
fa17629b 133 info.text = `<span>${status}${info.text}</span>`;
731436ee 134 info.text += PVE.Utils.renderTags(info.tags, PVE.UIOptions.tagOverrides);
a19652db
DC
135 },
136
2f414c50 137 getToolTip: function(info) {
dca23c54 138 if (info.type === 'pool' || info.groupbyid !== undefined) {
2f414c50 139 return undefined;
dca23c54
DC
140 }
141
ff0777c5 142 let qtips = [gettext('Status') + ': ' + (info.qmpstatus || info.status)];
82a33d4e 143 if (info.lock) {
ff0777c5 144 qtips.push(Ext.String.format(gettext('Config locked ({0})'), info.lock));
82a33d4e 145 }
ff0777c5 146 if (info.hastate !== 'unmanaged') {
dca23c54
DC
147 qtips.push(gettext('HA State') + ": " + info.hastate);
148 }
872e9978
DC
149 if (info.type === 'storage') {
150 let usage = info.disk / info.maxdisk;
151 if (usage >= 0.0 && usage <= 1.0) {
152 qtips.push(Ext.String.format(gettext("Usage: {0}%"), (usage*100).toFixed(2)));
153 }
154 }
dca23c54 155
2f414c50
DC
156 let tip = qtips.join(', ');
157 info.tip = tip;
158 return tip;
dca23c54
DC
159 },
160
83726b9d
DM
161 // private
162 addChildSorted: function(node, info) {
ff0777c5 163 let me = this;
83726b9d
DM
164
165 me.setIconCls(info);
a19652db 166 me.setText(info);
83726b9d 167
83726b9d 168 if (info.groupbyid) {
0c686cac 169 info.text = info.groupbyid;
83726b9d 170 if (info.type === 'type') {
ff0777c5 171 let defaults = PVE.tree.ResourceTree.typeDefaults[info.groupbyid];
83726b9d
DM
172 if (defaults && defaults.text) {
173 info.text = defaults.text;
174 }
175 }
176 }
ff0777c5 177 let child = Ext.create('PVETree', info);
83726b9d 178
ff0777c5
TL
179 if (node.childNodes) {
180 let pos = me.findInsertIndex(node, child, 0, node.childNodes.length);
181 node.insertBefore(child, node.childNodes[pos]);
182 } else {
183 node.insertBefore(child);
83726b9d
DM
184 }
185
83726b9d
DM
186 return child;
187 },
188
189 // private
190 groupChild: function(node, info, groups, level) {
ff0777c5 191 let me = this;
83726b9d 192
ff0777c5
TL
193 let groupBy = groups[level];
194 let v = info[groupBy];
83726b9d
DM
195
196 if (v) {
ff0777c5 197 let group = node.findChild('groupbyid', v);
83726b9d 198 if (!group) {
ff0777c5
TL
199 let groupinfo;
200 if (info.type === groupBy) {
83726b9d
DM
201 groupinfo = info;
202 } else {
203 groupinfo = {
ff0777c5
TL
204 type: groupBy,
205 id: groupBy + "/" + v,
83726b9d 206 };
ff0777c5
TL
207 if (groupBy !== 'type') {
208 groupinfo[groupBy] = v;
83726b9d
DM
209 }
210 }
211 groupinfo.leaf = false;
2a4971d8 212 groupinfo.groupbyid = v;
83726b9d 213 group = me.addChildSorted(node, groupinfo);
83726b9d 214 }
ff0777c5 215 if (info.type === groupBy) {
83726b9d
DM
216 return group;
217 }
218 if (group) {
219 return me.groupChild(group, info, groups, level + 1);
220 }
221 }
222
223 return me.addChildSorted(node, info);
224 },
225
8e9267f3
DC
226 saveSortingOptions: function() {
227 let me = this;
228 let changed = false;
229 for (const key of ['sort-field', 'group-templates', 'group-guest-types']) {
230 let newValue = PVE.UIOptions.getTreeSortingValue(key);
231 if (me[key] !== newValue) {
232 me[key] = newValue;
233 changed = true;
234 }
235 }
236 return changed;
237 },
238
8058410f 239 initComponent: function() {
ff0777c5 240 let me = this;
8e9267f3 241 me.saveSortingOptions();
83726b9d 242
ff0777c5
TL
243 let rstore = PVE.data.ResourceStore;
244 let sp = Ext.state.Manager.getProvider();
83726b9d
DM
245
246 if (!me.viewFilter) {
247 me.viewFilter = {};
248 }
249
ff0777c5 250 let pdata = {
83726b9d 251 dataIndex: {},
f6710aac 252 updateCount: 0,
83726b9d
DM
253 };
254
ff0777c5 255 let store = Ext.create('Ext.data.TreeStore', {
83726b9d
DM
256 model: 'PVETree',
257 root: {
258 expanded: true,
259 id: 'root',
4dbc64a7 260 text: gettext('Datacenter'),
f6710aac
TL
261 iconCls: 'fa fa-server',
262 },
83726b9d
DM
263 });
264
ff0777c5 265 let stateid = 'rid';
83726b9d 266
366558a7
DC
267 const changedFields = [
268 'text', 'running', 'template', 'status', 'qmpstatus', 'hastate', 'lock', 'tags',
269 ];
270
ff0777c5 271 let updateTree = function() {
397dfdd3 272 store.suspendEvents();
83726b9d 273
ff0777c5 274 let rootnode = me.store.getRootNode();
83726b9d 275 // remember selected node (and all parents)
ff0777c5
TL
276 let sm = me.getSelectionModel();
277 let lastsel = sm.getSelection()[0];
278 let parents = [];
8e9267f3 279 let sorting_changed = me.saveSortingOptions();
ff0777c5
TL
280 for (let node = lastsel; node; node = node.parentNode) {
281 parents.push(node);
83726b9d
DM
282 }
283
ff0777c5 284 let groups = me.viewFilter.groups || [];
d8da5538 285 // explicitly check for node/template, as those are not always grouping attributes
c04b8fea
DC
286 // also check for name for when the tree is sorted by name
287 let moveCheckAttrs = groups.concat(['node', 'template', 'name']);
ff0777c5
TL
288 let filterfn = me.viewFilter.filterfn;
289
290 let reselect = false; // for disappeared nodes
291 let index = pdata.dataIndex;
292 // remove vanished or moved items and update changed items in-place
293 for (const [key, olditem] of Object.entries(index)) {
294 // getById() use find(), which is slow (ExtJS4 DP5)
295 let item = rstore.data.get(olditem.data.id);
296
8e9267f3 297 let changed = sorting_changed, moved = sorting_changed;
ff0777c5
TL
298 if (item) {
299 // test if any grouping attributes changed, catches migrated tree-nodes in server view too
d8da5538 300 for (const attr of moveCheckAttrs) {
ff0777c5 301 if (item.data[attr] !== olditem.data[attr]) {
6ae31fc1 302 moved = true;
ff0777c5 303 break;
6ae31fc1 304 }
ff0777c5 305 }
6ae31fc1 306
ff0777c5 307 // tree item has been updated
366558a7 308 for (const field of changedFields) {
ff0777c5
TL
309 if (item.data[field] !== olditem.data[field]) {
310 changed = true;
311 break;
83726b9d 312 }
83726b9d 313 }
ff0777c5
TL
314 // FIXME: also test filterfn()?
315 }
83726b9d 316
ff0777c5
TL
317 if (changed) {
318 olditem.beginEdit();
319 let info = olditem.data;
320 Ext.apply(info, item.data);
321 me.setIconCls(info);
322 me.setText(info);
ff0777c5
TL
323 olditem.commit();
324 }
325 if ((!item || moved) && olditem.isLeaf()) {
326 delete index[key];
327 let parentNode = olditem.parentNode;
328 // a selected item moved (migration) or disappeared (destroyed), so deselect that
329 // node now and try to reselect the moved (or its parent) node later
330 if (lastsel && olditem.data.id === lastsel.data.id) {
331 reselect = true;
332 sm.deselect(olditem);
83726b9d 333 }
ff0777c5
TL
334 // store events are suspended, so remove the item manually
335 store.remove(olditem);
336 parentNode.removeChild(olditem, true);
83726b9d
DM
337 }
338 }
339
ff0777c5
TL
340 rstore.each(function(item) { // add new items
341 let olditem = index[item.data.id];
83726b9d
DM
342 if (olditem) {
343 return;
344 }
83726b9d
DM
345 if (filterfn && !filterfn(item)) {
346 return;
347 }
ff0777c5 348 let info = Ext.apply({ leaf: true }, item.data);
83726b9d 349
ff0777c5 350 let child = me.groupChild(rootnode, info, groups, 0);
83726b9d
DM
351 if (child) {
352 index[item.data.id] = child;
353 }
354 });
355
0f3e4bc2 356 store.resumeEvents();
cc3bcee0 357 store.fireEvent('refresh', store);
0f3e4bc2 358
ff0777c5 359 // select parent node if original selected node vanished
83726b9d
DM
360 if (lastsel && !rootnode.findChild('id', lastsel.data.id, true)) {
361 lastsel = rootnode;
ff0777c5
TL
362 for (const node of parents) {
363 if (rootnode.findChild('id', node.data.id, true)) {
364 lastsel = node;
83726b9d
DM
365 break;
366 }
367 }
368 me.selectById(lastsel.data.id);
d5a7996b
DC
369 } else if (lastsel && reselect) {
370 me.selectById(lastsel.data.id);
83726b9d
DM
371 }
372
d0e96c0d 373 // on first tree load set the selection from the stateful provider
83726b9d 374 if (!pdata.updateCount) {
83726b9d
DM
375 rootnode.expand();
376 me.applyState(sp.get(stateid));
377 }
378
379 pdata.updateCount++;
380 };
381
ff0777c5 382 sp.on('statechange', (_sp, key, value) => {
83726b9d
DM
383 if (key === stateid) {
384 me.applyState(value);
385 }
ff0777c5 386 });
83726b9d
DM
387
388 Ext.apply(me, {
91994a49 389 allowSelection: true,
83726b9d
DM
390 store: store,
391 viewConfig: {
ff0777c5 392 animate: false, // note: animate cause problems with applyState
83726b9d 393 },
83726b9d 394 listeners: {
685b7aa4 395 itemcontextmenu: PVE.Utils.createCmdMenu,
83726b9d
DM
396 destroy: function() {
397 rstore.un("load", updateTree);
91994a49 398 },
8058410f 399 beforecellmousedown: function(tree, td, cellIndex, record, tr, rowIndex, ev) {
ff0777c5
TL
400 let sm = me.getSelectionModel();
401 // disable selection when right clicking except if the record is already selected
53e3ea84 402 me.allowSelection = ev.button !== 2 || sm.isSelected(record);
91994a49 403 },
8058410f 404 beforeselect: function(tree, record, index, eopts) {
ff0777c5 405 let allow = me.allowSelection;
91994a49
DC
406 me.allowSelection = true;
407 return allow;
e3129443 408 },
f6710aac 409 itemdblclick: PVE.Utils.openTreeConsole,
2f414c50
DC
410 afterrender: function() {
411 if (me.tip) {
412 return;
413 }
414 let selectors = [
415 '.x-tree-node-text > span:not(.proxmox-tag-dark):not(.proxmox-tag-light)',
416 '.x-tree-icon',
417 ];
418 me.tip = Ext.create('Ext.tip.ToolTip', {
419 target: me.el,
420 delegate: selectors.join(', '),
421 trackMouse: true,
422 renderTo: Ext.getBody(),
423 listeners: {
424 beforeshow: function(tip) {
425 let rec = me.getView().getRecord(tip.triggerElement);
1ed8e009
DC
426 let tipText = me.getToolTip(rec.data);
427 if (tipText) {
428 tip.update(tipText);
429 return true;
430 }
431 return false;
2f414c50
DC
432 },
433 },
434 });
435 },
83726b9d
DM
436 },
437 setViewFilter: function(view) {
438 me.viewFilter = view;
439 me.clearTree();
440 updateTree();
441 },
3d7b2aa9 442 setDatacenterText: function(clustername) {
ff0777c5 443 let rootnode = me.store.getRootNode();
3d7b2aa9 444
ff0777c5 445 let rnodeText = gettext('Datacenter');
3d7b2aa9
TL
446 if (clustername !== undefined) {
447 rnodeText += ' (' + clustername + ')';
448 }
449
450 rootnode.beginEdit();
451 rootnode.data.text = rnodeText;
452 rootnode.commit();
453 },
83726b9d
DM
454 clearTree: function() {
455 pdata.updateCount = 0;
ff0777c5 456 let rootnode = me.store.getRootNode();
83726b9d 457 rootnode.collapse();
08801a5d 458 rootnode.removeAll();
83726b9d
DM
459 pdata.dataIndex = {};
460 me.getSelectionModel().deselectAll();
461 },
462 selectExpand: function(node) {
ff0777c5 463 let sm = me.getSelectionModel();
83726b9d
DM
464 if (!sm.isSelected(node)) {
465 sm.select(node);
ff0777c5
TL
466 for (let iter = node; iter; iter = iter.parentNode) {
467 if (!iter.isExpanded()) {
468 iter.expand();
83726b9d
DM
469 }
470 }
9f950723 471 me.getView().focusRow(node);
83726b9d
DM
472 }
473 },
474 selectById: function(nodeid) {
ff0777c5
TL
475 let rootnode = me.store.getRootNode();
476 let node;
83726b9d
DM
477 if (nodeid === 'root') {
478 node = rootnode;
479 } else {
480 node = rootnode.findChild('id', nodeid, true);
481 }
482 if (node) {
483 me.selectExpand(node);
484 }
cfffc271 485 return node;
83726b9d 486 },
8058410f 487 applyState: function(state) {
83726b9d
DM
488 if (state && state.value) {
489 me.selectById(state.value);
490 } else {
ff0777c5 491 me.getSelectionModel().deselectAll();
83726b9d 492 }
f6710aac 493 },
83726b9d
DM
494 });
495
496 me.callParent();
497
ff0777c5 498 me.getSelectionModel().on('select', (_sm, n) => sp.set(stateid, { value: n.data.id }));
83726b9d
DM
499
500 rstore.on("load", updateTree);
501 rstore.startUpdate();
f6710aac 502 },
83726b9d
DM
503
504});