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