]> git.proxmox.com Git - proxmox-backup.git/blob - www/DataStoreContent.js
ui: ds/content: show spinner for backups in progress
[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: 'encrypted',
16 type: 'boolean',
17 calculate: function(data) {
18 let encrypted = 0;
19 let files = 0;
20 data.files.forEach(file => {
21 if (file.filename === 'index.json.blob') return; // is never encrypted
22 if (file.encrypted) {
23 encrypted++;
24 }
25 files++;
26 });
27
28 if (encrypted === 0) {
29 return 0;
30 } else if (encrypted < files) {
31 return 1;
32 } else {
33 return 2;
34 }
35 }
36 }
37 ]
38 });
39
40 Ext.define('PBS.DataStoreContent', {
41 extend: 'Ext.tree.Panel',
42 alias: 'widget.pbsDataStoreContent',
43
44 rootVisible: false,
45
46 title: gettext('Content'),
47
48 controller: {
49 xclass: 'Ext.app.ViewController',
50
51 init: function(view) {
52 if (!view.datastore) {
53 throw "no datastore specified";
54 }
55
56 this.store = Ext.create('Ext.data.Store', {
57 model: 'pbs-data-store-snapshots',
58 groupField: 'backup-group',
59 });
60 this.store.on('load', this.onLoad, this);
61
62 view.getStore().setSorters([
63 'backup-group',
64 'text',
65 'backup-time'
66 ]);
67 Proxmox.Utils.monStoreErrors(view, this.store);
68 this.reload(); // initial load
69 },
70
71 reload: function() {
72 let view = this.getView();
73
74 if (!view.store || !this.store) {
75 console.warn('cannot reload, no store(s)');
76 return;
77 }
78
79 let url = `/api2/json/admin/datastore/${view.datastore}/snapshots`;
80 this.store.setProxy({
81 type: 'proxmox',
82 timeout: 300*1000, // 5 minutes, we should make that api call faster
83 url: url
84 });
85
86 this.store.load();
87 },
88
89 getRecordGroups: function(records) {
90 let groups = {};
91
92 for (const item of records) {
93 var btype = item.data["backup-type"];
94 let group = btype + "/" + item.data["backup-id"];
95
96 if (groups[group] !== undefined) {
97 continue;
98 }
99
100 var cls = '';
101 if (btype === 'vm') {
102 cls = 'fa-desktop';
103 } else if (btype === 'ct') {
104 cls = 'fa-cube';
105 } else if (btype === 'host') {
106 cls = 'fa-building';
107 } else {
108 console.warn(`got unknown backup-type '${btype}'`);
109 continue; // FIXME: auto render? what do?
110 }
111
112 groups[group] = {
113 text: group,
114 leaf: false,
115 iconCls: "fa " + cls,
116 expanded: false,
117 backup_type: item.data["backup-type"],
118 backup_id: item.data["backup-id"],
119 children: []
120 };
121 }
122
123 return groups;
124 },
125
126 onLoad: function(store, records, success, operation) {
127 let view = this.getView();
128
129 if (!success) {
130 Proxmox.Utils.setErrorMask(view, Proxmox.Utils.getResponseErrorMessage(operation.getError()));
131 return;
132 }
133
134 let groups = this.getRecordGroups(records);
135
136 for (const item of records) {
137 let group = item.data["backup-type"] + "/" + item.data["backup-id"];
138 let children = groups[group].children;
139
140 let data = item.data;
141
142 data.text = group + '/' + PBS.Utils.render_datetime_utc(data["backup-time"]);
143 data.leaf = true;
144 data.cls = 'no-leaf-icons';
145
146 children.push(data);
147 }
148
149 let children = [];
150 for (const [_key, group] of Object.entries(groups)) {
151 let last_backup = 0;
152 let encrypted = 0;
153 for (const item of group.children) {
154 if (item.encrypted > 0) {
155 encrypted++;
156 }
157 if (item["backup-time"] > last_backup) {
158 last_backup = item["backup-time"];
159 group["backup-time"] = last_backup;
160 group.files = item.files;
161 group.size = item.size;
162 group.owner = item.owner;
163 }
164
165 }
166 if (encrypted === 0) {
167 group.encrypted = 0;
168 } else if (encrypted < group.children.length) {
169 group.encrypted = 1;
170 } else {
171 group.encrypted = 2;
172 }
173 group.count = group.children.length;
174 children.push(group);
175 }
176
177 view.setRootNode({
178 expanded: true,
179 children: children
180 });
181 Proxmox.Utils.setErrorMask(view, false);
182 },
183
184 onPrune: function() {
185 var view = this.getView();
186
187 let rec = view.selModel.getSelection()[0];
188 if (!(rec && rec.data)) return;
189 let data = rec.data;
190 if (data.leaf) return;
191
192 if (!view.datastore) return;
193
194 let win = Ext.create('PBS.DataStorePrune', {
195 datastore: view.datastore,
196 backup_type: data.backup_type,
197 backup_id: data.backup_id,
198 });
199 win.on('destroy', this.reload, this);
200 win.show();
201 },
202
203 onVerify: function() {
204 var view = this.getView();
205
206 if (!view.datastore) return;
207
208 let rec = view.selModel.getSelection()[0];
209 if (!(rec && rec.data)) return;
210 let data = rec.data;
211
212 let params;
213
214 if (data.leaf) {
215 params = {
216 "backup-type": data["backup-type"],
217 "backup-id": data["backup-id"],
218 "backup-time": (data['backup-time'].getTime()/1000).toFixed(0),
219 };
220 } else {
221 params = {
222 "backup-type": data.backup_type,
223 "backup-id": data.backup_id,
224 };
225 }
226
227 Proxmox.Utils.API2Request({
228 params: params,
229 url: `/admin/datastore/${view.datastore}/verify`,
230 method: 'POST',
231 failure: function(response) {
232 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
233 },
234 success: function(response, options) {
235 Ext.create('Proxmox.window.TaskViewer', {
236 upid: response.result.data,
237 }).show();
238 },
239 });
240 },
241
242 onForget: function() {
243 var view = this.getView();
244
245 let rec = view.selModel.getSelection()[0];
246 if (!(rec && rec.data)) return;
247 let data = rec.data;
248 if (!data.leaf) return;
249
250 if (!view.datastore) return;
251
252 console.log(data);
253
254 Proxmox.Utils.API2Request({
255 params: {
256 "backup-type": data["backup-type"],
257 "backup-id": data["backup-id"],
258 "backup-time": (data['backup-time'].getTime()/1000).toFixed(0),
259 },
260 url: `/admin/datastore/${view.datastore}/snapshots`,
261 method: 'DELETE',
262 waitMsgTarget: view,
263 failure: function(response, opts) {
264 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
265 },
266 callback: this.reload.bind(this),
267 });
268 },
269
270 openBackupFileDownloader: function() {
271 let me = this;
272 let view = me.getView();
273
274 let rec = view.selModel.getSelection()[0];
275 if (!(rec && rec.data)) return;
276 let data = rec.data;
277
278 Ext.create('PBS.window.BackupFileDownloader', {
279 baseurl: `/api2/json/admin/datastore/${view.datastore}`,
280 params: {
281 'backup-id': data['backup-id'],
282 'backup-type': data['backup-type'],
283 'backup-time': (data['backup-time'].getTime()/1000).toFixed(0),
284 },
285 files: data.files,
286 }).show();
287 },
288
289 openPxarBrowser: function() {
290 let me = this;
291 let view = me.getView();
292
293 let rec = view.selModel.getSelection()[0];
294 if (!(rec && rec.data)) return;
295 let data = rec.data;
296
297 let encrypted = false;
298 data.files.forEach(file => {
299 if (file.filename === 'catalog.pcat1.didx' && file.encrypted) {
300 encrypted = true;
301 }
302 });
303
304 if (encrypted) {
305 Ext.Msg.alert(
306 gettext('Cannot open Catalog'),
307 gettext('Only unencrypted Backups can be opened on the server. Please use the client with the decryption key instead.'),
308 );
309 return;
310 }
311
312 let id = data['backup-id'];
313 let time = data['backup-time'];
314 let type = data['backup-type'];
315 let timetext = PBS.Utils.render_datetime_utc(data["backup-time"]);
316
317 Ext.create('PBS.window.FileBrowser', {
318 title: `${type}/${id}/${timetext}`,
319 datastore: view.datastore,
320 'backup-id': id,
321 'backup-time': (time.getTime()/1000).toFixed(0),
322 'backup-type': type,
323 }).show();
324 }
325 },
326
327 columns: [
328 {
329 xtype: 'treecolumn',
330 header: gettext("Backup Group"),
331 dataIndex: 'text',
332 flex: 1
333 },
334 {
335 xtype: 'datecolumn',
336 header: gettext('Backup Time'),
337 sortable: true,
338 dataIndex: 'backup-time',
339 format: 'Y-m-d H:i:s',
340 width: 150
341 },
342 {
343 header: gettext("Size"),
344 sortable: true,
345 dataIndex: 'size',
346 renderer: (v, meta, record) => {
347 if (v === undefined || v === null) {
348 meta.tdCls = "x-grid-row-loading";
349 return '';
350 }
351 return Proxmox.Utils.format_size(v);
352 },
353 },
354 {
355 xtype: 'numbercolumn',
356 format: '0',
357 header: gettext("Count"),
358 sortable: true,
359 dataIndex: 'count',
360 },
361 {
362 header: gettext("Owner"),
363 sortable: true,
364 dataIndex: 'owner',
365 },
366 {
367 header: gettext('Encrypted'),
368 dataIndex: 'encrypted',
369 renderer: function(value) {
370 switch (value) {
371 case 0: return Proxmox.Utils.noText;
372 case 1: return gettext('Mixed');
373 case 2: return Proxmox.Utils.yesText;
374 default: Proxmox.Utils.unknownText;
375 }
376 }
377 },
378 {
379 header: gettext("Files"),
380 sortable: false,
381 dataIndex: 'files',
382 renderer: function(files) {
383 return files.map((file) => {
384 let icon = '';
385 let size = '';
386 if (file.encrypted) {
387 icon = '<i class="fa fa-lock"></i> ';
388 }
389 if (file.size) {
390 size = ` (${Proxmox.Utils.format_size(file.size)})`;
391 }
392 return `${icon}${file.filename}${size}`;
393 }).join(', ');
394 },
395 flex: 2
396 },
397 ],
398
399 tbar: [
400 {
401 text: gettext('Reload'),
402 iconCls: 'fa fa-refresh',
403 handler: 'reload',
404 },
405 {
406 xtype: 'proxmoxButton',
407 text: gettext('Verify'),
408 disabled: true,
409 parentXType: 'pbsDataStoreContent',
410 enableFn: function(record) { return !!record.data; },
411 handler: 'onVerify',
412 },
413 {
414 xtype: 'proxmoxButton',
415 text: gettext('Prune'),
416 disabled: true,
417 parentXType: 'pbsDataStoreContent',
418 enableFn: function(record) { return !record.data.leaf; },
419 handler: 'onPrune',
420 },
421 {
422 xtype: 'proxmoxButton',
423 text: gettext('Forget'),
424 disabled: true,
425 parentXType: 'pbsDataStoreContent',
426 handler: 'onForget',
427 confirmMsg: function(record) {
428 console.log(record);
429 let name = record.data.text;
430 return Ext.String.format(gettext('Are you sure you want to remove snapshot {0}'), `'${name}'`);
431 },
432 enableFn: function(record) {
433 return !!record.data.leaf;
434 },
435 },
436 {
437 xtype: 'proxmoxButton',
438 text: gettext('Download Files'),
439 disabled: true,
440 parentXType: 'pbsDataStoreContent',
441 handler: 'openBackupFileDownloader',
442 enableFn: function(record) {
443 return !!record.data.leaf;
444 },
445 },
446 {
447 xtype: "proxmoxButton",
448 text: gettext('PXAR File Browser'),
449 disabled: true,
450 handler: 'openPxarBrowser',
451 parentXType: 'pbsDataStoreContent',
452 enableFn: function(record) {
453 return !!record.data.leaf && record.data.files.some(el => el.filename.endsWith('pxar.didx'));
454 },
455 }
456 ],
457 });