]> git.proxmox.com Git - proxmox-backup.git/blame - www/DataStoreContent.js
ui: datastore content: show last verify result from a snapshot
[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']]++;
e005f953 33 }
106603c5 34 crypt.count++;
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) {
184 file.text = file.filename,
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;
3f98b347 212 }
7b212c1f
TL
213 if (item.verification &&
214 (!group.verification || group.verification.state !== 'failed')) {
215 group.verification = item.verification;
216 }
e005f953 217
3f98b347
TL
218 }
219 group.count = group.children.length;
6d55603d 220 group.matchesFilter = true;
106603c5
DC
221 crypt.count = group.count;
222 group['crypt-mode'] = PBS.Utils.calculateCryptMode(crypt);
69d970a6 223 group.expanded = !!expanded[name];
3f98b347
TL
224 children.push(group);
225 }
226
227 view.setRootNode({
228 expanded: true,
229 children: children
230 });
69d970a6
DC
231
232 if (selected !== undefined) {
233 let selection = view.getRootNode().findChildBy(function(item) {
234 let id = item.data.text;
235 if (item.data.leaf) {
236 id = item.parentNode.data.text + id;
237 }
238 return selected === id;
239 }, undefined, true);
80db161e
SR
240 if (selection) {
241 view.setSelection(selection);
242 view.getView().focusRow(selection);
243 }
69d970a6
DC
244 }
245
90779237 246 Proxmox.Utils.setErrorMask(view, false);
6d55603d
DC
247 if (view.getStore().getFilters().length > 0) {
248 let searchBox = me.lookup("searchbox");
249 let searchvalue = searchBox.getValue();;
250 me.search(searchBox, searchvalue);
251 }
f1baa7f4 252 },
5f448992 253
3e395378 254 onPrune: function(view, rI, cI, item, e, rec) {
5f448992
DM
255 var view = this.getView();
256
5f448992
DM
257 if (!(rec && rec.data)) return;
258 let data = rec.data;
3e395378 259 if (rec.parentNode.id !== 'root') return;
5f448992
DM
260
261 if (!view.datastore) return;
262
263 let win = Ext.create('PBS.DataStorePrune', {
264 datastore: view.datastore,
265 backup_type: data.backup_type,
266 backup_id: data.backup_id,
267 });
268 win.on('destroy', this.reload, this);
269 win.show();
98425309
DC
270 },
271
3e395378 272 onVerify: function(view, rI, cI, item, e, rec) {
8f6088c1
DM
273 var view = this.getView();
274
275 if (!view.datastore) return;
276
8f6088c1
DM
277 if (!(rec && rec.data)) return;
278 let data = rec.data;
279
280 let params;
281
3e395378 282 if (rec.parentNode.id !== 'root') {
8f6088c1
DM
283 params = {
284 "backup-type": data["backup-type"],
285 "backup-id": data["backup-id"],
286 "backup-time": (data['backup-time'].getTime()/1000).toFixed(0),
287 };
288 } else {
289 params = {
290 "backup-type": data.backup_type,
291 "backup-id": data.backup_id,
292 };
293 }
294
295 Proxmox.Utils.API2Request({
296 params: params,
297 url: `/admin/datastore/${view.datastore}/verify`,
298 method: 'POST',
299 failure: function(response) {
300 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
301 },
302 success: function(response, options) {
303 Ext.create('Proxmox.window.TaskViewer', {
304 upid: response.result.data,
305 }).show();
306 },
307 });
308 },
309
3e395378
DC
310 onForget: function(view, rI, cI, item, e, rec) {
311 let me = this;
4ff2c9b8
DM
312 var view = this.getView();
313
4ff2c9b8
DM
314 if (!(rec && rec.data)) return;
315 let data = rec.data;
4ff2c9b8
DM
316 if (!view.datastore) return;
317
3e395378
DC
318 Ext.Msg.show({
319 title: gettext('Confirm'),
320 icon: Ext.Msg.WARNING,
321 message: Ext.String.format(gettext('Are you sure you want to remove snapshot {0}'), `'${data.text}'`),
322 buttons: Ext.Msg.YESNO,
323 defaultFocus: 'no',
324 callback: function(btn) {
325 if (btn !== 'yes') {
326 return;
327 }
4ff2c9b8 328
3e395378
DC
329 Proxmox.Utils.API2Request({
330 params: {
331 "backup-type": data["backup-type"],
332 "backup-id": data["backup-id"],
333 "backup-time": (data['backup-time'].getTime()/1000).toFixed(0),
334 },
335 url: `/admin/datastore/${view.datastore}/snapshots`,
336 method: 'DELETE',
337 waitMsgTarget: view,
338 failure: function(response, opts) {
339 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
340 },
341 callback: me.reload.bind(me),
342 });
4ff2c9b8 343 },
4ff2c9b8
DM
344 });
345 },
346
3e395378 347 downloadFile: function(tV, rI, cI, item, e, rec) {
98425309
DC
348 let me = this;
349 let view = me.getView();
350
98425309 351 if (!(rec && rec.data)) return;
3e395378
DC
352 let data = rec.parentNode.data;
353
354 let file = rec.data.filename;
355 let params = {
356 'backup-id': data['backup-id'],
357 'backup-type': data['backup-type'],
358 'backup-time': (data['backup-time'].getTime()/1000).toFixed(0),
359 'file-name': file,
360 };
361
362 let idx = file.lastIndexOf('.');
363 let filename = file.slice(0, idx);
364 let atag = document.createElement('a');
365 params['file-name'] = file;
366 atag.download = filename;
367 let url = new URL(`/api2/json/admin/datastore/${view.datastore}/download-decoded`, window.location.origin);
368 for (const [key, value] of Object.entries(params)) {
369 url.searchParams.append(key, value);
370 }
371 atag.href = url.href;
372 atag.click();
8567c0d2
DC
373 },
374
3e395378 375 openPxarBrowser: function(tv, rI, Ci, item, e, rec) {
8567c0d2
DC
376 let me = this;
377 let view = me.getView();
378
8567c0d2 379 if (!(rec && rec.data)) return;
3e395378 380 let data = rec.parentNode.data;
8567c0d2
DC
381
382 let id = data['backup-id'];
383 let time = data['backup-time'];
384 let type = data['backup-type'];
385 let timetext = PBS.Utils.render_datetime_utc(data["backup-time"]);
386
387 Ext.create('PBS.window.FileBrowser', {
388 title: `${type}/${id}/${timetext}`,
389 datastore: view.datastore,
390 'backup-id': id,
391 'backup-time': (time.getTime()/1000).toFixed(0),
392 'backup-type': type,
3e395378 393 archive: rec.data.filename,
8567c0d2 394 }).show();
6d55603d
DC
395 },
396
397 filter: function(item, value) {
398 if (item.data.text.indexOf(value) !== -1) {
399 return true;
400 }
401
402 if (item.data.owner && item.data.owner.indexOf(value) !== -1) {
403 return true;
404 }
405
406 return false;
407 },
408
409 search: function(tf, value) {
410 let me = this;
411 let view = me.getView();
412 let store = view.getStore();
413 if (!value && value !== 0) {
414 store.clearFilter();
415 store.getRoot().collapseChildren(true);
416 tf.triggers.clear.setVisible(false);
417 return;
418 }
419 tf.triggers.clear.setVisible(true);
420 if (value.length < 2) return;
421 Proxmox.Utils.setErrorMask(view, true);
422 // we do it a little bit later for the error mask to work
423 setTimeout(function() {
424 store.clearFilter();
425 store.getRoot().collapseChildren(true);
426
427 store.beginUpdate();
428 store.getRoot().cascadeBy({
429 before: function(item) {
430 if(me.filter(item, value)) {
431 item.set('matchesFilter', true);
432 if (item.parentNode && item.parentNode.id !== 'root') {
433 item.parentNode.childmatches = true;
434 }
435 return false;
436 }
437 return true;
438 },
439 after: function(item) {
440 if (me.filter(item, value) || item.id === 'root' || item.childmatches) {
441 item.set('matchesFilter', true);
442 if (item.parentNode && item.parentNode.id !== 'root') {
443 item.parentNode.childmatches = true;
444 }
445 if (item.childmatches) {
446 item.expand();
447 }
448 } else {
449 item.set('matchesFilter', false);
450 }
451 delete item.childmatches;
452 },
453 });
454 store.endUpdate();
455
456 store.filter((item) => !!item.get('matchesFilter'));
457 Proxmox.Utils.setErrorMask(view, false);
458 }, 10);
459 },
f1baa7f4
TL
460 },
461
04b0ca8b
DC
462 columns: [
463 {
464 xtype: 'treecolumn',
465 header: gettext("Backup Group"),
466 dataIndex: 'text',
467 flex: 1
468 },
3e395378
DC
469 {
470 header: gettext('Actions'),
471 xtype: 'actioncolumn',
472 dataIndex: 'text',
473 items: [
474 {
475 handler: 'onVerify',
476 tooltip: gettext('Verify'),
477 getClass: (v, m, rec) => rec.data.leaf ? 'pmx-hidden' : 'fa fa-search',
478 isDisabled: (v, r, c, i, rec) => !!rec.data.leaf,
479 },
480 {
481 handler: 'onPrune',
482 tooltip: gettext('Prune'),
483 getClass: (v, m, rec) => rec.parentNode.id ==='root' ? 'fa fa-scissors' : 'pmx-hidden',
484 isDisabled: (v, r, c, i, rec) => rec.parentNode.id !=='root',
485 },
486 {
487 handler: 'onForget',
488 tooltip: gettext('Forget Snapshot'),
489 getClass: (v, m, rec) => !rec.data.leaf && rec.parentNode.id !== 'root' ? 'fa critical fa-trash-o' : 'pmx-hidden',
490 isDisabled: (v, r, c, i, rec) => rec.data.leaf || rec.parentNode.id === 'root',
491 },
492 {
493 handler: 'downloadFile',
494 tooltip: gettext('Download'),
495 getClass: (v, m, rec) => rec.data.leaf && rec.data.filename ? 'fa fa-download' : 'pmx-hidden',
496 isDisabled: (v, r, c, i, rec) => !rec.data.leaf || !rec.data.filename || rec.data['crypt-mode'] > 2,
497 },
498 {
499 handler: 'openPxarBrowser',
500 tooltip: gettext('Browse'),
501 getClass: (v, m, rec) => {
502 let data = rec.data;
503 if (data.leaf && data.filename && data.filename.endsWith('pxar.didx')) {
504 return 'fa fa-folder-open-o';
505 }
506 return 'pmx-hidden';
507 },
508 isDisabled: (v, r, c, i, rec) => {
509 let data = rec.data;
510 return !(data.leaf &&
511 data.filename &&
512 data.filename.endsWith('pxar.didx') &&
c96b0de4 513 data['crypt-mode'] < 3);
3e395378
DC
514 }
515 },
516 ]
517 },
04b0ca8b
DC
518 {
519 xtype: 'datecolumn',
520 header: gettext('Backup Time'),
521 sortable: true,
522 dataIndex: 'backup-time',
523 format: 'Y-m-d H:i:s',
524 width: 150
525 },
526 {
527 header: gettext("Size"),
528 sortable: true,
529 dataIndex: 'size',
a7a5f56d 530 renderer: (v, meta, record) => {
3e395378
DC
531 if (record.data.text === 'client.log.blob' && v === undefined) {
532 return '';
533 }
a7a5f56d
TL
534 if (v === undefined || v === null) {
535 meta.tdCls = "x-grid-row-loading";
536 return '';
537 }
538 return Proxmox.Utils.format_size(v);
539 },
04b0ca8b
DC
540 },
541 {
542 xtype: 'numbercolumn',
543 format: '0',
544 header: gettext("Count"),
545 sortable: true,
546 dataIndex: 'count',
547 },
548 {
549 header: gettext("Owner"),
550 sortable: true,
551 dataIndex: 'owner',
552 },
e005f953
DC
553 {
554 header: gettext('Encrypted'),
2774566b 555 dataIndex: 'crypt-mode',
3e395378 556 renderer: (v, meta, record) => {
71282dd9
SR
557 if (record.data.size === undefined || record.data.size === null) {
558 return '';
559 }
3e395378
DC
560 if (v === -1) {
561 return '';
562 }
563 let iconCls = PBS.Utils.cryptIconCls[v] || '';
564 let iconTxt = "";
565 if (iconCls) {
566 iconTxt = `<i class="fa fa-fw fa-${iconCls}"></i> `;
567 }
568 return (iconTxt + PBS.Utils.cryptText[v]) || Proxmox.Utils.unknownText
569 }
04b0ca8b 570 },
7b212c1f
TL
571 {
572 header: gettext('Verify State'),
573 sortable: true,
574 dataIndex: 'verification',
575 renderer: (v, meta, record) => {
576 if (v === undefined || v === null || !v.state) {
577 //meta.tdCls = "x-grid-row-loading";
578 return record.data.leaf ? '' : gettext('None');
579 }
580 let task = Proxmox.Utils.parse_task_upid(v.upid);
581 let verify_time = Proxmox.Utils.render_timestamp(task.starttime);
582 let iconCls = v.state === 'ok' ? 'check good' : 'times critical';
583 let tip = `Verify task started on ${verify_time}`;
584 if (record.parentNode.id === 'root') {
585 tip = v.state === 'ok'
586 ? 'All verification OK in backup group'
587 : 'At least one failed verification in backup group!';
588 }
589 return `<span data-qtip="${tip}">
590 <i class="fa fa-fw fa-${iconCls}"></i> ${v.state}
591 </span>`;
592 },
593 listeners: {
594 dblclick: function(view, el, row, col, ev, rec) {
595 let data = rec.data || {};
596 let verify = data.verification;
597 if (verify && verify.upid && rec.parentNode.id !== 'root') {
598 let win = Ext.create('Proxmox.window.TaskViewer', {
599 upid: verify.upid,
600 });
601 win.show();
602 }
603 },
604 },
605 },
04b0ca8b 606 ],
b1127fd0 607
04b0ca8b
DC
608 tbar: [
609 {
610 text: gettext('Reload'),
611 iconCls: 'fa fa-refresh',
612 handler: 'reload',
613 },
6d55603d
DC
614 '->',
615 {
616 xtype: 'tbtext',
617 html: gettext('Search'),
618 },
619 {
620 xtype: 'textfield',
621 reference: 'searchbox',
622 triggers: {
623 clear: {
624 cls: 'pmx-clear-trigger',
625 weight: -1,
626 hidden: true,
627 handler: function() {
628 this.triggers.clear.setVisible(false);
629 this.setValue('');
630 },
631 }
632 },
633 listeners: {
634 change: {
635 fn: 'search',
636 buffer: 500,
637 },
638 },
639 }
04b0ca8b 640 ],
ca23a97f 641});