]>
Commit | Line | Data |
---|---|---|
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 |
4 | Ext.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 | }); |