]> git.proxmox.com Git - proxmox-backup.git/blob - www/DataStoreContent.js
ui: datastore: mark row invalid if last snapshot verification failed
[proxmox-backup.git] / www / DataStoreContent.js
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 'files',
12 'owner',
13 'verification',
14 { name: 'size', type: 'int', allowNull: true, },
15 {
16 name: 'crypt-mode',
17 type: 'boolean',
18 calculate: function(data) {
19 let encrypted = 0;
20 let crypt = {
21 none: 0,
22 mixed: 0,
23 'sign-only': 0,
24 encrypt: 0,
25 count: 0,
26 };
27 let signed = 0;
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 this.reload(); // initial load
77 },
78
79 reload: function() {
80 let view = this.getView();
81
82 if (!view.store || !this.store) {
83 console.warn('cannot reload, no store(s)');
84 return;
85 }
86
87 let url = `/api2/json/admin/datastore/${view.datastore}/snapshots`;
88 this.store.setProxy({
89 type: 'proxmox',
90 timeout: 300*1000, // 5 minutes, we should make that api call faster
91 url: url
92 });
93
94 this.store.load();
95 },
96
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 {
116 console.warn(`got unknown backup-type '${btype}'`);
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: []
128 };
129 }
130
131 return groups;
132 },
133
134 onLoad: function(store, records, success, operation) {
135 let me = this;
136 let view = this.getView();
137
138 if (!success) {
139 Proxmox.Utils.setErrorMask(view, Proxmox.Utils.getResponseErrorMessage(operation.getError()));
140 return;
141 }
142
143 let groups = this.getRecordGroups(records);
144
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
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
175 data.text = group + '/' + PBS.Utils.render_datetime_utc(data["backup-time"]);
176 data.leaf = false;
177 data.cls = 'no-leaf-icons';
178 data.matchesFilter = true;
179
180 data.expanded = !!expanded[data.text];
181
182 data.children = [];
183 for (const file of data.files) {
184 file.text = file.filename;
185 file['crypt-mode'] = PBS.Utils.cryptmap.indexOf(file['crypt-mode']);
186 file.leaf = true;
187 file.matchesFilter = true;
188
189 data.children.push(file);
190 }
191
192 children.push(data);
193 }
194
195 let children = [];
196 for (const [name, group] of Object.entries(groups)) {
197 let last_backup = 0;
198 let crypt = {
199 none: 0,
200 mixed: 0,
201 'sign-only': 0,
202 encrypt: 0,
203 };
204 for (const item of group.children) {
205 crypt[PBS.Utils.cryptmap[item['crypt-mode']]]++;
206 if (item["backup-time"] > last_backup && item.size !== null) {
207 last_backup = item["backup-time"];
208 group["backup-time"] = last_backup;
209 group.files = item.files;
210 group.size = item.size;
211 group.owner = item.owner;
212 verify.lastFailed = item.verification && item.verification.state !== 'ok';
213 }
214 if (item.verification &&
215 (!group.verification || group.verification.state !== 'failed')) {
216 group.verification = item.verification;
217 }
218
219 }
220 group.count = group.children.length;
221 group.matchesFilter = true;
222 crypt.count = group.count;
223 group['crypt-mode'] = PBS.Utils.calculateCryptMode(crypt);
224 group.expanded = !!expanded[name];
225 children.push(group);
226 }
227
228 view.setRootNode({
229 expanded: true,
230 children: children
231 });
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);
241 if (selection) {
242 view.setSelection(selection);
243 view.getView().focusRow(selection);
244 }
245 }
246
247 Proxmox.Utils.setErrorMask(view, false);
248 if (view.getStore().getFilters().length > 0) {
249 let searchBox = me.lookup("searchbox");
250 let searchvalue = searchBox.getValue();;
251 me.search(searchBox, searchvalue);
252 }
253 },
254
255 onPrune: function(view, rI, cI, item, e, rec) {
256 var view = this.getView();
257
258 if (!(rec && rec.data)) return;
259 let data = rec.data;
260 if (rec.parentNode.id !== 'root') return;
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();
271 },
272
273 onVerify: function(view, rI, cI, item, e, rec) {
274 let me = this;
275 view = me.getView();
276
277 if (!view.datastore) return;
278
279 if (!(rec && rec.data)) return;
280 let data = rec.data;
281
282 let params;
283
284 if (rec.parentNode.id !== 'root') {
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,
307 taskDone: () => me.reload(),
308 }).show();
309 },
310 });
311 },
312
313 onForget: function(view, rI, cI, item, e, rec) {
314 let me = this;
315 var view = this.getView();
316
317 if (!(rec && rec.data)) return;
318 let data = rec.data;
319 if (!view.datastore) return;
320
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 }
331
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 });
346 },
347 });
348 },
349
350 downloadFile: function(tV, rI, cI, item, e, rec) {
351 let me = this;
352 let view = me.getView();
353
354 if (!(rec && rec.data)) return;
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();
376 },
377
378 openPxarBrowser: function(tv, rI, Ci, item, e, rec) {
379 let me = this;
380 let view = me.getView();
381
382 if (!(rec && rec.data)) return;
383 let data = rec.parentNode.data;
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,
396 archive: rec.data.filename,
397 }).show();
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 },
463 },
464
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
474 columns: [
475 {
476 xtype: 'treecolumn',
477 header: gettext("Backup Group"),
478 dataIndex: 'text',
479 flex: 1
480 },
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') &&
525 data['crypt-mode'] < 3);
526 }
527 },
528 ]
529 },
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',
542 renderer: (v, meta, record) => {
543 if (record.data.text === 'client.log.blob' && v === undefined) {
544 return '';
545 }
546 if (v === undefined || v === null) {
547 meta.tdCls = "x-grid-row-loading";
548 return '';
549 }
550 return Proxmox.Utils.format_size(v);
551 },
552 },
553 {
554 xtype: 'numbercolumn',
555 format: '0',
556 header: gettext("Count"),
557 sortable: true,
558 width: 75,
559 align: 'right',
560 dataIndex: 'count',
561 },
562 {
563 header: gettext("Owner"),
564 sortable: true,
565 dataIndex: 'owner',
566 },
567 {
568 header: gettext('Encrypted'),
569 dataIndex: 'crypt-mode',
570 renderer: (v, meta, record) => {
571 if (record.data.size === undefined || record.data.size === null) {
572 return '';
573 }
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 }
584 },
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 },
620 ],
621
622 tbar: [
623 {
624 text: gettext('Reload'),
625 iconCls: 'fa fa-refresh',
626 handler: 'reload',
627 },
628 '->',
629 {
630 xtype: 'tbtext',
631 html: gettext('Search'),
632 },
633 {
634 xtype: 'textfield',
635 reference: 'searchbox',
636 emptyText: gettext('group, date or owner'),
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 }
655 ],
656 });