]>
Commit | Line | Data |
---|---|---|
faee4219 TL |
1 | Ext.define('PVE.dc.BackupDiskTree', { |
2 | extend: 'Ext.tree.Panel', | |
3 | alias: 'widget.pveBackupDiskTree', | |
4 | ||
5 | folderSort: true, | |
6 | rootVisible: false, | |
7 | ||
8 | store: { | |
9 | sorters: 'id', | |
10 | data: {}, | |
11 | }, | |
12 | ||
13 | tools: [ | |
14 | { | |
15 | type: 'expand', | |
16 | tooltip: gettext('Expand All'), | |
17 | callback: panel => panel.expandAll(), | |
18 | }, | |
19 | { | |
20 | type: 'collapse', | |
21 | tooltip: gettext('Collapse All'), | |
22 | callback: panel => panel.collapseAll(), | |
23 | }, | |
24 | ], | |
25 | ||
26 | columns: [ | |
27 | { | |
28 | xtype: 'treecolumn', | |
29 | text: gettext('Guest Image'), | |
30 | renderer: function(value, meta, record) { | |
31 | if (record.data.type) { | |
32 | // guest level | |
33 | let ret = value; | |
34 | if (record.data.name) { | |
35 | ret += " (" + record.data.name + ")"; | |
36 | } | |
37 | return ret; | |
38 | } else { | |
39 | // extJS needs unique IDs but we only want to show the volumes key from "vmid:key" | |
40 | return value.split(':')[1] + " - " + record.data.name; | |
41 | } | |
42 | }, | |
43 | dataIndex: 'id', | |
44 | flex: 6, | |
45 | }, | |
46 | { | |
47 | text: gettext('Type'), | |
48 | dataIndex: 'type', | |
49 | flex: 1, | |
50 | }, | |
51 | { | |
52 | text: gettext('Backup Job'), | |
53 | renderer: PVE.Utils.render_backup_status, | |
54 | dataIndex: 'included', | |
55 | flex: 3, | |
56 | }, | |
57 | ], | |
58 | ||
59 | reload: function() { | |
60 | let me = this; | |
61 | let sm = me.getSelectionModel(); | |
62 | ||
63 | Proxmox.Utils.API2Request({ | |
64 | url: `/cluster/backup/${me.jobid}/included_volumes`, | |
65 | waitMsgTarget: me, | |
66 | method: 'GET', | |
67 | failure: function(response, opts) { | |
68 | Proxmox.Utils.setErrorMask(me, response.htmlStatus); | |
69 | }, | |
70 | success: function(response, opts) { | |
71 | sm.deselectAll(); | |
72 | me.setRootNode(response.result.data); | |
73 | me.expandAll(); | |
74 | }, | |
75 | }); | |
76 | }, | |
77 | ||
78 | initComponent: function() { | |
79 | var me = this; | |
80 | ||
81 | if (!me.jobid) { | |
82 | throw "no job id specified"; | |
83 | } | |
84 | ||
85 | var sm = Ext.create('Ext.selection.TreeModel', {}); | |
86 | ||
87 | Ext.apply(me, { | |
88 | selModel: sm, | |
89 | fields: ['id', 'type', | |
90 | { | |
91 | type: 'string', | |
92 | name: 'iconCls', | |
93 | calculate: function(data) { | |
94 | var txt = 'fa x-fa-tree fa-'; | |
95 | if (data.leaf && !data.type) { | |
96 | return txt + 'hdd-o'; | |
97 | } else if (data.type === 'qemu') { | |
98 | return txt + 'desktop'; | |
99 | } else if (data.type === 'lxc') { | |
100 | return txt + 'cube'; | |
101 | } else { | |
102 | return txt + 'question-circle'; | |
103 | } | |
104 | }, | |
105 | }, | |
106 | ], | |
107 | header: { | |
108 | items: [{ | |
109 | xtype: 'textfield', | |
110 | fieldLabel: gettext('Search'), | |
111 | labelWidth: 50, | |
112 | emptyText: 'Name, VMID, Type', | |
113 | width: 200, | |
114 | padding: '0 5 0 0', | |
115 | enableKeyEvents: true, | |
116 | listeners: { | |
117 | buffer: 500, | |
118 | keyup: function(field) { | |
119 | let searchValue = field.getValue().toLowerCase(); | |
120 | me.store.clearFilter(true); | |
121 | me.store.filterBy(function(record) { | |
122 | let data = {}; | |
123 | if (record.data.depth === 0) { | |
124 | return true; | |
125 | } else if (record.data.depth === 1) { | |
126 | data = record.data; | |
127 | } else if (record.data.depth === 2) { | |
128 | data = record.parentNode.data; | |
129 | } | |
130 | ||
131 | for (const property of ['name', 'id', 'type']) { | |
132 | if (!data[property]) { | |
133 | continue; | |
134 | } | |
135 | let v = data[property].toString(); | |
136 | if (v !== undefined) { | |
137 | v = v.toLowerCase(); | |
138 | if (v.includes(searchValue)) { | |
139 | return true; | |
140 | } | |
141 | } | |
142 | } | |
143 | return false; | |
144 | }); | |
145 | }, | |
146 | }, | |
147 | }], | |
148 | }, | |
149 | }); | |
150 | ||
151 | me.callParent(); | |
152 | ||
153 | me.reload(); | |
154 | }, | |
155 | }); | |
156 | ||
157 | Ext.define('PVE.dc.BackupInfo', { | |
158 | extend: 'Proxmox.panel.InputPanel', | |
159 | alias: 'widget.pveBackupInfo', | |
160 | ||
161 | viewModel: { | |
162 | data: { | |
163 | retentionType: 'none', | |
164 | }, | |
165 | formulas: { | |
166 | hasRetention: (get) => get('retentionType') !== 'none', | |
167 | retentionKeepAll: (get) => get('retentionType') === 'all', | |
168 | }, | |
169 | }, | |
170 | ||
171 | padding: '5 0 5 10', | |
172 | ||
173 | column1: [ | |
174 | { | |
175 | xtype: 'displayfield', | |
176 | name: 'node', | |
177 | fieldLabel: gettext('Node'), | |
178 | renderer: value => value || `-- ${gettext('All')} --`, | |
179 | }, | |
180 | { | |
181 | xtype: 'displayfield', | |
182 | name: 'storage', | |
183 | fieldLabel: gettext('Storage'), | |
184 | }, | |
185 | { | |
186 | xtype: 'displayfield', | |
20d15804 DC |
187 | name: 'schedule', |
188 | fieldLabel: gettext('Schedule'), | |
faee4219 TL |
189 | }, |
190 | { | |
191 | xtype: 'displayfield', | |
452bdb03 TL |
192 | name: 'next-run', |
193 | fieldLabel: gettext('Next Run'), | |
194 | renderer: PVE.Utils.render_next_event, | |
195 | }, | |
196 | { | |
197 | xtype: 'displayfield', | |
faee4219 TL |
198 | name: 'selMode', |
199 | fieldLabel: gettext('Selection mode'), | |
200 | }, | |
faee4219 TL |
201 | ], |
202 | column2: [ | |
203 | { | |
204 | xtype: 'displayfield', | |
3be44912 | 205 | name: 'notification-policy', |
e040ee28 | 206 | fieldLabel: gettext('Notification'), |
faee4219 | 207 | renderer: function(value) { |
3be44912 LW |
208 | let record = this.up('pveBackupInfo')?.record; |
209 | ||
210 | // Fall back to old value, in case this option is not migrated yet. | |
211 | let policy = value || record?.mailnotification || 'always'; | |
212 | ||
faee4219 | 213 | let when = gettext('Always'); |
3be44912 | 214 | if (policy === 'failure') { |
faee4219 | 215 | when = gettext('On failure only'); |
3be44912 LW |
216 | } else if (policy === 'never') { |
217 | when = gettext('Never'); | |
faee4219 | 218 | } |
3be44912 LW |
219 | |
220 | // Notification-target takes precedence | |
221 | let target = record?.['notification-target'] || | |
222 | record?.mailto || | |
223 | gettext('No target configured'); | |
224 | ||
225 | return `${when} (${target})`; | |
faee4219 TL |
226 | }, |
227 | }, | |
228 | { | |
229 | xtype: 'displayfield', | |
230 | name: 'compress', | |
231 | fieldLabel: gettext('Compression'), | |
232 | }, | |
233 | { | |
234 | xtype: 'displayfield', | |
235 | name: 'mode', | |
236 | fieldLabel: gettext('Mode'), | |
237 | renderer: function(value) { | |
3d5b3eb6 TL |
238 | const modeToDisplay = { |
239 | snapshot: gettext('Snapshot'), | |
240 | stop: gettext('Stop'), | |
241 | suspend: gettext('Snapshot'), | |
242 | }; | |
243 | return modeToDisplay[value] ?? gettext('Unknown'); | |
faee4219 TL |
244 | }, |
245 | }, | |
246 | { | |
247 | xtype: 'displayfield', | |
248 | name: 'enabled', | |
249 | fieldLabel: gettext('Enabled'), | |
250 | renderer: v => PVE.Parser.parseBoolean(v.toString()) ? gettext('Yes') : gettext('No'), | |
251 | }, | |
64f3f346 TL |
252 | { |
253 | xtype: 'displayfield', | |
254 | name: 'pool', | |
255 | fieldLabel: gettext('Pool to backup'), | |
256 | }, | |
faee4219 TL |
257 | ], |
258 | ||
259 | columnB: [ | |
d9e05d34 DC |
260 | { |
261 | xtype: 'displayfield', | |
262 | name: 'comment', | |
263 | fieldLabel: gettext('Comment'), | |
98cbec54 | 264 | renderer: Ext.String.htmlEncode, |
d9e05d34 | 265 | }, |
faee4219 | 266 | { |
9a1678c5 TL |
267 | xtype: 'fieldset', |
268 | title: gettext('Retention Configuration'), | |
faee4219 | 269 | layout: 'hbox', |
9a1678c5 | 270 | collapsible: true, |
faee4219 TL |
271 | defaults: { |
272 | border: false, | |
273 | layout: 'anchor', | |
274 | flex: 1, | |
275 | }, | |
9a1678c5 TL |
276 | bind: { |
277 | hidden: '{!hasRetention}', | |
278 | }, | |
faee4219 TL |
279 | items: [ |
280 | { | |
281 | padding: '0 10 0 0', | |
282 | defaults: { | |
283 | labelWidth: 110, | |
284 | }, | |
285 | items: [{ | |
286 | xtype: 'displayfield', | |
287 | name: 'keep-all', | |
288 | fieldLabel: gettext('Keep All'), | |
289 | renderer: Proxmox.Utils.format_boolean, | |
290 | bind: { | |
291 | hidden: '{!retentionKeepAll}', | |
292 | }, | |
293 | }].concat( | |
294 | [ | |
295 | ['keep-last', gettext('Keep Last')], | |
296 | ['keep-hourly', gettext('Keep Hourly')], | |
297 | ].map( | |
298 | name => ({ | |
299 | xtype: 'displayfield', | |
300 | name: name[0], | |
301 | fieldLabel: name[1], | |
302 | bind: { | |
303 | hidden: '{!hasRetention || retentionKeepAll}', | |
304 | }, | |
305 | }), | |
306 | ), | |
307 | ), | |
308 | }, | |
309 | { | |
310 | padding: '0 0 0 10', | |
311 | defaults: { | |
312 | labelWidth: 110, | |
313 | }, | |
314 | items: [ | |
315 | ['keep-daily', gettext('Keep Daily')], | |
316 | ['keep-weekly', gettext('Keep Weekly')], | |
317 | ].map( | |
318 | name => ({ | |
319 | xtype: 'displayfield', | |
320 | name: name[0], | |
321 | fieldLabel: name[1], | |
322 | bind: { | |
323 | hidden: '{!hasRetention || retentionKeepAll}', | |
324 | }, | |
325 | }), | |
326 | ), | |
327 | }, | |
328 | { | |
329 | padding: '0 0 0 10', | |
330 | defaults: { | |
331 | labelWidth: 110, | |
332 | }, | |
333 | items: [ | |
334 | ['keep-monthly', gettext('Keep Monthly')], | |
335 | ['keep-yearly', gettext('Keep Yearly')], | |
336 | ].map( | |
337 | name => ({ | |
338 | xtype: 'displayfield', | |
339 | name: name[0], | |
340 | fieldLabel: name[1], | |
341 | bind: { | |
342 | hidden: '{!hasRetention || retentionKeepAll}', | |
343 | }, | |
344 | }), | |
345 | ), | |
346 | }, | |
347 | ], | |
348 | }, | |
349 | ], | |
350 | ||
351 | setValues: function(values) { | |
352 | var me = this; | |
353 | let vm = me.getViewModel(); | |
354 | ||
355 | Ext.iterate(values, function(fieldId, val) { | |
356 | let field = me.query('[isFormField][name=' + fieldId + ']')[0]; | |
357 | if (field) { | |
358 | field.setValue(val); | |
359 | } | |
360 | }); | |
361 | ||
362 | if (values['prune-backups'] || values.maxfiles !== undefined) { | |
363 | let keepValues; | |
364 | if (values['prune-backups']) { | |
365 | keepValues = values['prune-backups']; | |
366 | } else if (values.maxfiles > 0) { | |
367 | keepValues = { 'keep-last': values.maxfiles }; | |
368 | } else { | |
369 | keepValues = { 'keep-all': 1 }; | |
370 | } | |
371 | ||
372 | vm.set('retentionType', keepValues['keep-all'] ? 'all' : 'other'); | |
373 | ||
374 | // set values of all keep-X fields | |
375 | ['all', 'last', 'hourly', 'daily', 'weekly', 'monthly', 'yearly'].forEach(time => { | |
376 | let name = `keep-${time}`; | |
377 | me.query(`[isFormField][name=${name}]`)[0]?.setValue(keepValues[name]); | |
378 | }); | |
379 | } else { | |
380 | vm.set('retentionType', 'none'); | |
381 | } | |
382 | ||
383 | // selection Mode depends on the presence/absence of several keys | |
384 | let selModeField = me.query('[isFormField][name=selMode]')[0]; | |
385 | let selMode = 'none'; | |
386 | if (values.vmid) { | |
387 | selMode = gettext('Include selected VMs'); | |
388 | } | |
389 | if (values.all) { | |
390 | selMode = gettext('All'); | |
391 | } | |
392 | if (values.exclude) { | |
393 | selMode = gettext('Exclude selected VMs'); | |
394 | } | |
395 | if (values.pool) { | |
396 | selMode = gettext('Pool based'); | |
397 | } | |
398 | selModeField.setValue(selMode); | |
399 | ||
400 | if (!values.pool) { | |
401 | let poolField = me.query('[isFormField][name=pool]')[0]; | |
402 | poolField.setVisible(0); | |
403 | } | |
404 | }, | |
405 | ||
406 | initComponent: function() { | |
407 | var me = this; | |
408 | ||
409 | if (!me.record) { | |
410 | throw "no data provided"; | |
411 | } | |
412 | me.callParent(); | |
413 | ||
414 | me.setValues(me.record); | |
415 | }, | |
416 | }); | |
417 | ||
418 | ||
419 | Ext.define('PVE.dc.BackedGuests', { | |
420 | extend: 'Ext.grid.GridPanel', | |
421 | alias: 'widget.pveBackedGuests', | |
422 | ||
423 | stateful: true, | |
424 | stateId: 'grid-dc-backed-guests', | |
425 | ||
426 | textfilter: '', | |
427 | ||
428 | columns: [ | |
429 | { | |
430 | header: gettext('Type'), | |
431 | dataIndex: "type", | |
432 | renderer: PVE.Utils.render_resource_type, | |
433 | flex: 1, | |
434 | sortable: true, | |
435 | }, | |
436 | { | |
437 | header: gettext('VMID'), | |
438 | dataIndex: 'vmid', | |
439 | flex: 1, | |
440 | sortable: true, | |
441 | }, | |
442 | { | |
443 | header: gettext('Name'), | |
444 | dataIndex: 'name', | |
445 | flex: 2, | |
446 | sortable: true, | |
447 | }, | |
448 | ], | |
449 | viewConfig: { | |
450 | stripeRows: true, | |
451 | trackOver: false, | |
452 | }, | |
453 | ||
454 | initComponent: function() { | |
455 | let me = this; | |
456 | ||
457 | me.store.clearFilter(true); | |
458 | ||
459 | Ext.apply(me, { | |
460 | tbar: [ | |
461 | '->', | |
462 | gettext('Search') + ':', | |
463 | ' ', | |
464 | { | |
465 | xtype: 'textfield', | |
466 | width: 200, | |
467 | emptyText: 'Name, VMID, Type', | |
468 | enableKeyEvents: true, | |
469 | listeners: { | |
470 | buffer: 500, | |
471 | keyup: function(field) { | |
472 | let searchValue = field.getValue().toLowerCase(); | |
473 | me.store.clearFilter(true); | |
474 | me.store.filterBy(function(record) { | |
475 | let data = record.data; | |
b52d2b12 | 476 | for (const property of ['name', 'vmid', 'type']) { |
faee4219 TL |
477 | if (data[property] === null) { |
478 | continue; | |
479 | } | |
480 | let v = data[property].toString(); | |
481 | if (v !== undefined) { | |
482 | if (v.toLowerCase().includes(searchValue)) { | |
483 | return true; | |
484 | } | |
485 | } | |
486 | } | |
487 | return false; | |
488 | }); | |
489 | }, | |
490 | }, | |
491 | }, | |
492 | ], | |
493 | }); | |
494 | me.callParent(); | |
495 | }, | |
496 | }); |