]> git.proxmox.com Git - proxmox-backup.git/blame - www/datastore/Content.js
ui: datastore content: improve sorting verification column
[proxmox-backup.git] / www / datastore / Content.js
CommitLineData
33839735 1Ext.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',
6982a547 15 'fingerprint',
c6e07769 16 { name: 'size', type: 'int', allowNull: true },
e005f953 17 {
2774566b 18 name: 'crypt-mode',
676b0fde 19 type: 'boolean',
e005f953 20 calculate: function(data) {
2774566b
DC
21 let crypt = {
22 none: 0,
23 mixed: 0,
24 'sign-only': 0,
25 encrypt: 0,
106603c5 26 count: 0,
2774566b 27 };
e005f953
DC
28 data.files.forEach(file => {
29 if (file.filename === 'index.json.blob') return; // is never encrypted
2774566b
DC
30 let mode = PBS.Utils.cryptmap.indexOf(file['crypt-mode']);
31 if (mode !== -1) {
32 crypt[file['crypt-mode']]++;
1bfdae79 33 crypt.count++;
e005f953 34 }
e005f953
DC
35 });
36
106603c5 37 return PBS.Utils.calculateCryptMode(crypt);
c6e07769 38 },
6d55603d
DC
39 },
40 {
41 name: 'matchesFilter',
42 type: 'boolean',
43 defaultValue: true,
44 },
c6e07769 45 ],
ca23a97f
DM
46});
47
48Ext.define('PBS.DataStoreContent', {
e8f0ad19 49 extend: 'Ext.tree.Panel',
ca23a97f
DM
50 alias: 'widget.pbsDataStoreContent',
51
e8f0ad19 52 rootVisible: false,
507c39c5 53
c0ac2074
DC
54 title: gettext('Content'),
55
f1baa7f4
TL
56 controller: {
57 xclass: 'Ext.app.ViewController',
58
59 init: function(view) {
60 if (!view.datastore) {
61 throw "no datastore specified";
62 }
63
3f98b347 64 this.store = Ext.create('Ext.data.Store', {
33839735 65 model: 'pbs-data-store-snapshots',
e8f0ad19
DM
66 groupField: 'backup-group',
67 });
3f98b347 68 this.store.on('load', this.onLoad, this);
e8f0ad19 69
7b1e2669
DC
70 view.getStore().setSorters([
71 'backup-group',
72 'text',
c6e07769 73 'backup-time',
7b1e2669 74 ]);
90779237 75 Proxmox.Utils.monStoreErrors(view, this.store);
f1baa7f4
TL
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
f806c0ef
DC
107 var cls = PBS.Utils.get_type_icon_cls(btype);
108 if (cls === "") {
add5861e 109 console.warn(`got unknown backup-type '${btype}'`);
3f98b347
TL
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"],
c6e07769 120 children: [],
aeee4329 121 };
3f98b347 122 }
aeee4329 123
3f98b347
TL
124 return groups;
125 },
e8f0ad19 126
8866cbcc
TL
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 });
68e2ea99 140 }
8866cbcc
TL
141 });
142 } catch (err) {
143 console.debug(err);
144 }
68e2ea99
SR
145 },
146
90779237 147 onLoad: function(store, records, success, operation) {
6d55603d 148 let me = this;
3f98b347
TL
149 let view = this.getView();
150
151 if (!success) {
90779237 152 Proxmox.Utils.setErrorMask(view, Proxmox.Utils.getResponseErrorMessage(operation.getError()));
3f98b347
TL
153 return;
154 }
155
156 let groups = this.getRecordGroups(records);
e8f0ad19 157
69d970a6
DC
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 },
c6e07769 179 after: Ext.emptyFn,
69d970a6
DC
180 });
181
3f98b347
TL
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
f68ae22c 188 data.text = group + '/' + PBS.Utils.render_datetime_utc(data["backup-time"]);
3e395378 189 data.leaf = false;
3f98b347 190 data.cls = 'no-leaf-icons';
6d55603d 191 data.matchesFilter = true;
3f98b347 192
69d970a6
DC
193 data.expanded = !!expanded[data.text];
194
3e395378
DC
195 data.children = [];
196 for (const file of data.files) {
b1149ebb 197 file.text = file.filename;
3e395378 198 file['crypt-mode'] = PBS.Utils.cryptmap.indexOf(file['crypt-mode']);
6982a547 199 file.fingerprint = data.fingerprint;
3e395378 200 file.leaf = true;
6d55603d 201 file.matchesFilter = true;
3e395378
DC
202
203 data.children.push(file);
204 }
205
3f98b347
TL
206 children.push(data);
207 }
208
423df9b1 209 let nowSeconds = Date.now() / 1000;
3f98b347 210 let children = [];
69d970a6 211 for (const [name, group] of Object.entries(groups)) {
3f98b347 212 let last_backup = 0;
2774566b
DC
213 let crypt = {
214 none: 0,
215 mixed: 0,
216 'sign-only': 0,
106603c5 217 encrypt: 0,
2774566b 218 };
423df9b1
TL
219 let verify = {
220 outdated: 0,
221 none: 0,
222 failed: 0,
223 ok: 0,
224 };
225 for (let item of group.children) {
2774566b 226 crypt[PBS.Utils.cryptmap[item['crypt-mode']]]++;
48e22a89 227 if (item["backup-time"] > last_backup && item.size !== null) {
3f98b347
TL
228 last_backup = item["backup-time"];
229 group["backup-time"] = last_backup;
230 group.files = item.files;
231 group.size = item.size;
04b0ca8b 232 group.owner = item.owner;
c879e5af 233 verify.lastFailed = item.verification && item.verification.state !== 'ok';
3f98b347 234 }
423df9b1
TL
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 }
7b212c1f 248 }
3f98b347 249 }
423df9b1 250 group.verification = verify;
3f98b347 251 group.count = group.children.length;
6d55603d 252 group.matchesFilter = true;
106603c5
DC
253 crypt.count = group.count;
254 group['crypt-mode'] = PBS.Utils.calculateCryptMode(crypt);
69d970a6 255 group.expanded = !!expanded[name];
3f98b347
TL
256 children.push(group);
257 }
258
259 view.setRootNode({
260 expanded: true,
c6e07769 261 children: children,
3f98b347 262 });
69d970a6 263
ae9b5c07
DC
264 if (!children.length) {
265 view.setEmptyText(gettext('No Snapshots found'));
266 }
267
68e2ea99
SR
268 this.updateGroupNotes(view);
269
69d970a6
DC
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);
80db161e
SR
278 if (selection) {
279 view.setSelection(selection);
280 view.getView().focusRow(selection);
281 }
69d970a6
DC
282 }
283
90779237 284 Proxmox.Utils.setErrorMask(view, false);
6d55603d
DC
285 if (view.getStore().getFilters().length > 0) {
286 let searchBox = me.lookup("searchbox");
c6e07769 287 let searchvalue = searchBox.getValue();
6d55603d
DC
288 me.search(searchBox, searchvalue);
289 }
f1baa7f4 290 },
5f448992 291
61db0851
DW
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,
043406d6 305 owner: data.owner,
61db0851
DW
306 autoShow: true,
307 });
308 win.on('destroy', this.reload, this);
309 },
310
3e395378 311 onPrune: function(view, rI, cI, item, e, rec) {
c6e07769 312 view = this.getView();
5f448992 313
5f448992
DM
314 if (!(rec && rec.data)) return;
315 let data = rec.data;
3e395378 316 if (rec.parentNode.id !== 'root') return;
5f448992
DM
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();
98425309
DC
327 },
328
cd92fd73
DC
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
afbf2e10
DC
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
3e395378 375 onVerify: function(view, rI, cI, item, e, rec) {
484d439a
TL
376 let me = this;
377 view = me.getView();
8f6088c1
DM
378
379 if (!view.datastore) return;
380
8f6088c1
DM
381 if (!(rec && rec.data)) return;
382 let data = rec.data;
383
384 let params;
385
3e395378 386 if (rec.parentNode.id !== 'root') {
8f6088c1
DM
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,
484d439a 409 taskDone: () => me.reload(),
8f6088c1
DM
410 }).show();
411 },
412 });
413 },
414
68e2ea99 415 onNotesEdit: function(view, data, isGroup) {
c9725bb8
TL
416 let me = this;
417
68e2ea99
SR
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
c9725bb8
TL
435 Ext.create('PBS.window.NotesEdit', {
436 url: url,
437 autoShow: true,
438 apiCallDone: () => me.reload(), // FIXME: do something more efficient?
68e2ea99 439 extraRequestParams: params,
c9725bb8
TL
440 });
441 },
442
a904e375 443 forgetGroup: function(data) {
3e395378 444 let me = this;
a904e375 445 let view = me.getView();
4ff2c9b8 446
a904e375
DC
447 Ext.create('Proxmox.window.SafeDestroy', {
448 url: `/admin/datastore/${view.datastore}/groups`,
449 params: {
b32cf6a1
DC
450 "backup-type": data.backup_type,
451 "backup-id": data.backup_id,
a904e375
DC
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();
b32cf6a1 467
3e395378
DC
468 Ext.Msg.show({
469 title: gettext('Confirm'),
470 icon: Ext.Msg.WARNING,
a904e375 471 message: Ext.String.format(gettext('Are you sure you want to remove snapshot {0}'), `'${data.text}'`),
3e395378
DC
472 buttons: Ext.Msg.YESNO,
473 defaultFocus: 'no',
474 callback: function(btn) {
475 if (btn !== 'yes') {
476 return;
477 }
4ff2c9b8 478
3e395378 479 Proxmox.Utils.API2Request({
a904e375
DC
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 },
3e395378
DC
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 });
4ff2c9b8 493 },
4ff2c9b8
DM
494 });
495 },
496
a904e375
DC
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
3e395378 512 downloadFile: function(tV, rI, cI, item, e, rec) {
98425309
DC
513 let me = this;
514 let view = me.getView();
515
98425309 516 if (!(rec && rec.data)) return;
3e395378
DC
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;
c6e07769
TL
532 let url = new URL(`/api2/json/admin/datastore/${view.datastore}/download-decoded`,
533 window.location.origin);
3e395378
DC
534 for (const [key, value] of Object.entries(params)) {
535 url.searchParams.append(key, value);
536 }
537 atag.href = url.href;
538 atag.click();
8567c0d2
DC
539 },
540
3e395378 541 openPxarBrowser: function(tv, rI, Ci, item, e, rec) {
8567c0d2
DC
542 let me = this;
543 let view = me.getView();
544
8567c0d2 545 if (!(rec && rec.data)) return;
3e395378 546 let data = rec.parentNode.data;
8567c0d2
DC
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
3b032136 553 Ext.create('Proxmox.window.FileBrowser', {
8567c0d2 554 title: `${type}/${id}/${timetext}`,
61df02cd
TL
555 listURL: `/api2/json/admin/datastore/${view.datastore}/catalog`,
556 downloadURL: `/api2/json/admin/datastore/${view.datastore}/pxar-file-download`,
3b032136
SR
557 extraParams: {
558 'backup-id': id,
559 'backup-time': (time.getTime()/1000).toFixed(0),
560 'backup-type': type,
561 },
3e395378 562 archive: rec.data.filename,
8567c0d2 563 }).show();
6d55603d
DC
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) {
c6e07769 599 if (me.filter(item, value)) {
6d55603d
DC
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 },
f1baa7f4
TL
629 },
630
747446eb
DC
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
c879e5af
TL
642 viewConfig: {
643 getRowClass: function(record, index) {
644 let verify = record.get('verification');
645 if (verify && verify.lastFailed) {
646 return 'proxmox-invalid-row';
647 }
c6e07769 648 return null;
c879e5af
TL
649 },
650 },
651
04b0ca8b
DC
652 columns: [
653 {
654 xtype: 'treecolumn',
655 header: gettext("Backup Group"),
656 dataIndex: 'text',
c6e07769 657 flex: 1,
04b0ca8b 658 },
c9725bb8
TL
659 {
660 text: gettext('Comment'),
661 dataIndex: 'comment',
662 flex: 1,
663 renderer: (v, meta, record) => {
664 let data = record.data;
68e2ea99 665 if (!data || data.leaf) {
c9725bb8
TL
666 return '';
667 }
668 if (v === undefined || v === null) {
669 v = '';
670 }
671 v = Ext.String.htmlEncode(v);
ba2679c9 672 let icon = 'x-action-col-icon fa fa-fw fa-pencil pointer';
c9725bb8
TL
673
674 return `<span class="snapshot-comment-column">${v}</span>
ba2679c9 675 <i data-qtip="${gettext('Edit')}" style="float: right; margin: 0px;" class="${icon}"></i>`;
c9725bb8
TL
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;
68e2ea99 688 controller.onNotesEdit(view, rec.data, rec.parentNode.id === 'root');
c9725bb8
TL
689 });
690 },
691 dblclick: function(tree, el, row, col, ev, rec) {
692 let data = rec.data || {};
68e2ea99 693 if (data.leaf) {
c9725bb8
TL
694 return;
695 }
696 let view = tree.up();
697 let controller = view.controller;
68e2ea99 698 controller.onNotesEdit(view, rec.data, rec.parentNode.id === 'root');
c9725bb8
TL
699 },
700 },
701 },
3e395378
DC
702 {
703 header: gettext('Actions'),
704 xtype: 'actioncolumn',
705 dataIndex: 'text',
e373dcc5 706 width: 140,
3e395378
DC
707 items: [
708 {
709 handler: 'onVerify',
40492a56 710 getTip: (v, m, rec) => Ext.String.format(gettext("Verify '{0}'"), v),
db67e4fe 711 getClass: (v, m, rec) => rec.data.leaf ? 'pmx-hidden' : 'pve-icon-verify-lettering',
4e0faf5e 712 isActionDisabled: (v, r, c, i, rec) => !!rec.data.leaf,
61db0851
DW
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),
4e0faf5e 718 isActionDisabled: (v, r, c, i, rec) => rec.parentNode.id !=='root',
61db0851 719 },
3e395378
DC
720 {
721 handler: 'onPrune',
40492a56 722 getTip: (v, m, rec) => Ext.String.format(gettext("Prune '{0}'"), v),
3e395378 723 getClass: (v, m, rec) => rec.parentNode.id ==='root' ? 'fa fa-scissors' : 'pmx-hidden',
4e0faf5e 724 isActionDisabled: (v, r, c, i, rec) => rec.parentNode.id !=='root',
3e395378
DC
725 },
726 {
727 handler: 'onForget',
7ba99fef
DC
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),
b32cf6a1 731 getClass: (v, m, rec) => !rec.data.leaf ? 'fa critical fa-trash-o' : 'pmx-hidden',
4e0faf5e 732 isActionDisabled: (v, r, c, i, rec) => !!rec.data.leaf,
3e395378
DC
733 },
734 {
735 handler: 'downloadFile',
40492a56 736 getTip: (v, m, rec) => Ext.String.format(gettext("Download '{0}'"), v),
3e395378 737 getClass: (v, m, rec) => rec.data.leaf && rec.data.filename ? 'fa fa-download' : 'pmx-hidden',
4e0faf5e 738 isActionDisabled: (v, r, c, i, rec) => !rec.data.leaf || !rec.data.filename || rec.data['crypt-mode'] > 2,
3e395378
DC
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 },
4e0faf5e 750 isActionDisabled: (v, r, c, i, rec) => {
3e395378
DC
751 let data = rec.data;
752 return !(data.leaf &&
753 data.filename &&
754 data.filename.endsWith('pxar.didx') &&
c96b0de4 755 data['crypt-mode'] < 3);
c6e07769 756 },
3e395378 757 },
c6e07769 758 ],
3e395378 759 },
04b0ca8b
DC
760 {
761 xtype: 'datecolumn',
762 header: gettext('Backup Time'),
763 sortable: true,
764 dataIndex: 'backup-time',
765 format: 'Y-m-d H:i:s',
c6e07769 766 width: 150,
04b0ca8b
DC
767 },
768 {
769 header: gettext("Size"),
770 sortable: true,
771 dataIndex: 'size',
a7a5f56d 772 renderer: (v, meta, record) => {
3e395378
DC
773 if (record.data.text === 'client.log.blob' && v === undefined) {
774 return '';
775 }
a7a5f56d
TL
776 if (v === undefined || v === null) {
777 meta.tdCls = "x-grid-row-loading";
778 return '';
779 }
780 return Proxmox.Utils.format_size(v);
781 },
04b0ca8b
DC
782 },
783 {
784 xtype: 'numbercolumn',
785 format: '0',
786 header: gettext("Count"),
787 sortable: true,
46388e6a
TL
788 width: 75,
789 align: 'right',
04b0ca8b
DC
790 dataIndex: 'count',
791 },
792 {
793 header: gettext("Owner"),
794 sortable: true,
795 dataIndex: 'owner',
796 },
e005f953
DC
797 {
798 header: gettext('Encrypted'),
2774566b 799 dataIndex: 'crypt-mode',
3e395378 800 renderer: (v, meta, record) => {
71282dd9
SR
801 if (record.data.size === undefined || record.data.size === null) {
802 return '';
803 }
3e395378
DC
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 }
6982a547
FG
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 }
c6e07769 822 },
04b0ca8b 823 },
7b212c1f
TL
824 {
825 header: gettext('Verify State'),
826 sortable: true,
827 dataIndex: 'verification',
423df9b1 828 width: 120,
e9558f29
DC
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 },
7b212c1f 846 renderer: (v, meta, record) => {
423df9b1
TL
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'));
7b212c1f 850 }
423df9b1 851 let tip, iconCls, txt;
7b212c1f 852 if (record.parentNode.id === 'root') {
423df9b1
TL
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 }
7b212c1f
TL
895 }
896 return `<span data-qtip="${tip}">
423df9b1 897 <i class="fa fa-fw fa-${iconCls}"></i> ${txt}
7b212c1f
TL
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 },
04b0ca8b 913 ],
b1127fd0 914
04b0ca8b
DC
915 tbar: [
916 {
917 text: gettext('Reload'),
918 iconCls: 'fa fa-refresh',
919 handler: 'reload',
920 },
cd92fd73
DC
921 '-',
922 {
923 xtype: 'proxmoxButton',
924 text: gettext('Verify All'),
8d6b6a04 925 confirmMsg: gettext('Do you want to verify all snapshots now?'),
cd92fd73
DC
926 handler: 'verifyAll',
927 },
afbf2e10
DC
928 {
929 xtype: 'proxmoxButton',
930 text: gettext('Prune All'),
931 handler: 'pruneAll',
932 },
6d55603d
DC
933 '->',
934 {
935 xtype: 'tbtext',
936 html: gettext('Search'),
937 },
938 {
939 xtype: 'textfield',
940 reference: 'searchbox',
c3b1da9e 941 emptyText: gettext('group, date or owner'),
6d55603d
DC
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 },
c6e07769 951 },
6d55603d
DC
952 },
953 listeners: {
954 change: {
955 fn: 'search',
956 buffer: 500,
957 },
958 },
c6e07769 959 },
04b0ca8b 960 ],
ca23a97f 961});