]> git.proxmox.com Git - proxmox-backup.git/blob - www/DataStoreContent.js
ui: DataStoreContent: improve encrypted column
[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 }
213 if (item.verification &&
214 (!group.verification || group.verification.state !== 'failed')) {
215 group.verification = item.verification;
216 }
217
218 }
219 group.count = group.children.length;
220 group.matchesFilter = true;
221 crypt.count = group.count;
222 group['crypt-mode'] = PBS.Utils.calculateCryptMode(crypt);
223 group.expanded = !!expanded[name];
224 children.push(group);
225 }
226
227 view.setRootNode({
228 expanded: true,
229 children: children
230 });
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);
240 if (selection) {
241 view.setSelection(selection);
242 view.getView().focusRow(selection);
243 }
244 }
245
246 Proxmox.Utils.setErrorMask(view, false);
247 if (view.getStore().getFilters().length > 0) {
248 let searchBox = me.lookup("searchbox");
249 let searchvalue = searchBox.getValue();;
250 me.search(searchBox, searchvalue);
251 }
252 },
253
254 onPrune: function(view, rI, cI, item, e, rec) {
255 var view = this.getView();
256
257 if (!(rec && rec.data)) return;
258 let data = rec.data;
259 if (rec.parentNode.id !== 'root') return;
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();
270 },
271
272 onVerify: function(view, rI, cI, item, e, rec) {
273 var view = this.getView();
274
275 if (!view.datastore) return;
276
277 if (!(rec && rec.data)) return;
278 let data = rec.data;
279
280 let params;
281
282 if (rec.parentNode.id !== 'root') {
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
310 onForget: function(view, rI, cI, item, e, rec) {
311 let me = this;
312 var view = this.getView();
313
314 if (!(rec && rec.data)) return;
315 let data = rec.data;
316 if (!view.datastore) return;
317
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 }
328
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 });
343 },
344 });
345 },
346
347 downloadFile: function(tV, rI, cI, item, e, rec) {
348 let me = this;
349 let view = me.getView();
350
351 if (!(rec && rec.data)) return;
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();
373 },
374
375 openPxarBrowser: function(tv, rI, Ci, item, e, rec) {
376 let me = this;
377 let view = me.getView();
378
379 if (!(rec && rec.data)) return;
380 let data = rec.parentNode.data;
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,
393 archive: rec.data.filename,
394 }).show();
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 },
460 },
461
462 columns: [
463 {
464 xtype: 'treecolumn',
465 header: gettext("Backup Group"),
466 dataIndex: 'text',
467 flex: 1
468 },
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') &&
513 data['crypt-mode'] < 3);
514 }
515 },
516 ]
517 },
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',
530 renderer: (v, meta, record) => {
531 if (record.data.text === 'client.log.blob' && v === undefined) {
532 return '';
533 }
534 if (v === undefined || v === null) {
535 meta.tdCls = "x-grid-row-loading";
536 return '';
537 }
538 return Proxmox.Utils.format_size(v);
539 },
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 },
553 {
554 header: gettext('Encrypted'),
555 dataIndex: 'crypt-mode',
556 renderer: (v, meta, record) => {
557 if (record.data.size === undefined || record.data.size === null) {
558 return '';
559 }
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 }
570 },
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 },
606 ],
607
608 tbar: [
609 {
610 text: gettext('Reload'),
611 iconCls: 'fa fa-refresh',
612 handler: 'reload',
613 },
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 }
640 ],
641 });