]> git.proxmox.com Git - proxmox-backup.git/blame - www/DataStoreContent.js
api: define subscription key schema and use it
[proxmox-backup.git] / www / DataStoreContent.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',
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
47Ext.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});