]>
Commit | Line | Data |
---|---|---|
33839735 | 1 | Ext.define('pbs-data-store-snapshots', { |
ca23a97f | 2 | extend: 'Ext.data.Model', |
d9c38ddc | 3 | fields: [ |
d9c38ddc | 4 | 'backup-type', |
507c39c5 DM |
5 | 'backup-id', |
6 | { | |
e8f0ad19 | 7 | name: 'backup-time', |
507c39c5 | 8 | type: 'date', |
c6e07769 | 9 | dateFormat: 'timestamp', |
507c39c5 | 10 | }, |
c9725bb8 | 11 | 'comment', |
d9c38ddc | 12 | 'files', |
04b0ca8b | 13 | 'owner', |
7b212c1f | 14 | 'verification', |
c6e07769 | 15 | { name: 'size', type: 'int', allowNull: true }, |
e005f953 | 16 | { |
2774566b | 17 | name: 'crypt-mode', |
676b0fde | 18 | type: 'boolean', |
e005f953 | 19 | calculate: function(data) { |
2774566b DC |
20 | let crypt = { |
21 | none: 0, | |
22 | mixed: 0, | |
23 | 'sign-only': 0, | |
24 | encrypt: 0, | |
106603c5 | 25 | count: 0, |
2774566b | 26 | }; |
e005f953 DC |
27 | data.files.forEach(file => { |
28 | if (file.filename === 'index.json.blob') return; // is never encrypted | |
2774566b DC |
29 | let mode = PBS.Utils.cryptmap.indexOf(file['crypt-mode']); |
30 | if (mode !== -1) { | |
31 | crypt[file['crypt-mode']]++; | |
1bfdae79 | 32 | crypt.count++; |
e005f953 | 33 | } |
e005f953 DC |
34 | }); |
35 | ||
106603c5 | 36 | return PBS.Utils.calculateCryptMode(crypt); |
c6e07769 | 37 | }, |
6d55603d DC |
38 | }, |
39 | { | |
40 | name: 'matchesFilter', | |
41 | type: 'boolean', | |
42 | defaultValue: true, | |
43 | }, | |
c6e07769 | 44 | ], |
ca23a97f DM |
45 | }); |
46 | ||
47 | Ext.define('PBS.DataStoreContent', { | |
e8f0ad19 | 48 | extend: 'Ext.tree.Panel', |
ca23a97f DM |
49 | alias: 'widget.pbsDataStoreContent', |
50 | ||
e8f0ad19 | 51 | rootVisible: false, |
507c39c5 | 52 | |
c0ac2074 DC |
53 | title: gettext('Content'), |
54 | ||
f1baa7f4 TL |
55 | controller: { |
56 | xclass: 'Ext.app.ViewController', | |
57 | ||
58 | init: function(view) { | |
59 | if (!view.datastore) { | |
60 | throw "no datastore specified"; | |
61 | } | |
62 | ||
3f98b347 | 63 | this.store = Ext.create('Ext.data.Store', { |
33839735 | 64 | model: 'pbs-data-store-snapshots', |
e8f0ad19 DM |
65 | groupField: 'backup-group', |
66 | }); | |
3f98b347 | 67 | this.store.on('load', this.onLoad, this); |
e8f0ad19 | 68 | |
7b1e2669 DC |
69 | view.getStore().setSorters([ |
70 | 'backup-group', | |
71 | 'text', | |
c6e07769 | 72 | 'backup-time', |
7b1e2669 | 73 | ]); |
90779237 | 74 | Proxmox.Utils.monStoreErrors(view, this.store); |
f1baa7f4 TL |
75 | this.reload(); // initial load |
76 | }, | |
77 | ||
78 | reload: function() { | |
3f98b347 TL |
79 | let view = this.getView(); |
80 | ||
81 | if (!view.store || !this.store) { | |
82 | console.warn('cannot reload, no store(s)'); | |
83 | return; | |
84 | } | |
f1baa7f4 | 85 | |
e8f0ad19 | 86 | let url = `/api2/json/admin/datastore/${view.datastore}/snapshots`; |
3f98b347 | 87 | this.store.setProxy({ |
f1baa7f4 | 88 | type: 'proxmox', |
26f499b1 | 89 | timeout: 300*1000, // 5 minutes, we should make that api call faster |
c6e07769 | 90 | url: url, |
f1baa7f4 | 91 | }); |
e8f0ad19 | 92 | |
3f98b347 TL |
93 | this.store.load(); |
94 | }, | |
e8f0ad19 | 95 | |
3f98b347 TL |
96 | getRecordGroups: function(records) { |
97 | let groups = {}; | |
98 | ||
99 | for (const item of records) { | |
100 | var btype = item.data["backup-type"]; | |
101 | let group = btype + "/" + item.data["backup-id"]; | |
102 | ||
103 | if (groups[group] !== undefined) { | |
104 | continue; | |
105 | } | |
106 | ||
107 | var cls = ''; | |
108 | if (btype === 'vm') { | |
109 | cls = 'fa-desktop'; | |
110 | } else if (btype === 'ct') { | |
111 | cls = 'fa-cube'; | |
112 | } else if (btype === 'host') { | |
113 | cls = 'fa-building'; | |
114 | } else { | |
add5861e | 115 | console.warn(`got unknown backup-type '${btype}'`); |
3f98b347 TL |
116 | continue; // FIXME: auto render? what do? |
117 | } | |
118 | ||
119 | groups[group] = { | |
120 | text: group, | |
121 | leaf: false, | |
122 | iconCls: "fa " + cls, | |
123 | expanded: false, | |
124 | backup_type: item.data["backup-type"], | |
125 | backup_id: item.data["backup-id"], | |
c6e07769 | 126 | children: [], |
aeee4329 | 127 | }; |
3f98b347 | 128 | } |
aeee4329 | 129 | |
3f98b347 TL |
130 | return groups; |
131 | }, | |
e8f0ad19 | 132 | |
90779237 | 133 | onLoad: function(store, records, success, operation) { |
6d55603d | 134 | let me = this; |
3f98b347 TL |
135 | let view = this.getView(); |
136 | ||
137 | if (!success) { | |
90779237 | 138 | Proxmox.Utils.setErrorMask(view, Proxmox.Utils.getResponseErrorMessage(operation.getError())); |
3f98b347 TL |
139 | return; |
140 | } | |
141 | ||
142 | let groups = this.getRecordGroups(records); | |
e8f0ad19 | 143 | |
69d970a6 DC |
144 | let selected; |
145 | let expanded = {}; | |
146 | ||
147 | view.getSelection().some(function(item) { | |
148 | let id = item.data.text; | |
149 | if (item.data.leaf) { | |
150 | id = item.parentNode.data.text + id; | |
151 | } | |
152 | selected = id; | |
153 | return true; | |
154 | }); | |
155 | ||
156 | view.getRootNode().cascadeBy({ | |
157 | before: item => { | |
158 | if (item.isExpanded() && !item.data.leaf) { | |
159 | let id = item.data.text; | |
160 | expanded[id] = true; | |
161 | return true; | |
162 | } | |
163 | return false; | |
164 | }, | |
c6e07769 | 165 | after: Ext.emptyFn, |
69d970a6 DC |
166 | }); |
167 | ||
3f98b347 TL |
168 | for (const item of records) { |
169 | let group = item.data["backup-type"] + "/" + item.data["backup-id"]; | |
170 | let children = groups[group].children; | |
171 | ||
172 | let data = item.data; | |
173 | ||
f68ae22c | 174 | data.text = group + '/' + PBS.Utils.render_datetime_utc(data["backup-time"]); |
3e395378 | 175 | data.leaf = false; |
3f98b347 | 176 | data.cls = 'no-leaf-icons'; |
6d55603d | 177 | data.matchesFilter = true; |
3f98b347 | 178 | |
69d970a6 DC |
179 | data.expanded = !!expanded[data.text]; |
180 | ||
3e395378 DC |
181 | data.children = []; |
182 | for (const file of data.files) { | |
b1149ebb | 183 | file.text = file.filename; |
3e395378 DC |
184 | file['crypt-mode'] = PBS.Utils.cryptmap.indexOf(file['crypt-mode']); |
185 | file.leaf = true; | |
6d55603d | 186 | file.matchesFilter = true; |
3e395378 DC |
187 | |
188 | data.children.push(file); | |
189 | } | |
190 | ||
3f98b347 TL |
191 | children.push(data); |
192 | } | |
193 | ||
423df9b1 | 194 | let nowSeconds = Date.now() / 1000; |
3f98b347 | 195 | let children = []; |
69d970a6 | 196 | for (const [name, group] of Object.entries(groups)) { |
3f98b347 | 197 | let last_backup = 0; |
2774566b DC |
198 | let crypt = { |
199 | none: 0, | |
200 | mixed: 0, | |
201 | 'sign-only': 0, | |
106603c5 | 202 | encrypt: 0, |
2774566b | 203 | }; |
423df9b1 TL |
204 | let verify = { |
205 | outdated: 0, | |
206 | none: 0, | |
207 | failed: 0, | |
208 | ok: 0, | |
209 | }; | |
210 | for (let item of group.children) { | |
2774566b | 211 | crypt[PBS.Utils.cryptmap[item['crypt-mode']]]++; |
48e22a89 | 212 | if (item["backup-time"] > last_backup && item.size !== null) { |
3f98b347 TL |
213 | last_backup = item["backup-time"]; |
214 | group["backup-time"] = last_backup; | |
215 | group.files = item.files; | |
216 | group.size = item.size; | |
04b0ca8b | 217 | group.owner = item.owner; |
c879e5af | 218 | verify.lastFailed = item.verification && item.verification.state !== 'ok'; |
3f98b347 | 219 | } |
423df9b1 TL |
220 | if (!item.verification) { |
221 | verify.none++; | |
222 | } else { | |
223 | if (item.verification.state === 'ok') { | |
224 | verify.ok++; | |
225 | } else { | |
226 | verify.failed++; | |
227 | } | |
228 | let task = Proxmox.Utils.parse_task_upid(item.verification.upid); | |
229 | item.verification.lastTime = task.starttime; | |
230 | if (nowSeconds - task.starttime > 30 * 24 * 60 * 60) { | |
231 | verify.outdated++; | |
232 | } | |
7b212c1f | 233 | } |
3f98b347 | 234 | } |
423df9b1 | 235 | group.verification = verify; |
3f98b347 | 236 | group.count = group.children.length; |
6d55603d | 237 | group.matchesFilter = true; |
106603c5 DC |
238 | crypt.count = group.count; |
239 | group['crypt-mode'] = PBS.Utils.calculateCryptMode(crypt); | |
69d970a6 | 240 | group.expanded = !!expanded[name]; |
3f98b347 TL |
241 | children.push(group); |
242 | } | |
243 | ||
244 | view.setRootNode({ | |
245 | expanded: true, | |
c6e07769 | 246 | children: children, |
3f98b347 | 247 | }); |
69d970a6 DC |
248 | |
249 | if (selected !== undefined) { | |
250 | let selection = view.getRootNode().findChildBy(function(item) { | |
251 | let id = item.data.text; | |
252 | if (item.data.leaf) { | |
253 | id = item.parentNode.data.text + id; | |
254 | } | |
255 | return selected === id; | |
256 | }, undefined, true); | |
80db161e SR |
257 | if (selection) { |
258 | view.setSelection(selection); | |
259 | view.getView().focusRow(selection); | |
260 | } | |
69d970a6 DC |
261 | } |
262 | ||
90779237 | 263 | Proxmox.Utils.setErrorMask(view, false); |
6d55603d DC |
264 | if (view.getStore().getFilters().length > 0) { |
265 | let searchBox = me.lookup("searchbox"); | |
c6e07769 | 266 | let searchvalue = searchBox.getValue(); |
6d55603d DC |
267 | me.search(searchBox, searchvalue); |
268 | } | |
f1baa7f4 | 269 | }, |
5f448992 | 270 | |
3e395378 | 271 | onPrune: function(view, rI, cI, item, e, rec) { |
c6e07769 | 272 | view = this.getView(); |
5f448992 | 273 | |
5f448992 DM |
274 | if (!(rec && rec.data)) return; |
275 | let data = rec.data; | |
3e395378 | 276 | if (rec.parentNode.id !== 'root') return; |
5f448992 DM |
277 | |
278 | if (!view.datastore) return; | |
279 | ||
280 | let win = Ext.create('PBS.DataStorePrune', { | |
281 | datastore: view.datastore, | |
282 | backup_type: data.backup_type, | |
283 | backup_id: data.backup_id, | |
284 | }); | |
285 | win.on('destroy', this.reload, this); | |
286 | win.show(); | |
98425309 DC |
287 | }, |
288 | ||
cd92fd73 DC |
289 | verifyAll: function() { |
290 | var view = this.getView(); | |
291 | ||
292 | Proxmox.Utils.API2Request({ | |
293 | url: `/admin/datastore/${view.datastore}/verify`, | |
294 | method: 'POST', | |
295 | failure: function(response) { | |
296 | Ext.Msg.alert(gettext('Error'), response.htmlStatus); | |
297 | }, | |
298 | success: function(response, options) { | |
299 | Ext.create('Proxmox.window.TaskViewer', { | |
300 | upid: response.result.data, | |
301 | }).show(); | |
302 | }, | |
303 | }); | |
304 | }, | |
305 | ||
3e395378 | 306 | onVerify: function(view, rI, cI, item, e, rec) { |
484d439a TL |
307 | let me = this; |
308 | view = me.getView(); | |
8f6088c1 DM |
309 | |
310 | if (!view.datastore) return; | |
311 | ||
8f6088c1 DM |
312 | if (!(rec && rec.data)) return; |
313 | let data = rec.data; | |
314 | ||
315 | let params; | |
316 | ||
3e395378 | 317 | if (rec.parentNode.id !== 'root') { |
8f6088c1 DM |
318 | params = { |
319 | "backup-type": data["backup-type"], | |
320 | "backup-id": data["backup-id"], | |
321 | "backup-time": (data['backup-time'].getTime()/1000).toFixed(0), | |
322 | }; | |
323 | } else { | |
324 | params = { | |
325 | "backup-type": data.backup_type, | |
326 | "backup-id": data.backup_id, | |
327 | }; | |
328 | } | |
329 | ||
330 | Proxmox.Utils.API2Request({ | |
331 | params: params, | |
332 | url: `/admin/datastore/${view.datastore}/verify`, | |
333 | method: 'POST', | |
334 | failure: function(response) { | |
335 | Ext.Msg.alert(gettext('Error'), response.htmlStatus); | |
336 | }, | |
337 | success: function(response, options) { | |
338 | Ext.create('Proxmox.window.TaskViewer', { | |
339 | upid: response.result.data, | |
484d439a | 340 | taskDone: () => me.reload(), |
8f6088c1 DM |
341 | }).show(); |
342 | }, | |
343 | }); | |
344 | }, | |
345 | ||
c9725bb8 TL |
346 | onNotesEdit: function(view, data) { |
347 | let me = this; | |
348 | ||
349 | let url = `/admin/datastore/${view.datastore}/notes`; | |
350 | Ext.create('PBS.window.NotesEdit', { | |
351 | url: url, | |
352 | autoShow: true, | |
353 | apiCallDone: () => me.reload(), // FIXME: do something more efficient? | |
354 | extraRequestParams: { | |
355 | "backup-type": data["backup-type"], | |
356 | "backup-id": data["backup-id"], | |
357 | "backup-time": (data['backup-time'].getTime()/1000).toFixed(0), | |
358 | }, | |
359 | }); | |
360 | }, | |
361 | ||
3e395378 DC |
362 | onForget: function(view, rI, cI, item, e, rec) { |
363 | let me = this; | |
c6e07769 | 364 | view = this.getView(); |
4ff2c9b8 | 365 | |
4ff2c9b8 DM |
366 | if (!(rec && rec.data)) return; |
367 | let data = rec.data; | |
4ff2c9b8 DM |
368 | if (!view.datastore) return; |
369 | ||
3e395378 DC |
370 | Ext.Msg.show({ |
371 | title: gettext('Confirm'), | |
372 | icon: Ext.Msg.WARNING, | |
373 | message: Ext.String.format(gettext('Are you sure you want to remove snapshot {0}'), `'${data.text}'`), | |
374 | buttons: Ext.Msg.YESNO, | |
375 | defaultFocus: 'no', | |
376 | callback: function(btn) { | |
377 | if (btn !== 'yes') { | |
378 | return; | |
379 | } | |
4ff2c9b8 | 380 | |
3e395378 DC |
381 | Proxmox.Utils.API2Request({ |
382 | params: { | |
383 | "backup-type": data["backup-type"], | |
384 | "backup-id": data["backup-id"], | |
385 | "backup-time": (data['backup-time'].getTime()/1000).toFixed(0), | |
386 | }, | |
387 | url: `/admin/datastore/${view.datastore}/snapshots`, | |
388 | method: 'DELETE', | |
389 | waitMsgTarget: view, | |
390 | failure: function(response, opts) { | |
391 | Ext.Msg.alert(gettext('Error'), response.htmlStatus); | |
392 | }, | |
393 | callback: me.reload.bind(me), | |
394 | }); | |
4ff2c9b8 | 395 | }, |
4ff2c9b8 DM |
396 | }); |
397 | }, | |
398 | ||
3e395378 | 399 | downloadFile: function(tV, rI, cI, item, e, rec) { |
98425309 DC |
400 | let me = this; |
401 | let view = me.getView(); | |
402 | ||
98425309 | 403 | if (!(rec && rec.data)) return; |
3e395378 DC |
404 | let data = rec.parentNode.data; |
405 | ||
406 | let file = rec.data.filename; | |
407 | let params = { | |
408 | 'backup-id': data['backup-id'], | |
409 | 'backup-type': data['backup-type'], | |
410 | 'backup-time': (data['backup-time'].getTime()/1000).toFixed(0), | |
411 | 'file-name': file, | |
412 | }; | |
413 | ||
414 | let idx = file.lastIndexOf('.'); | |
415 | let filename = file.slice(0, idx); | |
416 | let atag = document.createElement('a'); | |
417 | params['file-name'] = file; | |
418 | atag.download = filename; | |
c6e07769 TL |
419 | let url = new URL(`/api2/json/admin/datastore/${view.datastore}/download-decoded`, |
420 | window.location.origin); | |
3e395378 DC |
421 | for (const [key, value] of Object.entries(params)) { |
422 | url.searchParams.append(key, value); | |
423 | } | |
424 | atag.href = url.href; | |
425 | atag.click(); | |
8567c0d2 DC |
426 | }, |
427 | ||
3e395378 | 428 | openPxarBrowser: function(tv, rI, Ci, item, e, rec) { |
8567c0d2 DC |
429 | let me = this; |
430 | let view = me.getView(); | |
431 | ||
8567c0d2 | 432 | if (!(rec && rec.data)) return; |
3e395378 | 433 | let data = rec.parentNode.data; |
8567c0d2 DC |
434 | |
435 | let id = data['backup-id']; | |
436 | let time = data['backup-time']; | |
437 | let type = data['backup-type']; | |
438 | let timetext = PBS.Utils.render_datetime_utc(data["backup-time"]); | |
439 | ||
440 | Ext.create('PBS.window.FileBrowser', { | |
441 | title: `${type}/${id}/${timetext}`, | |
442 | datastore: view.datastore, | |
443 | 'backup-id': id, | |
444 | 'backup-time': (time.getTime()/1000).toFixed(0), | |
445 | 'backup-type': type, | |
3e395378 | 446 | archive: rec.data.filename, |
8567c0d2 | 447 | }).show(); |
6d55603d DC |
448 | }, |
449 | ||
450 | filter: function(item, value) { | |
451 | if (item.data.text.indexOf(value) !== -1) { | |
452 | return true; | |
453 | } | |
454 | ||
455 | if (item.data.owner && item.data.owner.indexOf(value) !== -1) { | |
456 | return true; | |
457 | } | |
458 | ||
459 | return false; | |
460 | }, | |
461 | ||
462 | search: function(tf, value) { | |
463 | let me = this; | |
464 | let view = me.getView(); | |
465 | let store = view.getStore(); | |
466 | if (!value && value !== 0) { | |
467 | store.clearFilter(); | |
468 | store.getRoot().collapseChildren(true); | |
469 | tf.triggers.clear.setVisible(false); | |
470 | return; | |
471 | } | |
472 | tf.triggers.clear.setVisible(true); | |
473 | if (value.length < 2) return; | |
474 | Proxmox.Utils.setErrorMask(view, true); | |
475 | // we do it a little bit later for the error mask to work | |
476 | setTimeout(function() { | |
477 | store.clearFilter(); | |
478 | store.getRoot().collapseChildren(true); | |
479 | ||
480 | store.beginUpdate(); | |
481 | store.getRoot().cascadeBy({ | |
482 | before: function(item) { | |
c6e07769 | 483 | if (me.filter(item, value)) { |
6d55603d DC |
484 | item.set('matchesFilter', true); |
485 | if (item.parentNode && item.parentNode.id !== 'root') { | |
486 | item.parentNode.childmatches = true; | |
487 | } | |
488 | return false; | |
489 | } | |
490 | return true; | |
491 | }, | |
492 | after: function(item) { | |
493 | if (me.filter(item, value) || item.id === 'root' || item.childmatches) { | |
494 | item.set('matchesFilter', true); | |
495 | if (item.parentNode && item.parentNode.id !== 'root') { | |
496 | item.parentNode.childmatches = true; | |
497 | } | |
498 | if (item.childmatches) { | |
499 | item.expand(); | |
500 | } | |
501 | } else { | |
502 | item.set('matchesFilter', false); | |
503 | } | |
504 | delete item.childmatches; | |
505 | }, | |
506 | }); | |
507 | store.endUpdate(); | |
508 | ||
509 | store.filter((item) => !!item.get('matchesFilter')); | |
510 | Proxmox.Utils.setErrorMask(view, false); | |
511 | }, 10); | |
512 | }, | |
f1baa7f4 TL |
513 | }, |
514 | ||
c879e5af TL |
515 | viewConfig: { |
516 | getRowClass: function(record, index) { | |
517 | let verify = record.get('verification'); | |
518 | if (verify && verify.lastFailed) { | |
519 | return 'proxmox-invalid-row'; | |
520 | } | |
c6e07769 | 521 | return null; |
c879e5af TL |
522 | }, |
523 | }, | |
524 | ||
04b0ca8b DC |
525 | columns: [ |
526 | { | |
527 | xtype: 'treecolumn', | |
528 | header: gettext("Backup Group"), | |
529 | dataIndex: 'text', | |
c6e07769 | 530 | flex: 1, |
04b0ca8b | 531 | }, |
c9725bb8 TL |
532 | { |
533 | text: gettext('Comment'), | |
534 | dataIndex: 'comment', | |
535 | flex: 1, | |
536 | renderer: (v, meta, record) => { | |
537 | let data = record.data; | |
538 | if (!data || data.leaf || record.parentNode.id === 'root') { | |
539 | return ''; | |
540 | } | |
541 | if (v === undefined || v === null) { | |
542 | v = ''; | |
543 | } | |
544 | v = Ext.String.htmlEncode(v); | |
01f37e01 | 545 | let icon = 'fa fa-fw fa-pencil pointer'; |
c9725bb8 TL |
546 | |
547 | return `<span class="snapshot-comment-column">${v}</span> | |
548 | <i data-qtip="${gettext('Edit')}" style="float: right;" class="${icon}"></i>`; | |
549 | }, | |
550 | listeners: { | |
551 | afterrender: function(component) { | |
552 | // a bit of a hack, but relatively easy, cheap and works out well. | |
553 | // more efficient to use one handler for the whole column than for each icon | |
554 | component.on('click', function(tree, cell, rowI, colI, e, rec) { | |
555 | let el = e.target; | |
556 | if (el.tagName !== "I" || !el.classList.contains("fa-pencil")) { | |
557 | return; | |
558 | } | |
559 | let view = tree.up(); | |
560 | let controller = view.controller; | |
561 | controller.onNotesEdit(view, rec.data); | |
562 | }); | |
563 | }, | |
564 | dblclick: function(tree, el, row, col, ev, rec) { | |
565 | let data = rec.data || {}; | |
566 | if (data.leaf || rec.parentNode.id === 'root') { | |
567 | return; | |
568 | } | |
569 | let view = tree.up(); | |
570 | let controller = view.controller; | |
571 | controller.onNotesEdit(view, rec.data); | |
572 | }, | |
573 | }, | |
574 | }, | |
3e395378 DC |
575 | { |
576 | header: gettext('Actions'), | |
577 | xtype: 'actioncolumn', | |
578 | dataIndex: 'text', | |
579 | items: [ | |
580 | { | |
581 | handler: 'onVerify', | |
40492a56 | 582 | getTip: (v, m, rec) => Ext.String.format(gettext("Verify '{0}'"), v), |
db67e4fe | 583 | getClass: (v, m, rec) => rec.data.leaf ? 'pmx-hidden' : 'pve-icon-verify-lettering', |
3e395378 DC |
584 | isDisabled: (v, r, c, i, rec) => !!rec.data.leaf, |
585 | }, | |
586 | { | |
587 | handler: 'onPrune', | |
40492a56 | 588 | getTip: (v, m, rec) => Ext.String.format(gettext("Prune '{0}'"), v), |
3e395378 DC |
589 | getClass: (v, m, rec) => rec.parentNode.id ==='root' ? 'fa fa-scissors' : 'pmx-hidden', |
590 | isDisabled: (v, r, c, i, rec) => rec.parentNode.id !=='root', | |
591 | }, | |
592 | { | |
593 | handler: 'onForget', | |
40492a56 | 594 | getTip: (v, m, rec) => Ext.String.format(gettext("Permanently forget snapshot '{0}'"), v), |
3e395378 DC |
595 | getClass: (v, m, rec) => !rec.data.leaf && rec.parentNode.id !== 'root' ? 'fa critical fa-trash-o' : 'pmx-hidden', |
596 | isDisabled: (v, r, c, i, rec) => rec.data.leaf || rec.parentNode.id === 'root', | |
597 | }, | |
598 | { | |
599 | handler: 'downloadFile', | |
40492a56 | 600 | getTip: (v, m, rec) => Ext.String.format(gettext("Download '{0}'"), v), |
3e395378 DC |
601 | getClass: (v, m, rec) => rec.data.leaf && rec.data.filename ? 'fa fa-download' : 'pmx-hidden', |
602 | isDisabled: (v, r, c, i, rec) => !rec.data.leaf || !rec.data.filename || rec.data['crypt-mode'] > 2, | |
603 | }, | |
604 | { | |
605 | handler: 'openPxarBrowser', | |
606 | tooltip: gettext('Browse'), | |
607 | getClass: (v, m, rec) => { | |
608 | let data = rec.data; | |
609 | if (data.leaf && data.filename && data.filename.endsWith('pxar.didx')) { | |
610 | return 'fa fa-folder-open-o'; | |
611 | } | |
612 | return 'pmx-hidden'; | |
613 | }, | |
614 | isDisabled: (v, r, c, i, rec) => { | |
615 | let data = rec.data; | |
616 | return !(data.leaf && | |
617 | data.filename && | |
618 | data.filename.endsWith('pxar.didx') && | |
c96b0de4 | 619 | data['crypt-mode'] < 3); |
c6e07769 | 620 | }, |
3e395378 | 621 | }, |
c6e07769 | 622 | ], |
3e395378 | 623 | }, |
04b0ca8b DC |
624 | { |
625 | xtype: 'datecolumn', | |
626 | header: gettext('Backup Time'), | |
627 | sortable: true, | |
628 | dataIndex: 'backup-time', | |
629 | format: 'Y-m-d H:i:s', | |
c6e07769 | 630 | width: 150, |
04b0ca8b DC |
631 | }, |
632 | { | |
633 | header: gettext("Size"), | |
634 | sortable: true, | |
635 | dataIndex: 'size', | |
a7a5f56d | 636 | renderer: (v, meta, record) => { |
3e395378 DC |
637 | if (record.data.text === 'client.log.blob' && v === undefined) { |
638 | return ''; | |
639 | } | |
a7a5f56d TL |
640 | if (v === undefined || v === null) { |
641 | meta.tdCls = "x-grid-row-loading"; | |
642 | return ''; | |
643 | } | |
644 | return Proxmox.Utils.format_size(v); | |
645 | }, | |
04b0ca8b DC |
646 | }, |
647 | { | |
648 | xtype: 'numbercolumn', | |
649 | format: '0', | |
650 | header: gettext("Count"), | |
651 | sortable: true, | |
46388e6a TL |
652 | width: 75, |
653 | align: 'right', | |
04b0ca8b DC |
654 | dataIndex: 'count', |
655 | }, | |
656 | { | |
657 | header: gettext("Owner"), | |
658 | sortable: true, | |
659 | dataIndex: 'owner', | |
660 | }, | |
e005f953 DC |
661 | { |
662 | header: gettext('Encrypted'), | |
2774566b | 663 | dataIndex: 'crypt-mode', |
3e395378 | 664 | renderer: (v, meta, record) => { |
71282dd9 SR |
665 | if (record.data.size === undefined || record.data.size === null) { |
666 | return ''; | |
667 | } | |
3e395378 DC |
668 | if (v === -1) { |
669 | return ''; | |
670 | } | |
671 | let iconCls = PBS.Utils.cryptIconCls[v] || ''; | |
672 | let iconTxt = ""; | |
673 | if (iconCls) { | |
674 | iconTxt = `<i class="fa fa-fw fa-${iconCls}"></i> `; | |
675 | } | |
c6e07769 TL |
676 | return (iconTxt + PBS.Utils.cryptText[v]) || Proxmox.Utils.unknownText; |
677 | }, | |
04b0ca8b | 678 | }, |
7b212c1f TL |
679 | { |
680 | header: gettext('Verify State'), | |
681 | sortable: true, | |
682 | dataIndex: 'verification', | |
423df9b1 | 683 | width: 120, |
7b212c1f | 684 | renderer: (v, meta, record) => { |
423df9b1 TL |
685 | let i = (cls, txt) => `<i class="fa fa-fw fa-${cls}"></i> ${txt}`; |
686 | if (v === undefined || v === null) { | |
687 | return record.data.leaf ? '' : i('question-circle-o warning', gettext('None')); | |
7b212c1f | 688 | } |
423df9b1 | 689 | let tip, iconCls, txt; |
7b212c1f | 690 | if (record.parentNode.id === 'root') { |
423df9b1 TL |
691 | if (v.failed === 0) { |
692 | if (v.none === 0) { | |
693 | if (v.outdated > 0) { | |
694 | tip = 'All OK, but some snapshots were not verified in last 30 days'; | |
695 | iconCls = 'check warning'; | |
696 | txt = gettext('All OK (old)'); | |
697 | } else { | |
698 | tip = 'All snapshots verified at least once in last 30 days'; | |
699 | iconCls = 'check good'; | |
700 | txt = gettext('All OK'); | |
701 | } | |
702 | } else if (v.ok === 0) { | |
703 | tip = `${v.none} not verified yet`; | |
704 | iconCls = 'question-circle-o warning'; | |
705 | txt = gettext('None'); | |
706 | } else { | |
707 | tip = `${v.ok} OK, ${v.none} not verified yet`; | |
708 | iconCls = 'check faded'; | |
709 | txt = `${v.ok} OK`; | |
710 | } | |
711 | } else { | |
712 | tip = `${v.ok} OK, ${v.failed} failed, ${v.none} not verified yet`; | |
713 | iconCls = 'times critical'; | |
714 | txt = v.ok === 0 && v.none === 0 | |
715 | ? gettext('All failed') | |
716 | : `${v.failed} failed`; | |
717 | } | |
718 | } else if (!v.state) { | |
719 | return record.data.leaf ? '' : gettext('None'); | |
720 | } else { | |
721 | let verify_time = Proxmox.Utils.render_timestamp(v.lastTime); | |
722 | tip = `Last verify task started on ${verify_time}`; | |
723 | txt = v.state; | |
724 | iconCls = 'times critical'; | |
725 | if (v.state === 'ok') { | |
726 | iconCls = 'check good'; | |
727 | let now = Date.now() / 1000; | |
728 | if (now - v.lastTime > 30 * 24 * 60 * 60) { | |
729 | tip = `Last verify task over 30 days ago: ${verify_time}`; | |
730 | iconCls = 'check warning'; | |
731 | } | |
732 | } | |
7b212c1f TL |
733 | } |
734 | return `<span data-qtip="${tip}"> | |
423df9b1 | 735 | <i class="fa fa-fw fa-${iconCls}"></i> ${txt} |
7b212c1f TL |
736 | </span>`; |
737 | }, | |
738 | listeners: { | |
739 | dblclick: function(view, el, row, col, ev, rec) { | |
740 | let data = rec.data || {}; | |
741 | let verify = data.verification; | |
742 | if (verify && verify.upid && rec.parentNode.id !== 'root') { | |
743 | let win = Ext.create('Proxmox.window.TaskViewer', { | |
744 | upid: verify.upid, | |
745 | }); | |
746 | win.show(); | |
747 | } | |
748 | }, | |
749 | }, | |
750 | }, | |
04b0ca8b | 751 | ], |
b1127fd0 | 752 | |
04b0ca8b DC |
753 | tbar: [ |
754 | { | |
755 | text: gettext('Reload'), | |
756 | iconCls: 'fa fa-refresh', | |
757 | handler: 'reload', | |
758 | }, | |
cd92fd73 DC |
759 | '-', |
760 | { | |
761 | xtype: 'proxmoxButton', | |
762 | text: gettext('Verify All'), | |
8d6b6a04 | 763 | confirmMsg: gettext('Do you want to verify all snapshots now?'), |
cd92fd73 DC |
764 | handler: 'verifyAll', |
765 | }, | |
6d55603d DC |
766 | '->', |
767 | { | |
768 | xtype: 'tbtext', | |
769 | html: gettext('Search'), | |
770 | }, | |
771 | { | |
772 | xtype: 'textfield', | |
773 | reference: 'searchbox', | |
c3b1da9e | 774 | emptyText: gettext('group, date or owner'), |
6d55603d DC |
775 | triggers: { |
776 | clear: { | |
777 | cls: 'pmx-clear-trigger', | |
778 | weight: -1, | |
779 | hidden: true, | |
780 | handler: function() { | |
781 | this.triggers.clear.setVisible(false); | |
782 | this.setValue(''); | |
783 | }, | |
c6e07769 | 784 | }, |
6d55603d DC |
785 | }, |
786 | listeners: { | |
787 | change: { | |
788 | fn: 'search', | |
789 | buffer: 500, | |
790 | }, | |
791 | }, | |
c6e07769 | 792 | }, |
04b0ca8b | 793 | ], |
ca23a97f | 794 | }); |