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