]> git.proxmox.com Git - pve-manager.git/blame - www/manager6/tree/ResourceTree.js
ui: add window for changing tree related options
[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) {
ff0777c5
TL
47 let n1 = node1.data, n2 = node2.data;
48
49 if (!n1.groupbyid === !n2.groupbyid) {
50 // first sort (group) by type
51 if (n1.type > n2.type) {
52 return 1;
53 } else if (n1.type < n2.type) {
54 return -1;
83726b9d 55 }
ff0777c5
TL
56 // then sort (group) by ID
57 if (n1.type === 'qemu' || n2.type === 'lxc') {
58 if (!n1.template !== !n2.template) {
59 return n1.template ? 1 : -1; // sort templates after regular VMs
60 }
61 if (n1.vmid > n2.vmid) { // prefer VMID as metric for guests
83726b9d 62 return 1;
ff0777c5 63 } else if (n1.vmid < n2.vmid) {
83726b9d
DM
64 return -1;
65 }
83726b9d 66 }
ff0777c5 67 // same types but not a guest
53e3ea84 68 return n1.id > n2.id ? 1 : n1.id < n2.id ? -1 : 0;
83726b9d
DM
69 } else if (n1.groupbyid) {
70 return -1;
71 } else if (n2.groupbyid) {
72 return 1;
73 }
ff0777c5 74 return 0; // should not happen
83726b9d
DM
75 },
76
77 // private: fast binary search
78 findInsertIndex: function(node, child, start, end) {
ff0777c5 79 let me = this;
83726b9d 80
ff0777c5 81 let diff = end - start;
83726b9d
DM
82 if (diff <= 0) {
83 return start;
84 }
ff0777c5 85 let mid = start + (diff >> 1);
83726b9d 86
ff0777c5 87 let res = me.nodeSortFn(child, node.childNodes[mid]);
83726b9d
DM
88 if (res <= 0) {
89 return me.findInsertIndex(node, child, start, mid);
90 } else {
91 return me.findInsertIndex(node, child, mid + 1, end);
92 }
93 },
94
95 setIconCls: function(info) {
ff0777c5 96 let cls = PVE.Utils.get_object_icon_class(info.type, info);
4dbc64a7
DC
97 if (cls !== '') {
98 info.iconCls = cls;
83726b9d
DM
99 }
100 },
101
ff0777c5 102 // add additional elements to text. Currently only the usage indicator for storages
a19652db 103 setText: function(info) {
ff0777c5 104 let me = this;
a19652db 105
ff0777c5 106 let status = '';
a19652db 107 if (info.type === 'storage') {
ff0777c5
TL
108 let usage = info.disk / info.maxdisk;
109 if (usage >= 0.0 && usage <= 1.0) {
110 let barHeight = (usage * 100).toFixed(0);
111 let remainingHeight = (100 - barHeight).toFixed(0);
a19652db 112 status = '<div class="usage-wrapper">';
ff0777c5
TL
113 status += `<div class="usage-negative" style="height: ${remainingHeight}%"></div>`;
114 status += `<div class="usage" style="height: ${barHeight}%"></div>`;
a19652db
DC
115 status += '</div> ';
116 }
117 }
118
731436ee 119 info.text += PVE.Utils.renderTags(info.tags, PVE.UIOptions.tagOverrides);
366558a7 120
a19652db
DC
121 info.text = status + info.text;
122 },
123
dca23c54
DC
124 setToolTip: function(info) {
125 if (info.type === 'pool' || info.groupbyid !== undefined) {
126 return;
127 }
128
ff0777c5 129 let qtips = [gettext('Status') + ': ' + (info.qmpstatus || info.status)];
82a33d4e 130 if (info.lock) {
ff0777c5 131 qtips.push(Ext.String.format(gettext('Config locked ({0})'), info.lock));
82a33d4e 132 }
ff0777c5 133 if (info.hastate !== 'unmanaged') {
dca23c54
DC
134 qtips.push(gettext('HA State') + ": " + info.hastate);
135 }
136
137 info.qtip = qtips.join(', ');
138 },
139
83726b9d
DM
140 // private
141 addChildSorted: function(node, info) {
ff0777c5 142 let me = this;
83726b9d
DM
143
144 me.setIconCls(info);
a19652db 145 me.setText(info);
dca23c54 146 me.setToolTip(info);
83726b9d 147
83726b9d 148 if (info.groupbyid) {
0c686cac 149 info.text = info.groupbyid;
83726b9d 150 if (info.type === 'type') {
ff0777c5 151 let defaults = PVE.tree.ResourceTree.typeDefaults[info.groupbyid];
83726b9d
DM
152 if (defaults && defaults.text) {
153 info.text = defaults.text;
154 }
155 }
156 }
ff0777c5 157 let child = Ext.create('PVETree', info);
83726b9d 158
ff0777c5
TL
159 if (node.childNodes) {
160 let pos = me.findInsertIndex(node, child, 0, node.childNodes.length);
161 node.insertBefore(child, node.childNodes[pos]);
162 } else {
163 node.insertBefore(child);
83726b9d
DM
164 }
165
83726b9d
DM
166 return child;
167 },
168
169 // private
170 groupChild: function(node, info, groups, level) {
ff0777c5 171 let me = this;
83726b9d 172
ff0777c5
TL
173 let groupBy = groups[level];
174 let v = info[groupBy];
83726b9d
DM
175
176 if (v) {
ff0777c5 177 let group = node.findChild('groupbyid', v);
83726b9d 178 if (!group) {
ff0777c5
TL
179 let groupinfo;
180 if (info.type === groupBy) {
83726b9d
DM
181 groupinfo = info;
182 } else {
183 groupinfo = {
ff0777c5
TL
184 type: groupBy,
185 id: groupBy + "/" + v,
83726b9d 186 };
ff0777c5
TL
187 if (groupBy !== 'type') {
188 groupinfo[groupBy] = v;
83726b9d
DM
189 }
190 }
191 groupinfo.leaf = false;
2a4971d8 192 groupinfo.groupbyid = v;
83726b9d 193 group = me.addChildSorted(node, groupinfo);
83726b9d 194 }
ff0777c5 195 if (info.type === groupBy) {
83726b9d
DM
196 return group;
197 }
198 if (group) {
199 return me.groupChild(group, info, groups, level + 1);
200 }
201 }
202
203 return me.addChildSorted(node, info);
204 },
205
8058410f 206 initComponent: function() {
ff0777c5 207 let me = this;
83726b9d 208
ff0777c5
TL
209 let rstore = PVE.data.ResourceStore;
210 let sp = Ext.state.Manager.getProvider();
83726b9d
DM
211
212 if (!me.viewFilter) {
213 me.viewFilter = {};
214 }
215
ff0777c5 216 let pdata = {
83726b9d 217 dataIndex: {},
f6710aac 218 updateCount: 0,
83726b9d
DM
219 };
220
ff0777c5 221 let store = Ext.create('Ext.data.TreeStore', {
83726b9d
DM
222 model: 'PVETree',
223 root: {
224 expanded: true,
225 id: 'root',
4dbc64a7 226 text: gettext('Datacenter'),
f6710aac
TL
227 iconCls: 'fa fa-server',
228 },
83726b9d
DM
229 });
230
ff0777c5 231 let stateid = 'rid';
83726b9d 232
366558a7
DC
233 const changedFields = [
234 'text', 'running', 'template', 'status', 'qmpstatus', 'hastate', 'lock', 'tags',
235 ];
236
ff0777c5 237 let updateTree = function() {
397dfdd3 238 store.suspendEvents();
83726b9d 239
ff0777c5 240 let rootnode = me.store.getRootNode();
83726b9d 241 // remember selected node (and all parents)
ff0777c5
TL
242 let sm = me.getSelectionModel();
243 let lastsel = sm.getSelection()[0];
244 let parents = [];
245 for (let node = lastsel; node; node = node.parentNode) {
246 parents.push(node);
83726b9d
DM
247 }
248
ff0777c5 249 let groups = me.viewFilter.groups || [];
d8da5538
DC
250 // explicitly check for node/template, as those are not always grouping attributes
251 let moveCheckAttrs = groups.concat(['node', 'template']);
ff0777c5
TL
252 let filterfn = me.viewFilter.filterfn;
253
254 let reselect = false; // for disappeared nodes
255 let index = pdata.dataIndex;
256 // remove vanished or moved items and update changed items in-place
257 for (const [key, olditem] of Object.entries(index)) {
258 // getById() use find(), which is slow (ExtJS4 DP5)
259 let item = rstore.data.get(olditem.data.id);
260
261 let changed = false, moved = false;
262 if (item) {
263 // test if any grouping attributes changed, catches migrated tree-nodes in server view too
d8da5538 264 for (const attr of moveCheckAttrs) {
ff0777c5 265 if (item.data[attr] !== olditem.data[attr]) {
6ae31fc1 266 moved = true;
ff0777c5 267 break;
6ae31fc1 268 }
ff0777c5 269 }
6ae31fc1 270
ff0777c5 271 // tree item has been updated
366558a7 272 for (const field of changedFields) {
ff0777c5
TL
273 if (item.data[field] !== olditem.data[field]) {
274 changed = true;
275 break;
83726b9d 276 }
83726b9d 277 }
ff0777c5
TL
278 // FIXME: also test filterfn()?
279 }
83726b9d 280
ff0777c5
TL
281 if (changed) {
282 olditem.beginEdit();
283 let info = olditem.data;
284 Ext.apply(info, item.data);
285 me.setIconCls(info);
286 me.setText(info);
287 me.setToolTip(info);
288 olditem.commit();
289 }
290 if ((!item || moved) && olditem.isLeaf()) {
291 delete index[key];
292 let parentNode = olditem.parentNode;
293 // a selected item moved (migration) or disappeared (destroyed), so deselect that
294 // node now and try to reselect the moved (or its parent) node later
295 if (lastsel && olditem.data.id === lastsel.data.id) {
296 reselect = true;
297 sm.deselect(olditem);
83726b9d 298 }
ff0777c5
TL
299 // store events are suspended, so remove the item manually
300 store.remove(olditem);
301 parentNode.removeChild(olditem, true);
83726b9d
DM
302 }
303 }
304
ff0777c5
TL
305 rstore.each(function(item) { // add new items
306 let olditem = index[item.data.id];
83726b9d
DM
307 if (olditem) {
308 return;
309 }
83726b9d
DM
310 if (filterfn && !filterfn(item)) {
311 return;
312 }
ff0777c5 313 let info = Ext.apply({ leaf: true }, item.data);
83726b9d 314
ff0777c5 315 let child = me.groupChild(rootnode, info, groups, 0);
83726b9d
DM
316 if (child) {
317 index[item.data.id] = child;
318 }
319 });
320
0f3e4bc2 321 store.resumeEvents();
cc3bcee0 322 store.fireEvent('refresh', store);
0f3e4bc2 323
ff0777c5 324 // select parent node if original selected node vanished
83726b9d
DM
325 if (lastsel && !rootnode.findChild('id', lastsel.data.id, true)) {
326 lastsel = rootnode;
ff0777c5
TL
327 for (const node of parents) {
328 if (rootnode.findChild('id', node.data.id, true)) {
329 lastsel = node;
83726b9d
DM
330 break;
331 }
332 }
333 me.selectById(lastsel.data.id);
d5a7996b
DC
334 } else if (lastsel && reselect) {
335 me.selectById(lastsel.data.id);
83726b9d
DM
336 }
337
d0e96c0d 338 // on first tree load set the selection from the stateful provider
83726b9d 339 if (!pdata.updateCount) {
83726b9d
DM
340 rootnode.expand();
341 me.applyState(sp.get(stateid));
342 }
343
344 pdata.updateCount++;
345 };
346
ff0777c5 347 sp.on('statechange', (_sp, key, value) => {
83726b9d
DM
348 if (key === stateid) {
349 me.applyState(value);
350 }
ff0777c5 351 });
83726b9d
DM
352
353 Ext.apply(me, {
91994a49 354 allowSelection: true,
83726b9d
DM
355 store: store,
356 viewConfig: {
ff0777c5 357 animate: false, // note: animate cause problems with applyState
83726b9d 358 },
83726b9d 359 listeners: {
685b7aa4 360 itemcontextmenu: PVE.Utils.createCmdMenu,
83726b9d
DM
361 destroy: function() {
362 rstore.un("load", updateTree);
91994a49 363 },
8058410f 364 beforecellmousedown: function(tree, td, cellIndex, record, tr, rowIndex, ev) {
ff0777c5
TL
365 let sm = me.getSelectionModel();
366 // disable selection when right clicking except if the record is already selected
53e3ea84 367 me.allowSelection = ev.button !== 2 || sm.isSelected(record);
91994a49 368 },
8058410f 369 beforeselect: function(tree, record, index, eopts) {
ff0777c5 370 let allow = me.allowSelection;
91994a49
DC
371 me.allowSelection = true;
372 return allow;
e3129443 373 },
f6710aac 374 itemdblclick: PVE.Utils.openTreeConsole,
83726b9d
DM
375 },
376 setViewFilter: function(view) {
377 me.viewFilter = view;
378 me.clearTree();
379 updateTree();
380 },
3d7b2aa9 381 setDatacenterText: function(clustername) {
ff0777c5 382 let rootnode = me.store.getRootNode();
3d7b2aa9 383
ff0777c5 384 let rnodeText = gettext('Datacenter');
3d7b2aa9
TL
385 if (clustername !== undefined) {
386 rnodeText += ' (' + clustername + ')';
387 }
388
389 rootnode.beginEdit();
390 rootnode.data.text = rnodeText;
391 rootnode.commit();
392 },
83726b9d
DM
393 clearTree: function() {
394 pdata.updateCount = 0;
ff0777c5 395 let rootnode = me.store.getRootNode();
83726b9d 396 rootnode.collapse();
08801a5d 397 rootnode.removeAll();
83726b9d
DM
398 pdata.dataIndex = {};
399 me.getSelectionModel().deselectAll();
400 },
401 selectExpand: function(node) {
ff0777c5 402 let sm = me.getSelectionModel();
83726b9d
DM
403 if (!sm.isSelected(node)) {
404 sm.select(node);
ff0777c5
TL
405 for (let iter = node; iter; iter = iter.parentNode) {
406 if (!iter.isExpanded()) {
407 iter.expand();
83726b9d
DM
408 }
409 }
9f950723 410 me.getView().focusRow(node);
83726b9d
DM
411 }
412 },
413 selectById: function(nodeid) {
ff0777c5
TL
414 let rootnode = me.store.getRootNode();
415 let node;
83726b9d
DM
416 if (nodeid === 'root') {
417 node = rootnode;
418 } else {
419 node = rootnode.findChild('id', nodeid, true);
420 }
421 if (node) {
422 me.selectExpand(node);
423 }
cfffc271 424 return node;
83726b9d 425 },
8058410f 426 applyState: function(state) {
83726b9d
DM
427 if (state && state.value) {
428 me.selectById(state.value);
429 } else {
ff0777c5 430 me.getSelectionModel().deselectAll();
83726b9d 431 }
f6710aac 432 },
83726b9d
DM
433 });
434
435 me.callParent();
436
ff0777c5 437 me.getSelectionModel().on('select', (_sm, n) => sp.set(stateid, { value: n.data.id }));
83726b9d
DM
438
439 rstore.on("load", updateTree);
440 rstore.startUpdate();
f6710aac 441 },
83726b9d
DM
442
443});