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