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