]> git.proxmox.com Git - proxmox-backup.git/blame - www/DataStoreContent.js
ui: datastore: mark row invalid if last snapshot verification failed
[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
DM
8 type: 'date',
9 dateFormat: 'timestamp'
10 },
d9c38ddc 11 'files',
04b0ca8b 12 'owner',
7b212c1f 13 'verification',
a7a5f56d 14 { name: 'size', type: 'int', allowNull: true, },
e005f953 15 {
2774566b 16 name: 'crypt-mode',
676b0fde 17 type: 'boolean',
e005f953
DC
18 calculate: function(data) {
19 let encrypted = 0;
2774566b
DC
20 let crypt = {
21 none: 0,
22 mixed: 0,
23 'sign-only': 0,
24 encrypt: 0,
106603c5 25 count: 0,
2774566b
DC
26 };
27 let signed = 0;
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);
e005f953 38 }
6d55603d
DC
39 },
40 {
41 name: 'matchesFilter',
42 type: 'boolean',
43 defaultValue: true,
44 },
33839735 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',
73 'backup-time'
74 ]);
90779237 75 Proxmox.Utils.monStoreErrors(view, this.store);
f1baa7f4
TL
76 this.reload(); // initial load
77 },
78
79 reload: function() {
3f98b347
TL
80 let view = this.getView();
81
82 if (!view.store || !this.store) {
83 console.warn('cannot reload, no store(s)');
84 return;
85 }
f1baa7f4 86
e8f0ad19 87 let url = `/api2/json/admin/datastore/${view.datastore}/snapshots`;
3f98b347 88 this.store.setProxy({
f1baa7f4 89 type: 'proxmox',
26f499b1 90 timeout: 300*1000, // 5 minutes, we should make that api call faster
f1baa7f4
TL
91 url: url
92 });
e8f0ad19 93
3f98b347
TL
94 this.store.load();
95 },
e8f0ad19 96
3f98b347
TL
97 getRecordGroups: function(records) {
98 let groups = {};
99
100 for (const item of records) {
101 var btype = item.data["backup-type"];
102 let group = btype + "/" + item.data["backup-id"];
103
104 if (groups[group] !== undefined) {
105 continue;
106 }
107
108 var cls = '';
109 if (btype === 'vm') {
110 cls = 'fa-desktop';
111 } else if (btype === 'ct') {
112 cls = 'fa-cube';
113 } else if (btype === 'host') {
114 cls = 'fa-building';
115 } else {
add5861e 116 console.warn(`got unknown backup-type '${btype}'`);
3f98b347
TL
117 continue; // FIXME: auto render? what do?
118 }
119
120 groups[group] = {
121 text: group,
122 leaf: false,
123 iconCls: "fa " + cls,
124 expanded: false,
125 backup_type: item.data["backup-type"],
126 backup_id: item.data["backup-id"],
127 children: []
aeee4329 128 };
3f98b347 129 }
aeee4329 130
3f98b347
TL
131 return groups;
132 },
e8f0ad19 133
90779237 134 onLoad: function(store, records, success, operation) {
6d55603d 135 let me = this;
3f98b347
TL
136 let view = this.getView();
137
138 if (!success) {
90779237 139 Proxmox.Utils.setErrorMask(view, Proxmox.Utils.getResponseErrorMessage(operation.getError()));
3f98b347
TL
140 return;
141 }
142
143 let groups = this.getRecordGroups(records);
e8f0ad19 144
69d970a6
DC
145 let selected;
146 let expanded = {};
147
148 view.getSelection().some(function(item) {
149 let id = item.data.text;
150 if (item.data.leaf) {
151 id = item.parentNode.data.text + id;
152 }
153 selected = id;
154 return true;
155 });
156
157 view.getRootNode().cascadeBy({
158 before: item => {
159 if (item.isExpanded() && !item.data.leaf) {
160 let id = item.data.text;
161 expanded[id] = true;
162 return true;
163 }
164 return false;
165 },
166 after: () => {},
167 });
168
3f98b347
TL
169 for (const item of records) {
170 let group = item.data["backup-type"] + "/" + item.data["backup-id"];
171 let children = groups[group].children;
172
173 let data = item.data;
174
f68ae22c 175 data.text = group + '/' + PBS.Utils.render_datetime_utc(data["backup-time"]);
3e395378 176 data.leaf = false;
3f98b347 177 data.cls = 'no-leaf-icons';
6d55603d 178 data.matchesFilter = true;
3f98b347 179
69d970a6
DC
180 data.expanded = !!expanded[data.text];
181
3e395378
DC
182 data.children = [];
183 for (const file of data.files) {
b1149ebb 184 file.text = file.filename;
3e395378
DC
185 file['crypt-mode'] = PBS.Utils.cryptmap.indexOf(file['crypt-mode']);
186 file.leaf = true;
6d55603d 187 file.matchesFilter = true;
3e395378
DC
188
189 data.children.push(file);
190 }
191
3f98b347
TL
192 children.push(data);
193 }
194
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 };
3f98b347 204 for (const item of group.children) {
2774566b 205 crypt[PBS.Utils.cryptmap[item['crypt-mode']]]++;
48e22a89 206 if (item["backup-time"] > last_backup && item.size !== null) {
3f98b347
TL
207 last_backup = item["backup-time"];
208 group["backup-time"] = last_backup;
209 group.files = item.files;
210 group.size = item.size;
04b0ca8b 211 group.owner = item.owner;
c879e5af 212 verify.lastFailed = item.verification && item.verification.state !== 'ok';
3f98b347 213 }
7b212c1f
TL
214 if (item.verification &&
215 (!group.verification || group.verification.state !== 'failed')) {
216 group.verification = item.verification;
217 }
e005f953 218
3f98b347
TL
219 }
220 group.count = group.children.length;
6d55603d 221 group.matchesFilter = true;
106603c5
DC
222 crypt.count = group.count;
223 group['crypt-mode'] = PBS.Utils.calculateCryptMode(crypt);
69d970a6 224 group.expanded = !!expanded[name];
3f98b347
TL
225 children.push(group);
226 }
227
228 view.setRootNode({
229 expanded: true,
230 children: children
231 });
69d970a6
DC
232
233 if (selected !== undefined) {
234 let selection = view.getRootNode().findChildBy(function(item) {
235 let id = item.data.text;
236 if (item.data.leaf) {
237 id = item.parentNode.data.text + id;
238 }
239 return selected === id;
240 }, undefined, true);
80db161e
SR
241 if (selection) {
242 view.setSelection(selection);
243 view.getView().focusRow(selection);
244 }
69d970a6
DC
245 }
246
90779237 247 Proxmox.Utils.setErrorMask(view, false);
6d55603d
DC
248 if (view.getStore().getFilters().length > 0) {
249 let searchBox = me.lookup("searchbox");
250 let searchvalue = searchBox.getValue();;
251 me.search(searchBox, searchvalue);
252 }
f1baa7f4 253 },
5f448992 254
3e395378 255 onPrune: function(view, rI, cI, item, e, rec) {
5f448992
DM
256 var view = this.getView();
257
5f448992
DM
258 if (!(rec && rec.data)) return;
259 let data = rec.data;
3e395378 260 if (rec.parentNode.id !== 'root') return;
5f448992
DM
261
262 if (!view.datastore) return;
263
264 let win = Ext.create('PBS.DataStorePrune', {
265 datastore: view.datastore,
266 backup_type: data.backup_type,
267 backup_id: data.backup_id,
268 });
269 win.on('destroy', this.reload, this);
270 win.show();
98425309
DC
271 },
272
3e395378 273 onVerify: function(view, rI, cI, item, e, rec) {
484d439a
TL
274 let me = this;
275 view = me.getView();
8f6088c1
DM
276
277 if (!view.datastore) return;
278
8f6088c1
DM
279 if (!(rec && rec.data)) return;
280 let data = rec.data;
281
282 let params;
283
3e395378 284 if (rec.parentNode.id !== 'root') {
8f6088c1
DM
285 params = {
286 "backup-type": data["backup-type"],
287 "backup-id": data["backup-id"],
288 "backup-time": (data['backup-time'].getTime()/1000).toFixed(0),
289 };
290 } else {
291 params = {
292 "backup-type": data.backup_type,
293 "backup-id": data.backup_id,
294 };
295 }
296
297 Proxmox.Utils.API2Request({
298 params: params,
299 url: `/admin/datastore/${view.datastore}/verify`,
300 method: 'POST',
301 failure: function(response) {
302 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
303 },
304 success: function(response, options) {
305 Ext.create('Proxmox.window.TaskViewer', {
306 upid: response.result.data,
484d439a 307 taskDone: () => me.reload(),
8f6088c1
DM
308 }).show();
309 },
310 });
311 },
312
3e395378
DC
313 onForget: function(view, rI, cI, item, e, rec) {
314 let me = this;
4ff2c9b8
DM
315 var view = this.getView();
316
4ff2c9b8
DM
317 if (!(rec && rec.data)) return;
318 let data = rec.data;
4ff2c9b8
DM
319 if (!view.datastore) return;
320
3e395378
DC
321 Ext.Msg.show({
322 title: gettext('Confirm'),
323 icon: Ext.Msg.WARNING,
324 message: Ext.String.format(gettext('Are you sure you want to remove snapshot {0}'), `'${data.text}'`),
325 buttons: Ext.Msg.YESNO,
326 defaultFocus: 'no',
327 callback: function(btn) {
328 if (btn !== 'yes') {
329 return;
330 }
4ff2c9b8 331
3e395378
DC
332 Proxmox.Utils.API2Request({
333 params: {
334 "backup-type": data["backup-type"],
335 "backup-id": data["backup-id"],
336 "backup-time": (data['backup-time'].getTime()/1000).toFixed(0),
337 },
338 url: `/admin/datastore/${view.datastore}/snapshots`,
339 method: 'DELETE',
340 waitMsgTarget: view,
341 failure: function(response, opts) {
342 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
343 },
344 callback: me.reload.bind(me),
345 });
4ff2c9b8 346 },
4ff2c9b8
DM
347 });
348 },
349
3e395378 350 downloadFile: function(tV, rI, cI, item, e, rec) {
98425309
DC
351 let me = this;
352 let view = me.getView();
353
98425309 354 if (!(rec && rec.data)) return;
3e395378
DC
355 let data = rec.parentNode.data;
356
357 let file = rec.data.filename;
358 let params = {
359 'backup-id': data['backup-id'],
360 'backup-type': data['backup-type'],
361 'backup-time': (data['backup-time'].getTime()/1000).toFixed(0),
362 'file-name': file,
363 };
364
365 let idx = file.lastIndexOf('.');
366 let filename = file.slice(0, idx);
367 let atag = document.createElement('a');
368 params['file-name'] = file;
369 atag.download = filename;
370 let url = new URL(`/api2/json/admin/datastore/${view.datastore}/download-decoded`, window.location.origin);
371 for (const [key, value] of Object.entries(params)) {
372 url.searchParams.append(key, value);
373 }
374 atag.href = url.href;
375 atag.click();
8567c0d2
DC
376 },
377
3e395378 378 openPxarBrowser: function(tv, rI, Ci, item, e, rec) {
8567c0d2
DC
379 let me = this;
380 let view = me.getView();
381
8567c0d2 382 if (!(rec && rec.data)) return;
3e395378 383 let data = rec.parentNode.data;
8567c0d2
DC
384
385 let id = data['backup-id'];
386 let time = data['backup-time'];
387 let type = data['backup-type'];
388 let timetext = PBS.Utils.render_datetime_utc(data["backup-time"]);
389
390 Ext.create('PBS.window.FileBrowser', {
391 title: `${type}/${id}/${timetext}`,
392 datastore: view.datastore,
393 'backup-id': id,
394 'backup-time': (time.getTime()/1000).toFixed(0),
395 'backup-type': type,
3e395378 396 archive: rec.data.filename,
8567c0d2 397 }).show();
6d55603d
DC
398 },
399
400 filter: function(item, value) {
401 if (item.data.text.indexOf(value) !== -1) {
402 return true;
403 }
404
405 if (item.data.owner && item.data.owner.indexOf(value) !== -1) {
406 return true;
407 }
408
409 return false;
410 },
411
412 search: function(tf, value) {
413 let me = this;
414 let view = me.getView();
415 let store = view.getStore();
416 if (!value && value !== 0) {
417 store.clearFilter();
418 store.getRoot().collapseChildren(true);
419 tf.triggers.clear.setVisible(false);
420 return;
421 }
422 tf.triggers.clear.setVisible(true);
423 if (value.length < 2) return;
424 Proxmox.Utils.setErrorMask(view, true);
425 // we do it a little bit later for the error mask to work
426 setTimeout(function() {
427 store.clearFilter();
428 store.getRoot().collapseChildren(true);
429
430 store.beginUpdate();
431 store.getRoot().cascadeBy({
432 before: function(item) {
433 if(me.filter(item, value)) {
434 item.set('matchesFilter', true);
435 if (item.parentNode && item.parentNode.id !== 'root') {
436 item.parentNode.childmatches = true;
437 }
438 return false;
439 }
440 return true;
441 },
442 after: function(item) {
443 if (me.filter(item, value) || item.id === 'root' || item.childmatches) {
444 item.set('matchesFilter', true);
445 if (item.parentNode && item.parentNode.id !== 'root') {
446 item.parentNode.childmatches = true;
447 }
448 if (item.childmatches) {
449 item.expand();
450 }
451 } else {
452 item.set('matchesFilter', false);
453 }
454 delete item.childmatches;
455 },
456 });
457 store.endUpdate();
458
459 store.filter((item) => !!item.get('matchesFilter'));
460 Proxmox.Utils.setErrorMask(view, false);
461 }, 10);
462 },
f1baa7f4
TL
463 },
464
c879e5af
TL
465 viewConfig: {
466 getRowClass: function(record, index) {
467 let verify = record.get('verification');
468 if (verify && verify.lastFailed) {
469 return 'proxmox-invalid-row';
470 }
471 },
472 },
473
04b0ca8b
DC
474 columns: [
475 {
476 xtype: 'treecolumn',
477 header: gettext("Backup Group"),
478 dataIndex: 'text',
479 flex: 1
480 },
3e395378
DC
481 {
482 header: gettext('Actions'),
483 xtype: 'actioncolumn',
484 dataIndex: 'text',
485 items: [
486 {
487 handler: 'onVerify',
488 tooltip: gettext('Verify'),
489 getClass: (v, m, rec) => rec.data.leaf ? 'pmx-hidden' : 'fa fa-search',
490 isDisabled: (v, r, c, i, rec) => !!rec.data.leaf,
491 },
492 {
493 handler: 'onPrune',
494 tooltip: gettext('Prune'),
495 getClass: (v, m, rec) => rec.parentNode.id ==='root' ? 'fa fa-scissors' : 'pmx-hidden',
496 isDisabled: (v, r, c, i, rec) => rec.parentNode.id !=='root',
497 },
498 {
499 handler: 'onForget',
500 tooltip: gettext('Forget Snapshot'),
501 getClass: (v, m, rec) => !rec.data.leaf && rec.parentNode.id !== 'root' ? 'fa critical fa-trash-o' : 'pmx-hidden',
502 isDisabled: (v, r, c, i, rec) => rec.data.leaf || rec.parentNode.id === 'root',
503 },
504 {
505 handler: 'downloadFile',
506 tooltip: gettext('Download'),
507 getClass: (v, m, rec) => rec.data.leaf && rec.data.filename ? 'fa fa-download' : 'pmx-hidden',
508 isDisabled: (v, r, c, i, rec) => !rec.data.leaf || !rec.data.filename || rec.data['crypt-mode'] > 2,
509 },
510 {
511 handler: 'openPxarBrowser',
512 tooltip: gettext('Browse'),
513 getClass: (v, m, rec) => {
514 let data = rec.data;
515 if (data.leaf && data.filename && data.filename.endsWith('pxar.didx')) {
516 return 'fa fa-folder-open-o';
517 }
518 return 'pmx-hidden';
519 },
520 isDisabled: (v, r, c, i, rec) => {
521 let data = rec.data;
522 return !(data.leaf &&
523 data.filename &&
524 data.filename.endsWith('pxar.didx') &&
c96b0de4 525 data['crypt-mode'] < 3);
3e395378
DC
526 }
527 },
528 ]
529 },
04b0ca8b
DC
530 {
531 xtype: 'datecolumn',
532 header: gettext('Backup Time'),
533 sortable: true,
534 dataIndex: 'backup-time',
535 format: 'Y-m-d H:i:s',
536 width: 150
537 },
538 {
539 header: gettext("Size"),
540 sortable: true,
541 dataIndex: 'size',
a7a5f56d 542 renderer: (v, meta, record) => {
3e395378
DC
543 if (record.data.text === 'client.log.blob' && v === undefined) {
544 return '';
545 }
a7a5f56d
TL
546 if (v === undefined || v === null) {
547 meta.tdCls = "x-grid-row-loading";
548 return '';
549 }
550 return Proxmox.Utils.format_size(v);
551 },
04b0ca8b
DC
552 },
553 {
554 xtype: 'numbercolumn',
555 format: '0',
556 header: gettext("Count"),
557 sortable: true,
46388e6a
TL
558 width: 75,
559 align: 'right',
04b0ca8b
DC
560 dataIndex: 'count',
561 },
562 {
563 header: gettext("Owner"),
564 sortable: true,
565 dataIndex: 'owner',
566 },
e005f953
DC
567 {
568 header: gettext('Encrypted'),
2774566b 569 dataIndex: 'crypt-mode',
3e395378 570 renderer: (v, meta, record) => {
71282dd9
SR
571 if (record.data.size === undefined || record.data.size === null) {
572 return '';
573 }
3e395378
DC
574 if (v === -1) {
575 return '';
576 }
577 let iconCls = PBS.Utils.cryptIconCls[v] || '';
578 let iconTxt = "";
579 if (iconCls) {
580 iconTxt = `<i class="fa fa-fw fa-${iconCls}"></i> `;
581 }
582 return (iconTxt + PBS.Utils.cryptText[v]) || Proxmox.Utils.unknownText
583 }
04b0ca8b 584 },
7b212c1f
TL
585 {
586 header: gettext('Verify State'),
587 sortable: true,
588 dataIndex: 'verification',
589 renderer: (v, meta, record) => {
590 if (v === undefined || v === null || !v.state) {
591 //meta.tdCls = "x-grid-row-loading";
592 return record.data.leaf ? '' : gettext('None');
593 }
594 let task = Proxmox.Utils.parse_task_upid(v.upid);
595 let verify_time = Proxmox.Utils.render_timestamp(task.starttime);
596 let iconCls = v.state === 'ok' ? 'check good' : 'times critical';
597 let tip = `Verify task started on ${verify_time}`;
598 if (record.parentNode.id === 'root') {
599 tip = v.state === 'ok'
600 ? 'All verification OK in backup group'
601 : 'At least one failed verification in backup group!';
602 }
603 return `<span data-qtip="${tip}">
604 <i class="fa fa-fw fa-${iconCls}"></i> ${v.state}
605 </span>`;
606 },
607 listeners: {
608 dblclick: function(view, el, row, col, ev, rec) {
609 let data = rec.data || {};
610 let verify = data.verification;
611 if (verify && verify.upid && rec.parentNode.id !== 'root') {
612 let win = Ext.create('Proxmox.window.TaskViewer', {
613 upid: verify.upid,
614 });
615 win.show();
616 }
617 },
618 },
619 },
04b0ca8b 620 ],
b1127fd0 621
04b0ca8b
DC
622 tbar: [
623 {
624 text: gettext('Reload'),
625 iconCls: 'fa fa-refresh',
626 handler: 'reload',
627 },
6d55603d
DC
628 '->',
629 {
630 xtype: 'tbtext',
631 html: gettext('Search'),
632 },
633 {
634 xtype: 'textfield',
635 reference: 'searchbox',
c3b1da9e 636 emptyText: gettext('group, date or owner'),
6d55603d
DC
637 triggers: {
638 clear: {
639 cls: 'pmx-clear-trigger',
640 weight: -1,
641 hidden: true,
642 handler: function() {
643 this.triggers.clear.setVisible(false);
644 this.setValue('');
645 },
646 }
647 },
648 listeners: {
649 change: {
650 fn: 'search',
651 buffer: 500,
652 },
653 },
654 }
04b0ca8b 655 ],
ca23a97f 656});