]> git.proxmox.com Git - proxmox-backup.git/blob - www/DataStoreContent.js
ui: DataStoreContent: keep selection and expansion on reload
[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 { name: 'size', type: 'int', allowNull: true, },
14 {
15 name: 'crypt-mode',
16 type: 'boolean',
17 calculate: function(data) {
18 let encrypted = 0;
19 let crypt = {
20 none: 0,
21 mixed: 0,
22 'sign-only': 0,
23 encrypt: 0,
24 count: 0,
25 };
26 let signed = 0;
27 data.files.forEach(file => {
28 if (file.filename === 'index.json.blob') return; // is never encrypted
29 let mode = PBS.Utils.cryptmap.indexOf(file['crypt-mode']);
30 if (mode !== -1) {
31 crypt[file['crypt-mode']]++;
32 }
33 crypt.count++;
34 });
35
36 return PBS.Utils.calculateCryptMode(crypt);
37 }
38 },
39 {
40 name: 'matchesFilter',
41 type: 'boolean',
42 defaultValue: true,
43 },
44 ]
45 });
46
47 Ext.define('PBS.DataStoreContent', {
48 extend: 'Ext.tree.Panel',
49 alias: 'widget.pbsDataStoreContent',
50
51 rootVisible: false,
52
53 title: gettext('Content'),
54
55 controller: {
56 xclass: 'Ext.app.ViewController',
57
58 init: function(view) {
59 if (!view.datastore) {
60 throw "no datastore specified";
61 }
62
63 this.store = Ext.create('Ext.data.Store', {
64 model: 'pbs-data-store-snapshots',
65 groupField: 'backup-group',
66 });
67 this.store.on('load', this.onLoad, this);
68
69 view.getStore().setSorters([
70 'backup-group',
71 'text',
72 'backup-time'
73 ]);
74 Proxmox.Utils.monStoreErrors(view, this.store);
75 this.reload(); // initial load
76 },
77
78 reload: function() {
79 let view = this.getView();
80
81 if (!view.store || !this.store) {
82 console.warn('cannot reload, no store(s)');
83 return;
84 }
85
86 let url = `/api2/json/admin/datastore/${view.datastore}/snapshots`;
87 this.store.setProxy({
88 type: 'proxmox',
89 timeout: 300*1000, // 5 minutes, we should make that api call faster
90 url: url
91 });
92
93 this.store.load();
94 },
95
96 getRecordGroups: function(records) {
97 let groups = {};
98
99 for (const item of records) {
100 var btype = item.data["backup-type"];
101 let group = btype + "/" + item.data["backup-id"];
102
103 if (groups[group] !== undefined) {
104 continue;
105 }
106
107 var cls = '';
108 if (btype === 'vm') {
109 cls = 'fa-desktop';
110 } else if (btype === 'ct') {
111 cls = 'fa-cube';
112 } else if (btype === 'host') {
113 cls = 'fa-building';
114 } else {
115 console.warn(`got unknown backup-type '${btype}'`);
116 continue; // FIXME: auto render? what do?
117 }
118
119 groups[group] = {
120 text: group,
121 leaf: false,
122 iconCls: "fa " + cls,
123 expanded: false,
124 backup_type: item.data["backup-type"],
125 backup_id: item.data["backup-id"],
126 children: []
127 };
128 }
129
130 return groups;
131 },
132
133 onLoad: function(store, records, success, operation) {
134 let me = this;
135 let view = this.getView();
136
137 if (!success) {
138 Proxmox.Utils.setErrorMask(view, Proxmox.Utils.getResponseErrorMessage(operation.getError()));
139 return;
140 }
141
142 let groups = this.getRecordGroups(records);
143
144 let selected;
145 let expanded = {};
146
147 view.getSelection().some(function(item) {
148 let id = item.data.text;
149 if (item.data.leaf) {
150 id = item.parentNode.data.text + id;
151 }
152 selected = id;
153 return true;
154 });
155
156 view.getRootNode().cascadeBy({
157 before: item => {
158 if (item.isExpanded() && !item.data.leaf) {
159 let id = item.data.text;
160 expanded[id] = true;
161 return true;
162 }
163 return false;
164 },
165 after: () => {},
166 });
167
168 for (const item of records) {
169 let group = item.data["backup-type"] + "/" + item.data["backup-id"];
170 let children = groups[group].children;
171
172 let data = item.data;
173
174 data.text = group + '/' + PBS.Utils.render_datetime_utc(data["backup-time"]);
175 data.leaf = false;
176 data.cls = 'no-leaf-icons';
177 data.matchesFilter = true;
178
179 data.expanded = !!expanded[data.text];
180
181 data.children = [];
182 for (const file of data.files) {
183 file.text = file.filename,
184 file['crypt-mode'] = PBS.Utils.cryptmap.indexOf(file['crypt-mode']);
185 file.leaf = true;
186 file.matchesFilter = true;
187
188 data.children.push(file);
189 }
190
191 children.push(data);
192 }
193
194 let children = [];
195 for (const [name, group] of Object.entries(groups)) {
196 let last_backup = 0;
197 let crypt = {
198 none: 0,
199 mixed: 0,
200 'sign-only': 0,
201 encrypt: 0,
202 };
203 for (const item of group.children) {
204 crypt[PBS.Utils.cryptmap[item['crypt-mode']]]++;
205 if (item["backup-time"] > last_backup && item.size !== null) {
206 last_backup = item["backup-time"];
207 group["backup-time"] = last_backup;
208 group.files = item.files;
209 group.size = item.size;
210 group.owner = item.owner;
211 }
212
213 }
214 group.count = group.children.length;
215 group.matchesFilter = true;
216 crypt.count = group.count;
217 group['crypt-mode'] = PBS.Utils.calculateCryptMode(crypt);
218 group.expanded = !!expanded[name];
219 children.push(group);
220 }
221
222 view.setRootNode({
223 expanded: true,
224 children: children
225 });
226
227 if (selected !== undefined) {
228 let selection = view.getRootNode().findChildBy(function(item) {
229 let id = item.data.text;
230 if (item.data.leaf) {
231 id = item.parentNode.data.text + id;
232 }
233 return selected === id;
234 }, undefined, true);
235 view.setSelection(selection);
236 view.getView().focusRow(selection);
237 }
238
239 Proxmox.Utils.setErrorMask(view, false);
240 if (view.getStore().getFilters().length > 0) {
241 let searchBox = me.lookup("searchbox");
242 let searchvalue = searchBox.getValue();;
243 me.search(searchBox, searchvalue);
244 }
245 },
246
247 onPrune: function(view, rI, cI, item, e, rec) {
248 var view = this.getView();
249
250 if (!(rec && rec.data)) return;
251 let data = rec.data;
252 if (rec.parentNode.id !== 'root') return;
253
254 if (!view.datastore) return;
255
256 let win = Ext.create('PBS.DataStorePrune', {
257 datastore: view.datastore,
258 backup_type: data.backup_type,
259 backup_id: data.backup_id,
260 });
261 win.on('destroy', this.reload, this);
262 win.show();
263 },
264
265 onVerify: function(view, rI, cI, item, e, rec) {
266 var view = this.getView();
267
268 if (!view.datastore) return;
269
270 if (!(rec && rec.data)) return;
271 let data = rec.data;
272
273 let params;
274
275 if (rec.parentNode.id !== 'root') {
276 params = {
277 "backup-type": data["backup-type"],
278 "backup-id": data["backup-id"],
279 "backup-time": (data['backup-time'].getTime()/1000).toFixed(0),
280 };
281 } else {
282 params = {
283 "backup-type": data.backup_type,
284 "backup-id": data.backup_id,
285 };
286 }
287
288 Proxmox.Utils.API2Request({
289 params: params,
290 url: `/admin/datastore/${view.datastore}/verify`,
291 method: 'POST',
292 failure: function(response) {
293 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
294 },
295 success: function(response, options) {
296 Ext.create('Proxmox.window.TaskViewer', {
297 upid: response.result.data,
298 }).show();
299 },
300 });
301 },
302
303 onForget: function(view, rI, cI, item, e, rec) {
304 let me = this;
305 var view = this.getView();
306
307 if (!(rec && rec.data)) return;
308 let data = rec.data;
309 if (!view.datastore) return;
310
311 Ext.Msg.show({
312 title: gettext('Confirm'),
313 icon: Ext.Msg.WARNING,
314 message: Ext.String.format(gettext('Are you sure you want to remove snapshot {0}'), `'${data.text}'`),
315 buttons: Ext.Msg.YESNO,
316 defaultFocus: 'no',
317 callback: function(btn) {
318 if (btn !== 'yes') {
319 return;
320 }
321
322 Proxmox.Utils.API2Request({
323 params: {
324 "backup-type": data["backup-type"],
325 "backup-id": data["backup-id"],
326 "backup-time": (data['backup-time'].getTime()/1000).toFixed(0),
327 },
328 url: `/admin/datastore/${view.datastore}/snapshots`,
329 method: 'DELETE',
330 waitMsgTarget: view,
331 failure: function(response, opts) {
332 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
333 },
334 callback: me.reload.bind(me),
335 });
336 },
337 });
338 },
339
340 downloadFile: function(tV, rI, cI, item, e, rec) {
341 let me = this;
342 let view = me.getView();
343
344 if (!(rec && rec.data)) return;
345 let data = rec.parentNode.data;
346
347 let file = rec.data.filename;
348 let params = {
349 'backup-id': data['backup-id'],
350 'backup-type': data['backup-type'],
351 'backup-time': (data['backup-time'].getTime()/1000).toFixed(0),
352 'file-name': file,
353 };
354
355 let idx = file.lastIndexOf('.');
356 let filename = file.slice(0, idx);
357 let atag = document.createElement('a');
358 params['file-name'] = file;
359 atag.download = filename;
360 let url = new URL(`/api2/json/admin/datastore/${view.datastore}/download-decoded`, window.location.origin);
361 for (const [key, value] of Object.entries(params)) {
362 url.searchParams.append(key, value);
363 }
364 atag.href = url.href;
365 atag.click();
366 },
367
368 openPxarBrowser: function(tv, rI, Ci, item, e, rec) {
369 let me = this;
370 let view = me.getView();
371
372 if (!(rec && rec.data)) return;
373 let data = rec.parentNode.data;
374
375 let id = data['backup-id'];
376 let time = data['backup-time'];
377 let type = data['backup-type'];
378 let timetext = PBS.Utils.render_datetime_utc(data["backup-time"]);
379
380 Ext.create('PBS.window.FileBrowser', {
381 title: `${type}/${id}/${timetext}`,
382 datastore: view.datastore,
383 'backup-id': id,
384 'backup-time': (time.getTime()/1000).toFixed(0),
385 'backup-type': type,
386 archive: rec.data.filename,
387 }).show();
388 },
389
390 filter: function(item, value) {
391 if (item.data.text.indexOf(value) !== -1) {
392 return true;
393 }
394
395 if (item.data.owner && item.data.owner.indexOf(value) !== -1) {
396 return true;
397 }
398
399 return false;
400 },
401
402 search: function(tf, value) {
403 let me = this;
404 let view = me.getView();
405 let store = view.getStore();
406 if (!value && value !== 0) {
407 store.clearFilter();
408 store.getRoot().collapseChildren(true);
409 tf.triggers.clear.setVisible(false);
410 return;
411 }
412 tf.triggers.clear.setVisible(true);
413 if (value.length < 2) return;
414 Proxmox.Utils.setErrorMask(view, true);
415 // we do it a little bit later for the error mask to work
416 setTimeout(function() {
417 store.clearFilter();
418 store.getRoot().collapseChildren(true);
419
420 store.beginUpdate();
421 store.getRoot().cascadeBy({
422 before: function(item) {
423 if(me.filter(item, value)) {
424 item.set('matchesFilter', true);
425 if (item.parentNode && item.parentNode.id !== 'root') {
426 item.parentNode.childmatches = true;
427 }
428 return false;
429 }
430 return true;
431 },
432 after: function(item) {
433 if (me.filter(item, value) || item.id === 'root' || item.childmatches) {
434 item.set('matchesFilter', true);
435 if (item.parentNode && item.parentNode.id !== 'root') {
436 item.parentNode.childmatches = true;
437 }
438 if (item.childmatches) {
439 item.expand();
440 }
441 } else {
442 item.set('matchesFilter', false);
443 }
444 delete item.childmatches;
445 },
446 });
447 store.endUpdate();
448
449 store.filter((item) => !!item.get('matchesFilter'));
450 Proxmox.Utils.setErrorMask(view, false);
451 }, 10);
452 },
453 },
454
455 columns: [
456 {
457 xtype: 'treecolumn',
458 header: gettext("Backup Group"),
459 dataIndex: 'text',
460 flex: 1
461 },
462 {
463 header: gettext('Actions'),
464 xtype: 'actioncolumn',
465 dataIndex: 'text',
466 items: [
467 {
468 handler: 'onVerify',
469 tooltip: gettext('Verify'),
470 getClass: (v, m, rec) => rec.data.leaf ? 'pmx-hidden' : 'fa fa-search',
471 isDisabled: (v, r, c, i, rec) => !!rec.data.leaf,
472 },
473 {
474 handler: 'onPrune',
475 tooltip: gettext('Prune'),
476 getClass: (v, m, rec) => rec.parentNode.id ==='root' ? 'fa fa-scissors' : 'pmx-hidden',
477 isDisabled: (v, r, c, i, rec) => rec.parentNode.id !=='root',
478 },
479 {
480 handler: 'onForget',
481 tooltip: gettext('Forget Snapshot'),
482 getClass: (v, m, rec) => !rec.data.leaf && rec.parentNode.id !== 'root' ? 'fa critical fa-trash-o' : 'pmx-hidden',
483 isDisabled: (v, r, c, i, rec) => rec.data.leaf || rec.parentNode.id === 'root',
484 },
485 {
486 handler: 'downloadFile',
487 tooltip: gettext('Download'),
488 getClass: (v, m, rec) => rec.data.leaf && rec.data.filename ? 'fa fa-download' : 'pmx-hidden',
489 isDisabled: (v, r, c, i, rec) => !rec.data.leaf || !rec.data.filename || rec.data['crypt-mode'] > 2,
490 },
491 {
492 handler: 'openPxarBrowser',
493 tooltip: gettext('Browse'),
494 getClass: (v, m, rec) => {
495 let data = rec.data;
496 if (data.leaf && data.filename && data.filename.endsWith('pxar.didx')) {
497 return 'fa fa-folder-open-o';
498 }
499 return 'pmx-hidden';
500 },
501 isDisabled: (v, r, c, i, rec) => {
502 let data = rec.data;
503 return !(data.leaf &&
504 data.filename &&
505 data.filename.endsWith('pxar.didx') &&
506 data['crypt-mode'] < 2);
507 }
508 },
509 ]
510 },
511 {
512 xtype: 'datecolumn',
513 header: gettext('Backup Time'),
514 sortable: true,
515 dataIndex: 'backup-time',
516 format: 'Y-m-d H:i:s',
517 width: 150
518 },
519 {
520 header: gettext("Size"),
521 sortable: true,
522 dataIndex: 'size',
523 renderer: (v, meta, record) => {
524 if (record.data.text === 'client.log.blob' && v === undefined) {
525 return '';
526 }
527 if (v === undefined || v === null) {
528 meta.tdCls = "x-grid-row-loading";
529 return '';
530 }
531 return Proxmox.Utils.format_size(v);
532 },
533 },
534 {
535 xtype: 'numbercolumn',
536 format: '0',
537 header: gettext("Count"),
538 sortable: true,
539 dataIndex: 'count',
540 },
541 {
542 header: gettext("Owner"),
543 sortable: true,
544 dataIndex: 'owner',
545 },
546 {
547 header: gettext('Encrypted'),
548 dataIndex: 'crypt-mode',
549 renderer: (v, meta, record) => {
550 if (v === -1) {
551 return '';
552 }
553 let iconCls = PBS.Utils.cryptIconCls[v] || '';
554 let iconTxt = "";
555 if (iconCls) {
556 iconTxt = `<i class="fa fa-fw fa-${iconCls}"></i> `;
557 }
558 return (iconTxt + PBS.Utils.cryptText[v]) || Proxmox.Utils.unknownText
559 }
560 },
561 ],
562
563 tbar: [
564 {
565 text: gettext('Reload'),
566 iconCls: 'fa fa-refresh',
567 handler: 'reload',
568 },
569 '->',
570 {
571 xtype: 'tbtext',
572 html: gettext('Search'),
573 },
574 {
575 xtype: 'textfield',
576 reference: 'searchbox',
577 triggers: {
578 clear: {
579 cls: 'pmx-clear-trigger',
580 weight: -1,
581 hidden: true,
582 handler: function() {
583 this.triggers.clear.setVisible(false);
584 this.setValue('');
585 },
586 }
587 },
588 listeners: {
589 change: {
590 fn: 'search',
591 buffer: 500,
592 },
593 },
594 }
595 ],
596 });