]>
Commit | Line | Data |
---|---|---|
5957f47f SR |
1 | Ext.define('PVE.form.CorosyncLinkEditorController', { |
2 | extend: 'Ext.app.ViewController', | |
3 | alias: 'controller.pveCorosyncLinkEditorController', | |
4 | ||
5 | addLinkIfEmpty: function() { | |
6 | let view = this.getView(); | |
95f59d0b | 7 | if (view.items || view.items.length === 0) { |
5957f47f SR |
8 | this.addLink(); |
9 | } | |
10 | }, | |
11 | ||
12 | addEmptyLink: function() { | |
95f59d0b | 13 | this.addLink(); // discard parameters to allow being called from 'handler' |
5957f47f SR |
14 | }, |
15 | ||
4ea2cac2 | 16 | addLink: function(link) { |
5957f47f SR |
17 | let me = this; |
18 | let view = me.getView(); | |
19 | let vm = view.getViewModel(); | |
20 | ||
21 | let linkCount = vm.get('linkCount'); | |
22 | if (linkCount >= vm.get('maxLinkCount')) { | |
23 | return; | |
24 | } | |
25 | ||
4ea2cac2 TL |
26 | link = link || {}; |
27 | ||
28 | if (link.number === undefined) { | |
29 | link.number = me.getNextFreeNumber(); | |
5957f47f | 30 | } |
4ea2cac2 TL |
31 | if (link.value === undefined) { |
32 | link.value = me.getNextFreeNetwork(); | |
5957f47f SR |
33 | } |
34 | ||
35 | let linkSelector = Ext.create('PVE.form.CorosyncLinkSelector', { | |
36 | maxLinkNumber: vm.get('maxLinkCount') - 1, | |
37 | allowNumberEdit: vm.get('allowNumberEdit'), | |
4ea2cac2 TL |
38 | allowBlankNetwork: link.allowBlank, |
39 | initNumber: link.number, | |
40 | initNetwork: link.value, | |
41 | text: link.text, | |
42 | emptyText: link.emptyText, | |
5957f47f SR |
43 | |
44 | // needs to be set here, because we need to update the viewmodel | |
45 | removeBtnHandler: function() { | |
46 | let curLinkCount = vm.get('linkCount'); | |
47 | ||
48 | if (curLinkCount <= 1) { | |
49 | return; | |
50 | } | |
51 | ||
52 | vm.set('linkCount', curLinkCount - 1); | |
53 | ||
54 | // 'this' is the linkSelector here | |
55 | view.remove(this); | |
56 | ||
57 | me.updateDeleteButtonState(); | |
f6710aac | 58 | }, |
5957f47f SR |
59 | }); |
60 | ||
61 | view.add(linkSelector); | |
62 | ||
63 | linkCount++; | |
64 | vm.set('linkCount', linkCount); | |
65 | ||
66 | me.updateDeleteButtonState(); | |
67 | }, | |
68 | ||
69 | // ExtJS trips on binding this for some reason, so do it manually | |
70 | updateDeleteButtonState: function() { | |
71 | let view = this.getView(); | |
72 | let vm = view.getViewModel(); | |
73 | ||
74 | let disabled = vm.get('linkCount') <= 1; | |
75 | ||
76 | let deleteButtons = view.query('button[cls=removeLinkBtn]'); | |
77 | Ext.Array.each(deleteButtons, btn => { | |
78 | btn.setDisabled(disabled); | |
8058410f | 79 | }); |
5957f47f SR |
80 | }, |
81 | ||
82 | getNextFreeNetwork: function() { | |
83 | let view = this.getView(); | |
84 | let vm = view.getViewModel(); | |
5957f47f | 85 | |
95f59d0b | 86 | let networksInUse = view.query('proxmoxNetworkSelector').map(selector => selector.value); |
5957f47f | 87 | |
95f59d0b TL |
88 | for (const network of vm.get('networks')) { |
89 | if (!networksInUse.includes(network)) { | |
90 | return network; | |
5957f47f | 91 | } |
95f59d0b TL |
92 | } |
93 | return undefined; // default to empty field, user has to set up link manually | |
5957f47f SR |
94 | }, |
95 | ||
96 | getNextFreeNumber: function() { | |
97 | let view = this.getView(); | |
98 | let vm = view.getViewModel(); | |
95f59d0b TL |
99 | |
100 | let numbersInUse = view.query('numberfield').map(field => field.value); | |
5957f47f SR |
101 | |
102 | for (let i = 0; i < vm.get('maxLinkCount'); i++) { | |
95f59d0b | 103 | if (!numbersInUse.includes(i)) { |
5957f47f SR |
104 | return i; |
105 | } | |
106 | } | |
95f59d0b | 107 | // all numbers in use, this should never happen since add button is disabled automatically |
5957f47f | 108 | return 0; |
f6710aac | 109 | }, |
5957f47f SR |
110 | }); |
111 | ||
112 | Ext.define('PVE.form.CorosyncLinkSelector', { | |
113 | extend: 'Ext.panel.Panel', | |
114 | xtype: 'pveCorosyncLinkSelector', | |
115 | ||
8058410f | 116 | mixins: ['Proxmox.Mixin.CBind'], |
5957f47f SR |
117 | cbindData: [], |
118 | ||
119 | // config | |
120 | maxLinkNumber: 7, | |
121 | allowNumberEdit: true, | |
122 | allowBlankNetwork: false, | |
123 | removeBtnHandler: undefined, | |
4ea2cac2 | 124 | emptyText: '', |
5957f47f SR |
125 | |
126 | // values | |
127 | initNumber: 0, | |
128 | initNetwork: '', | |
b1e7a7d3 | 129 | text: '', |
5957f47f SR |
130 | |
131 | layout: 'hbox', | |
132 | bodyPadding: 5, | |
133 | border: 0, | |
134 | ||
135 | items: [ | |
61b05a5d TL |
136 | { |
137 | xtype: 'displayfield', | |
138 | fieldLabel: 'Link', | |
139 | cbind: { | |
140 | hidden: '{allowNumberEdit}', | |
f6710aac | 141 | value: '{initNumber}', |
61b05a5d TL |
142 | }, |
143 | width: 45, | |
144 | labelWidth: 30, | |
145 | allowBlank: false, | |
146 | }, | |
5957f47f SR |
147 | { |
148 | xtype: 'numberfield', | |
61b05a5d | 149 | fieldLabel: 'Link', |
5957f47f SR |
150 | cbind: { |
151 | maxValue: '{maxLinkNumber}', | |
61b05a5d | 152 | hidden: '{!allowNumberEdit}', |
f6710aac | 153 | value: '{initNumber}', |
5957f47f | 154 | }, |
5957f47f SR |
155 | width: 80, |
156 | labelWidth: 30, | |
61b05a5d TL |
157 | minValue: 0, |
158 | submitValue: false, // see getSubmitValue of network selector | |
159 | allowBlank: false, | |
5957f47f SR |
160 | }, |
161 | { | |
162 | xtype: 'proxmoxNetworkSelector', | |
163 | cbind: { | |
164 | allowBlank: '{allowBlankNetwork}', | |
61b05a5d TL |
165 | value: '{initNetwork}', |
166 | emptyText: '{emptyText}', | |
5957f47f | 167 | }, |
5957f47f SR |
168 | autoSelect: false, |
169 | valueField: 'address', | |
170 | displayField: 'address', | |
61b05a5d | 171 | width: 220, |
5957f47f SR |
172 | margin: '0 5px 0 5px', |
173 | getSubmitValue: function() { | |
5957f47f | 174 | let me = this; |
95f59d0b TL |
175 | // link number is encoded into key, so we need to set field name before value retrieval |
176 | let linkNumber = me.prev('numberfield').getValue(); // always the correct one | |
5957f47f SR |
177 | me.name = 'link' + linkNumber; |
178 | return me.getValue(); | |
f6710aac | 179 | }, |
5957f47f SR |
180 | }, |
181 | { | |
182 | xtype: 'button', | |
183 | iconCls: 'fa fa-trash-o', | |
184 | cls: 'removeLinkBtn', | |
5957f47f | 185 | cbind: { |
f6710aac | 186 | hidden: '{!allowNumberEdit}', |
5957f47f | 187 | }, |
5957f47f SR |
188 | handler: function() { |
189 | let me = this; | |
190 | let parent = me.up('pveCorosyncLinkSelector'); | |
191 | if (parent.removeBtnHandler !== undefined) { | |
192 | parent.removeBtnHandler(); | |
193 | } | |
f6710aac | 194 | }, |
b1e7a7d3 SR |
195 | }, |
196 | { | |
197 | xtype: 'label', | |
198 | margin: '-1px 0 0 5px', | |
199 | ||
200 | // for muted effect | |
201 | cls: 'x-form-item-label-default', | |
202 | ||
203 | cbind: { | |
f6710aac TL |
204 | text: '{text}', |
205 | }, | |
206 | }, | |
5957f47f SR |
207 | ], |
208 | ||
209 | initComponent: function() { | |
210 | let me = this; | |
211 | ||
212 | me.callParent(); | |
213 | ||
214 | let numSelect = me.down('numberfield'); | |
215 | let netSelect = me.down('proxmoxNetworkSelector'); | |
216 | ||
95f59d0b | 217 | numSelect.validator = me.createNoDuplicatesValidator( |
5957f47f | 218 | 'numberfield', |
f6710aac | 219 | gettext("Duplicate link number not allowed."), |
5957f47f SR |
220 | ); |
221 | ||
95f59d0b | 222 | netSelect.validator = me.createNoDuplicatesValidator( |
5957f47f | 223 | 'proxmoxNetworkSelector', |
f6710aac | 224 | gettext("Duplicate link address not allowed."), |
5957f47f SR |
225 | ); |
226 | }, | |
227 | ||
95f59d0b TL |
228 | createNoDuplicatesValidator: function(queryString, errorMsg) { // linkSelector generator |
229 | let view = this; // eslint-disable-line consistent-this | |
230 | /** @this is the field itself, as the validator this is called from scopes it that way */ | |
5957f47f | 231 | return function(val) { |
95f59d0b TL |
232 | let me = this; |
233 | let form = view.up('form'); | |
234 | let linkEditor = view.up('pveCorosyncLinkEditor'); | |
5957f47f SR |
235 | |
236 | if (!form.validating) { | |
237 | // avoid recursion/double validation by setting temporary states | |
95f59d0b | 238 | me.validating = true; |
5957f47f SR |
239 | form.validating = true; |
240 | ||
241 | // validate all other fields as well, to always mark both | |
242 | // parties involved in a 'duplicate' error | |
243 | form.isValid(); | |
244 | ||
245 | form.validating = false; | |
95f59d0b TL |
246 | me.validating = false; |
247 | } else if (me.validating) { | |
248 | // we'll be validated by the original call in the other if-branch, avoid double work | |
5957f47f SR |
249 | return true; |
250 | } | |
251 | ||
95f59d0b TL |
252 | if (val === undefined || (val instanceof String && val.length === 0)) { |
253 | return true; // let this be caught by allowBlank, if at all | |
5957f47f SR |
254 | } |
255 | ||
256 | let allFields = linkEditor.query(queryString); | |
95f59d0b TL |
257 | for (const field of allFields) { |
258 | if (field !== me && String(field.getValue()) === String(val)) { | |
259 | return errorMsg; | |
5957f47f | 260 | } |
95f59d0b TL |
261 | } |
262 | return true; | |
5957f47f | 263 | }; |
f6710aac | 264 | }, |
5957f47f SR |
265 | }); |
266 | ||
267 | Ext.define('PVE.form.CorosyncLinkEditor', { | |
268 | extend: 'Ext.panel.Panel', | |
269 | xtype: 'pveCorosyncLinkEditor', | |
270 | ||
271 | controller: 'pveCorosyncLinkEditorController', | |
272 | ||
273 | // only initial config, use setter otherwise | |
274 | allowNumberEdit: true, | |
275 | ||
276 | viewModel: { | |
277 | data: { | |
278 | linkCount: 0, | |
279 | maxLinkCount: 8, | |
280 | networks: null, | |
281 | allowNumberEdit: true, | |
f6710aac | 282 | infoText: '', |
5957f47f SR |
283 | }, |
284 | formulas: { | |
285 | addDisabled: function(get) { | |
286 | return !get('allowNumberEdit') || | |
287 | get('linkCount') >= get('maxLinkCount'); | |
288 | }, | |
289 | dockHidden: function(get) { | |
290 | return !(get('allowNumberEdit') || get('infoText')); | |
f6710aac TL |
291 | }, |
292 | }, | |
5957f47f SR |
293 | }, |
294 | ||
295 | dockedItems: [{ | |
296 | xtype: 'toolbar', | |
297 | dock: 'bottom', | |
8058410f | 298 | defaultButtonUI: 'default', |
61b05a5d TL |
299 | border: false, |
300 | padding: '6 0 6 0', | |
5957f47f | 301 | bind: { |
f6710aac | 302 | hidden: '{dockHidden}', |
5957f47f SR |
303 | }, |
304 | items: [ | |
305 | { | |
306 | xtype: 'button', | |
307 | text: gettext('Add'), | |
308 | bind: { | |
309 | disabled: '{addDisabled}', | |
f6710aac | 310 | hidden: '{!allowNumberEdit}', |
5957f47f | 311 | }, |
f6710aac | 312 | handler: 'addEmptyLink', |
5957f47f SR |
313 | }, |
314 | { | |
315 | xtype: 'label', | |
316 | bind: { | |
f6710aac TL |
317 | text: '{infoText}', |
318 | }, | |
319 | }, | |
320 | ], | |
5957f47f SR |
321 | }], |
322 | ||
323 | setInfoText: function(text) { | |
324 | let me = this; | |
325 | let vm = me.getViewModel(); | |
326 | ||
327 | vm.set('infoText', text || ''); | |
328 | }, | |
329 | ||
330 | setLinks: function(links) { | |
331 | let me = this; | |
332 | let controller = me.getController(); | |
333 | let vm = me.getViewModel(); | |
334 | ||
335 | me.removeAll(); | |
336 | vm.set('linkCount', 0); | |
337 | ||
4ea2cac2 | 338 | Ext.Array.each(links, link => controller.addLink(link)); |
5957f47f SR |
339 | }, |
340 | ||
341 | setDefaultLinks: function() { | |
342 | let me = this; | |
343 | let controller = me.getController(); | |
344 | let vm = me.getViewModel(); | |
345 | ||
346 | me.removeAll(); | |
347 | vm.set('linkCount', 0); | |
348 | controller.addLink(); | |
349 | }, | |
350 | ||
351 | // clears all links | |
352 | setAllowNumberEdit: function(allow) { | |
353 | let me = this; | |
354 | let vm = me.getViewModel(); | |
355 | vm.set('allowNumberEdit', allow); | |
356 | me.removeAll(); | |
357 | vm.set('linkCount', 0); | |
358 | }, | |
359 | ||
360 | items: [{ | |
361 | // No links is never a valid scenario, but can occur during a slow load | |
362 | xtype: 'hiddenfield', | |
363 | submitValue: false, | |
364 | isValid: function() { | |
365 | let me = this; | |
366 | let vm = me.up('pveCorosyncLinkEditor').getViewModel(); | |
367 | return vm.get('linkCount') > 0; | |
f6710aac | 368 | }, |
5957f47f SR |
369 | }], |
370 | ||
371 | initComponent: function() { | |
372 | let me = this; | |
373 | let vm = me.getViewModel(); | |
374 | let controller = me.getController(); | |
375 | ||
376 | vm.set('allowNumberEdit', me.allowNumberEdit); | |
41aa53ea | 377 | vm.set('infoText', me.infoText || ''); |
5957f47f SR |
378 | |
379 | me.callParent(); | |
380 | ||
381 | // Request local node networks to pre-populate first link. | |
382 | Proxmox.Utils.API2Request({ | |
383 | url: '/nodes/localhost/network', | |
384 | method: 'GET', | |
385 | waitMsgTarget: me, | |
386 | success: response => { | |
387 | let data = response.result.data; | |
388 | if (data.length > 0) { | |
389 | data.sort((a, b) => a.iface.localeCompare(b.iface)); | |
390 | let addresses = []; | |
391 | for (let net of data) { | |
392 | if (net.address) { | |
393 | addresses.push(net.address); | |
394 | } | |
395 | if (net.address6) { | |
396 | addresses.push(net.address6); | |
397 | } | |
398 | } | |
399 | ||
400 | vm.set('networks', addresses); | |
401 | } | |
402 | ||
403 | // Always have at least one link, but account for delay in API, | |
404 | // someone might have called 'setLinks' in the meantime - | |
405 | // except if 'allowNumberEdit' is false, in which case we're | |
406 | // probably waiting for the user to input the join info | |
407 | if (vm.get('allowNumberEdit')) { | |
408 | controller.addLinkIfEmpty(); | |
409 | } | |
410 | }, | |
411 | failure: () => { | |
412 | if (vm.get('allowNumberEdit')) { | |
413 | controller.addLinkIfEmpty(); | |
414 | } | |
f6710aac | 415 | }, |
5957f47f | 416 | }); |
f6710aac | 417 | }, |
5957f47f SR |
418 | }); |
419 |