]>
Commit | Line | Data |
---|---|---|
33839735 | 1 | Ext.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 | 8 | type: 'date', |
c6e07769 | 9 | dateFormat: 'timestamp', |
507c39c5 | 10 | }, |
c9725bb8 | 11 | 'comment', |
d9c38ddc | 12 | 'files', |
04b0ca8b | 13 | 'owner', |
7b212c1f | 14 | 'verification', |
6982a547 | 15 | 'fingerprint', |
c6e07769 | 16 | { name: 'size', type: 'int', allowNull: true }, |
e005f953 | 17 | { |
2774566b | 18 | name: 'crypt-mode', |
676b0fde | 19 | type: 'boolean', |
e005f953 | 20 | calculate: function(data) { |
2774566b DC |
21 | let crypt = { |
22 | none: 0, | |
23 | mixed: 0, | |
24 | 'sign-only': 0, | |
25 | encrypt: 0, | |
106603c5 | 26 | count: 0, |
2774566b | 27 | }; |
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); |
c6e07769 | 38 | }, |
6d55603d DC |
39 | }, |
40 | { | |
41 | name: 'matchesFilter', | |
42 | type: 'boolean', | |
43 | defaultValue: true, | |
44 | }, | |
c6e07769 | 45 | ], |
ca23a97f DM |
46 | }); |
47 | ||
48 | Ext.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', | |
c6e07769 | 73 | 'backup-time', |
7b1e2669 | 74 | ]); |
90779237 | 75 | Proxmox.Utils.monStoreErrors(view, this.store); |
f1baa7f4 TL |
76 | }, |
77 | ||
78 | reload: function() { | |
3f98b347 TL |
79 | let view = this.getView(); |
80 | ||
81 | if (!view.store || !this.store) { | |
82 | console.warn('cannot reload, no store(s)'); | |
83 | return; | |
84 | } | |
f1baa7f4 | 85 | |
e8f0ad19 | 86 | let url = `/api2/json/admin/datastore/${view.datastore}/snapshots`; |
3f98b347 | 87 | this.store.setProxy({ |
f1baa7f4 | 88 | type: 'proxmox', |
26f499b1 | 89 | timeout: 300*1000, // 5 minutes, we should make that api call faster |
c6e07769 | 90 | url: url, |
f1baa7f4 | 91 | }); |
e8f0ad19 | 92 | |
3f98b347 TL |
93 | this.store.load(); |
94 | }, | |
e8f0ad19 | 95 | |
3f98b347 TL |
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 | ||
f806c0ef DC |
107 | var cls = PBS.Utils.get_type_icon_cls(btype); |
108 | if (cls === "") { | |
add5861e | 109 | console.warn(`got unknown backup-type '${btype}'`); |
3f98b347 TL |
110 | continue; // FIXME: auto render? what do? |
111 | } | |
112 | ||
113 | groups[group] = { | |
114 | text: group, | |
115 | leaf: false, | |
116 | iconCls: "fa " + cls, | |
117 | expanded: false, | |
118 | backup_type: item.data["backup-type"], | |
119 | backup_id: item.data["backup-id"], | |
c6e07769 | 120 | children: [], |
aeee4329 | 121 | }; |
3f98b347 | 122 | } |
aeee4329 | 123 | |
3f98b347 TL |
124 | return groups; |
125 | }, | |
e8f0ad19 | 126 | |
8866cbcc TL |
127 | updateGroupNotes: async function(view) { |
128 | try { | |
129 | let { result: { data: groups } } = await Proxmox.Async.api2({ | |
130 | url: `/api2/extjs/admin/datastore/${view.datastore}/groups`, | |
131 | }); | |
132 | let map = {}; | |
133 | for (const group of groups) { | |
134 | map[`${group["backup-type"]}/${group["backup-id"]}`] = group.comment; | |
135 | } | |
136 | view.getRootNode().cascade(node => { | |
137 | if (node.parentNode && node.parentNode.id === 'root') { | |
138 | let group = `${node.data.backup_type}/${node.data.backup_id}`; | |
139 | node.set('comment', map[group], { dirty: false }); | |
68e2ea99 | 140 | } |
8866cbcc TL |
141 | }); |
142 | } catch (err) { | |
143 | console.debug(err); | |
144 | } | |
68e2ea99 SR |
145 | }, |
146 | ||
90779237 | 147 | onLoad: function(store, records, success, operation) { |
6d55603d | 148 | let me = this; |
3f98b347 TL |
149 | let view = this.getView(); |
150 | ||
151 | if (!success) { | |
90779237 | 152 | Proxmox.Utils.setErrorMask(view, Proxmox.Utils.getResponseErrorMessage(operation.getError())); |
3f98b347 TL |
153 | return; |
154 | } | |
155 | ||
156 | let groups = this.getRecordGroups(records); | |
e8f0ad19 | 157 | |
69d970a6 DC |
158 | let selected; |
159 | let expanded = {}; | |
160 | ||
161 | view.getSelection().some(function(item) { | |
162 | let id = item.data.text; | |
163 | if (item.data.leaf) { | |
164 | id = item.parentNode.data.text + id; | |
165 | } | |
166 | selected = id; | |
167 | return true; | |
168 | }); | |
169 | ||
170 | view.getRootNode().cascadeBy({ | |
171 | before: item => { | |
172 | if (item.isExpanded() && !item.data.leaf) { | |
173 | let id = item.data.text; | |
174 | expanded[id] = true; | |
175 | return true; | |
176 | } | |
177 | return false; | |
178 | }, | |
c6e07769 | 179 | after: Ext.emptyFn, |
69d970a6 DC |
180 | }); |
181 | ||
3f98b347 TL |
182 | for (const item of records) { |
183 | let group = item.data["backup-type"] + "/" + item.data["backup-id"]; | |
184 | let children = groups[group].children; | |
185 | ||
186 | let data = item.data; | |
187 | ||
f68ae22c | 188 | data.text = group + '/' + PBS.Utils.render_datetime_utc(data["backup-time"]); |
3e395378 | 189 | data.leaf = false; |
3f98b347 | 190 | data.cls = 'no-leaf-icons'; |
6d55603d | 191 | data.matchesFilter = true; |
3f98b347 | 192 | |
69d970a6 DC |
193 | data.expanded = !!expanded[data.text]; |
194 | ||
3e395378 DC |
195 | data.children = []; |
196 | for (const file of data.files) { | |
b1149ebb | 197 | file.text = file.filename; |
3e395378 | 198 | file['crypt-mode'] = PBS.Utils.cryptmap.indexOf(file['crypt-mode']); |
6982a547 | 199 | file.fingerprint = data.fingerprint; |
3e395378 | 200 | file.leaf = true; |
6d55603d | 201 | file.matchesFilter = true; |
3e395378 DC |
202 | |
203 | data.children.push(file); | |
204 | } | |
205 | ||
3f98b347 TL |
206 | children.push(data); |
207 | } | |
208 | ||
423df9b1 | 209 | let nowSeconds = Date.now() / 1000; |
3f98b347 | 210 | let children = []; |
69d970a6 | 211 | for (const [name, group] of Object.entries(groups)) { |
3f98b347 | 212 | let last_backup = 0; |
2774566b DC |
213 | let crypt = { |
214 | none: 0, | |
215 | mixed: 0, | |
216 | 'sign-only': 0, | |
106603c5 | 217 | encrypt: 0, |
2774566b | 218 | }; |
423df9b1 TL |
219 | let verify = { |
220 | outdated: 0, | |
221 | none: 0, | |
222 | failed: 0, | |
223 | ok: 0, | |
224 | }; | |
225 | for (let item of group.children) { | |
2774566b | 226 | crypt[PBS.Utils.cryptmap[item['crypt-mode']]]++; |
48e22a89 | 227 | if (item["backup-time"] > last_backup && item.size !== null) { |
3f98b347 TL |
228 | last_backup = item["backup-time"]; |
229 | group["backup-time"] = last_backup; | |
230 | group.files = item.files; | |
231 | group.size = item.size; | |
04b0ca8b | 232 | group.owner = item.owner; |
c879e5af | 233 | verify.lastFailed = item.verification && item.verification.state !== 'ok'; |
3f98b347 | 234 | } |
423df9b1 TL |
235 | if (!item.verification) { |
236 | verify.none++; | |
237 | } else { | |
238 | if (item.verification.state === 'ok') { | |
239 | verify.ok++; | |
240 | } else { | |
241 | verify.failed++; | |
242 | } | |
243 | let task = Proxmox.Utils.parse_task_upid(item.verification.upid); | |
244 | item.verification.lastTime = task.starttime; | |
245 | if (nowSeconds - task.starttime > 30 * 24 * 60 * 60) { | |
246 | verify.outdated++; | |
247 | } | |
7b212c1f | 248 | } |
3f98b347 | 249 | } |
423df9b1 | 250 | group.verification = verify; |
3f98b347 | 251 | group.count = group.children.length; |
6d55603d | 252 | group.matchesFilter = true; |
106603c5 DC |
253 | crypt.count = group.count; |
254 | group['crypt-mode'] = PBS.Utils.calculateCryptMode(crypt); | |
69d970a6 | 255 | group.expanded = !!expanded[name]; |
3f98b347 TL |
256 | children.push(group); |
257 | } | |
258 | ||
259 | view.setRootNode({ | |
260 | expanded: true, | |
c6e07769 | 261 | children: children, |
3f98b347 | 262 | }); |
69d970a6 | 263 | |
ae9b5c07 DC |
264 | if (!children.length) { |
265 | view.setEmptyText(gettext('No Snapshots found')); | |
266 | } | |
267 | ||
68e2ea99 SR |
268 | this.updateGroupNotes(view); |
269 | ||
69d970a6 DC |
270 | if (selected !== undefined) { |
271 | let selection = view.getRootNode().findChildBy(function(item) { | |
272 | let id = item.data.text; | |
273 | if (item.data.leaf) { | |
274 | id = item.parentNode.data.text + id; | |
275 | } | |
276 | return selected === id; | |
277 | }, undefined, true); | |
80db161e SR |
278 | if (selection) { |
279 | view.setSelection(selection); | |
280 | view.getView().focusRow(selection); | |
281 | } | |
69d970a6 DC |
282 | } |
283 | ||
90779237 | 284 | Proxmox.Utils.setErrorMask(view, false); |
6d55603d DC |
285 | if (view.getStore().getFilters().length > 0) { |
286 | let searchBox = me.lookup("searchbox"); | |
c6e07769 | 287 | let searchvalue = searchBox.getValue(); |
6d55603d DC |
288 | me.search(searchBox, searchvalue); |
289 | } | |
f1baa7f4 | 290 | }, |
5f448992 | 291 | |
61db0851 DW |
292 | onChangeOwner: function(view, rI, cI, item, e, rec) { |
293 | view = this.getView(); | |
294 | ||
295 | if (!rec || !rec.data || rec.parentNode.id !== 'root' || !view.datastore) { | |
296 | return; | |
297 | } | |
298 | ||
299 | let data = rec.data; | |
300 | ||
301 | let win = Ext.create('PBS.BackupGroupChangeOwner', { | |
302 | datastore: view.datastore, | |
303 | backup_type: data.backup_type, | |
304 | backup_id: data.backup_id, | |
043406d6 | 305 | owner: data.owner, |
61db0851 DW |
306 | autoShow: true, |
307 | }); | |
308 | win.on('destroy', this.reload, this); | |
309 | }, | |
310 | ||
3e395378 | 311 | onPrune: function(view, rI, cI, item, e, rec) { |
c6e07769 | 312 | view = this.getView(); |
5f448992 | 313 | |
5f448992 DM |
314 | if (!(rec && rec.data)) return; |
315 | let data = rec.data; | |
3e395378 | 316 | if (rec.parentNode.id !== 'root') return; |
5f448992 DM |
317 | |
318 | if (!view.datastore) return; | |
319 | ||
320 | let win = Ext.create('PBS.DataStorePrune', { | |
321 | datastore: view.datastore, | |
322 | backup_type: data.backup_type, | |
323 | backup_id: data.backup_id, | |
324 | }); | |
325 | win.on('destroy', this.reload, this); | |
326 | win.show(); | |
98425309 DC |
327 | }, |
328 | ||
cd92fd73 DC |
329 | verifyAll: function() { |
330 | var view = this.getView(); | |
331 | ||
332 | Proxmox.Utils.API2Request({ | |
333 | url: `/admin/datastore/${view.datastore}/verify`, | |
334 | method: 'POST', | |
335 | failure: function(response) { | |
336 | Ext.Msg.alert(gettext('Error'), response.htmlStatus); | |
337 | }, | |
338 | success: function(response, options) { | |
339 | Ext.create('Proxmox.window.TaskViewer', { | |
340 | upid: response.result.data, | |
341 | }).show(); | |
342 | }, | |
343 | }); | |
344 | }, | |
345 | ||
afbf2e10 DC |
346 | pruneAll: function() { |
347 | let me = this; | |
348 | let view = me.getView(); | |
349 | ||
350 | if (!view.datastore) return; | |
351 | ||
352 | Ext.create('Proxmox.window.Edit', { | |
353 | title: `Prune Datastore '${view.datastore}'`, | |
354 | onlineHelp: 'maintenance_pruning', | |
355 | ||
356 | method: 'POST', | |
357 | submitText: "Prune", | |
358 | autoShow: true, | |
359 | isCreate: true, | |
360 | showTaskViewer: true, | |
361 | ||
362 | taskDone: () => me.reload(), | |
363 | ||
364 | url: `/api2/extjs/admin/datastore/${view.datastore}/prune-datastore`, | |
365 | ||
366 | items: [ | |
367 | { | |
368 | xtype: 'pbsPruneInputPanel', | |
369 | dryrun: true, | |
370 | }, | |
371 | ], | |
372 | }); | |
373 | }, | |
374 | ||
3e395378 | 375 | onVerify: function(view, rI, cI, item, e, rec) { |
484d439a TL |
376 | let me = this; |
377 | view = me.getView(); | |
8f6088c1 DM |
378 | |
379 | if (!view.datastore) return; | |
380 | ||
8f6088c1 DM |
381 | if (!(rec && rec.data)) return; |
382 | let data = rec.data; | |
383 | ||
384 | let params; | |
385 | ||
3e395378 | 386 | if (rec.parentNode.id !== 'root') { |
8f6088c1 DM |
387 | params = { |
388 | "backup-type": data["backup-type"], | |
389 | "backup-id": data["backup-id"], | |
390 | "backup-time": (data['backup-time'].getTime()/1000).toFixed(0), | |
391 | }; | |
392 | } else { | |
393 | params = { | |
394 | "backup-type": data.backup_type, | |
395 | "backup-id": data.backup_id, | |
396 | }; | |
397 | } | |
398 | ||
399 | Proxmox.Utils.API2Request({ | |
400 | params: params, | |
401 | url: `/admin/datastore/${view.datastore}/verify`, | |
402 | method: 'POST', | |
403 | failure: function(response) { | |
404 | Ext.Msg.alert(gettext('Error'), response.htmlStatus); | |
405 | }, | |
406 | success: function(response, options) { | |
407 | Ext.create('Proxmox.window.TaskViewer', { | |
408 | upid: response.result.data, | |
484d439a | 409 | taskDone: () => me.reload(), |
8f6088c1 DM |
410 | }).show(); |
411 | }, | |
412 | }); | |
413 | }, | |
414 | ||
68e2ea99 | 415 | onNotesEdit: function(view, data, isGroup) { |
c9725bb8 TL |
416 | let me = this; |
417 | ||
68e2ea99 SR |
418 | let url = `/admin/datastore/${view.datastore}/`; |
419 | url += isGroup ? 'group-notes' : 'notes'; | |
420 | ||
421 | let params; | |
422 | if (isGroup) { | |
423 | params = { | |
424 | "backup-type": data.backup_type, | |
425 | "backup-id": data.backup_id, | |
426 | }; | |
427 | } else { | |
428 | params = { | |
429 | "backup-type": data["backup-type"], | |
430 | "backup-id": data["backup-id"], | |
431 | "backup-time": (data['backup-time'].getTime()/1000).toFixed(0), | |
432 | }; | |
433 | } | |
434 | ||
c9725bb8 TL |
435 | Ext.create('PBS.window.NotesEdit', { |
436 | url: url, | |
437 | autoShow: true, | |
438 | apiCallDone: () => me.reload(), // FIXME: do something more efficient? | |
68e2ea99 | 439 | extraRequestParams: params, |
c9725bb8 TL |
440 | }); |
441 | }, | |
442 | ||
a904e375 | 443 | forgetGroup: function(data) { |
3e395378 | 444 | let me = this; |
a904e375 | 445 | let view = me.getView(); |
4ff2c9b8 | 446 | |
a904e375 DC |
447 | Ext.create('Proxmox.window.SafeDestroy', { |
448 | url: `/admin/datastore/${view.datastore}/groups`, | |
449 | params: { | |
b32cf6a1 DC |
450 | "backup-type": data.backup_type, |
451 | "backup-id": data.backup_id, | |
a904e375 DC |
452 | }, |
453 | item: { | |
454 | id: data.text, | |
455 | }, | |
456 | autoShow: true, | |
457 | taskName: 'forget-group', | |
458 | listeners: { | |
459 | destroy: () => me.reload(), | |
460 | }, | |
461 | }); | |
462 | }, | |
463 | ||
464 | forgetSnapshot: function(data) { | |
465 | let me = this; | |
466 | let view = me.getView(); | |
b32cf6a1 | 467 | |
3e395378 DC |
468 | Ext.Msg.show({ |
469 | title: gettext('Confirm'), | |
470 | icon: Ext.Msg.WARNING, | |
a904e375 | 471 | message: Ext.String.format(gettext('Are you sure you want to remove snapshot {0}'), `'${data.text}'`), |
3e395378 DC |
472 | buttons: Ext.Msg.YESNO, |
473 | defaultFocus: 'no', | |
474 | callback: function(btn) { | |
475 | if (btn !== 'yes') { | |
476 | return; | |
477 | } | |
4ff2c9b8 | 478 | |
3e395378 | 479 | Proxmox.Utils.API2Request({ |
a904e375 DC |
480 | url: `/admin/datastore/${view.datastore}/snapshots`, |
481 | params: { | |
482 | "backup-type": data["backup-type"], | |
483 | "backup-id": data["backup-id"], | |
484 | "backup-time": (data['backup-time'].getTime()/1000).toFixed(0), | |
485 | }, | |
3e395378 DC |
486 | method: 'DELETE', |
487 | waitMsgTarget: view, | |
488 | failure: function(response, opts) { | |
489 | Ext.Msg.alert(gettext('Error'), response.htmlStatus); | |
490 | }, | |
491 | callback: me.reload.bind(me), | |
492 | }); | |
4ff2c9b8 | 493 | }, |
4ff2c9b8 DM |
494 | }); |
495 | }, | |
496 | ||
a904e375 DC |
497 | onForget: function(view, rI, cI, item, e, rec) { |
498 | let me = this; | |
499 | view = this.getView(); | |
500 | ||
501 | if (!(rec && rec.data)) return; | |
502 | let data = rec.data; | |
503 | if (!view.datastore) return; | |
504 | ||
505 | if (rec.parentNode.id !== 'root') { | |
506 | me.forgetSnapshot(data); | |
507 | } else { | |
508 | me.forgetGroup(data); | |
509 | } | |
510 | }, | |
511 | ||
3e395378 | 512 | downloadFile: function(tV, rI, cI, item, e, rec) { |
98425309 DC |
513 | let me = this; |
514 | let view = me.getView(); | |
515 | ||
98425309 | 516 | if (!(rec && rec.data)) return; |
3e395378 DC |
517 | let data = rec.parentNode.data; |
518 | ||
519 | let file = rec.data.filename; | |
520 | let params = { | |
521 | 'backup-id': data['backup-id'], | |
522 | 'backup-type': data['backup-type'], | |
523 | 'backup-time': (data['backup-time'].getTime()/1000).toFixed(0), | |
524 | 'file-name': file, | |
525 | }; | |
526 | ||
527 | let idx = file.lastIndexOf('.'); | |
528 | let filename = file.slice(0, idx); | |
529 | let atag = document.createElement('a'); | |
530 | params['file-name'] = file; | |
531 | atag.download = filename; | |
c6e07769 TL |
532 | let url = new URL(`/api2/json/admin/datastore/${view.datastore}/download-decoded`, |
533 | window.location.origin); | |
3e395378 DC |
534 | for (const [key, value] of Object.entries(params)) { |
535 | url.searchParams.append(key, value); | |
536 | } | |
537 | atag.href = url.href; | |
538 | atag.click(); | |
8567c0d2 DC |
539 | }, |
540 | ||
3e395378 | 541 | openPxarBrowser: function(tv, rI, Ci, item, e, rec) { |
8567c0d2 DC |
542 | let me = this; |
543 | let view = me.getView(); | |
544 | ||
8567c0d2 | 545 | if (!(rec && rec.data)) return; |
3e395378 | 546 | let data = rec.parentNode.data; |
8567c0d2 DC |
547 | |
548 | let id = data['backup-id']; | |
549 | let time = data['backup-time']; | |
550 | let type = data['backup-type']; | |
551 | let timetext = PBS.Utils.render_datetime_utc(data["backup-time"]); | |
552 | ||
3b032136 | 553 | Ext.create('Proxmox.window.FileBrowser', { |
8567c0d2 | 554 | title: `${type}/${id}/${timetext}`, |
61df02cd TL |
555 | listURL: `/api2/json/admin/datastore/${view.datastore}/catalog`, |
556 | downloadURL: `/api2/json/admin/datastore/${view.datastore}/pxar-file-download`, | |
3b032136 SR |
557 | extraParams: { |
558 | 'backup-id': id, | |
559 | 'backup-time': (time.getTime()/1000).toFixed(0), | |
560 | 'backup-type': type, | |
561 | }, | |
3e395378 | 562 | archive: rec.data.filename, |
8567c0d2 | 563 | }).show(); |
6d55603d DC |
564 | }, |
565 | ||
566 | filter: function(item, value) { | |
567 | if (item.data.text.indexOf(value) !== -1) { | |
568 | return true; | |
569 | } | |
570 | ||
571 | if (item.data.owner && item.data.owner.indexOf(value) !== -1) { | |
572 | return true; | |
573 | } | |
574 | ||
575 | return false; | |
576 | }, | |
577 | ||
578 | search: function(tf, value) { | |
579 | let me = this; | |
580 | let view = me.getView(); | |
581 | let store = view.getStore(); | |
582 | if (!value && value !== 0) { | |
583 | store.clearFilter(); | |
584 | store.getRoot().collapseChildren(true); | |
585 | tf.triggers.clear.setVisible(false); | |
586 | return; | |
587 | } | |
588 | tf.triggers.clear.setVisible(true); | |
589 | if (value.length < 2) return; | |
590 | Proxmox.Utils.setErrorMask(view, true); | |
591 | // we do it a little bit later for the error mask to work | |
592 | setTimeout(function() { | |
593 | store.clearFilter(); | |
594 | store.getRoot().collapseChildren(true); | |
595 | ||
596 | store.beginUpdate(); | |
597 | store.getRoot().cascadeBy({ | |
598 | before: function(item) { | |
c6e07769 | 599 | if (me.filter(item, value)) { |
6d55603d DC |
600 | item.set('matchesFilter', true); |
601 | if (item.parentNode && item.parentNode.id !== 'root') { | |
602 | item.parentNode.childmatches = true; | |
603 | } | |
604 | return false; | |
605 | } | |
606 | return true; | |
607 | }, | |
608 | after: function(item) { | |
609 | if (me.filter(item, value) || item.id === 'root' || item.childmatches) { | |
610 | item.set('matchesFilter', true); | |
611 | if (item.parentNode && item.parentNode.id !== 'root') { | |
612 | item.parentNode.childmatches = true; | |
613 | } | |
614 | if (item.childmatches) { | |
615 | item.expand(); | |
616 | } | |
617 | } else { | |
618 | item.set('matchesFilter', false); | |
619 | } | |
620 | delete item.childmatches; | |
621 | }, | |
622 | }); | |
623 | store.endUpdate(); | |
624 | ||
625 | store.filter((item) => !!item.get('matchesFilter')); | |
626 | Proxmox.Utils.setErrorMask(view, false); | |
627 | }, 10); | |
628 | }, | |
f1baa7f4 TL |
629 | }, |
630 | ||
747446eb DC |
631 | listeners: { |
632 | activate: function() { | |
633 | let me = this; | |
634 | // only load on first activate to not load every tab switch | |
635 | if (!me.firstLoad) { | |
636 | me.getController().reload(); | |
637 | me.firstLoad = true; | |
638 | } | |
639 | }, | |
640 | }, | |
641 | ||
c879e5af TL |
642 | viewConfig: { |
643 | getRowClass: function(record, index) { | |
644 | let verify = record.get('verification'); | |
645 | if (verify && verify.lastFailed) { | |
646 | return 'proxmox-invalid-row'; | |
647 | } | |
c6e07769 | 648 | return null; |
c879e5af TL |
649 | }, |
650 | }, | |
651 | ||
04b0ca8b DC |
652 | columns: [ |
653 | { | |
654 | xtype: 'treecolumn', | |
655 | header: gettext("Backup Group"), | |
656 | dataIndex: 'text', | |
c6e07769 | 657 | flex: 1, |
04b0ca8b | 658 | }, |
c9725bb8 TL |
659 | { |
660 | text: gettext('Comment'), | |
661 | dataIndex: 'comment', | |
662 | flex: 1, | |
663 | renderer: (v, meta, record) => { | |
664 | let data = record.data; | |
68e2ea99 | 665 | if (!data || data.leaf) { |
c9725bb8 TL |
666 | return ''; |
667 | } | |
668 | if (v === undefined || v === null) { | |
669 | v = ''; | |
670 | } | |
671 | v = Ext.String.htmlEncode(v); | |
ba2679c9 | 672 | let icon = 'x-action-col-icon fa fa-fw fa-pencil pointer'; |
c9725bb8 TL |
673 | |
674 | return `<span class="snapshot-comment-column">${v}</span> | |
ba2679c9 | 675 | <i data-qtip="${gettext('Edit')}" style="float: right; margin: 0px;" class="${icon}"></i>`; |
c9725bb8 TL |
676 | }, |
677 | listeners: { | |
678 | afterrender: function(component) { | |
679 | // a bit of a hack, but relatively easy, cheap and works out well. | |
680 | // more efficient to use one handler for the whole column than for each icon | |
681 | component.on('click', function(tree, cell, rowI, colI, e, rec) { | |
682 | let el = e.target; | |
683 | if (el.tagName !== "I" || !el.classList.contains("fa-pencil")) { | |
684 | return; | |
685 | } | |
686 | let view = tree.up(); | |
687 | let controller = view.controller; | |
68e2ea99 | 688 | controller.onNotesEdit(view, rec.data, rec.parentNode.id === 'root'); |
c9725bb8 TL |
689 | }); |
690 | }, | |
691 | dblclick: function(tree, el, row, col, ev, rec) { | |
692 | let data = rec.data || {}; | |
68e2ea99 | 693 | if (data.leaf) { |
c9725bb8 TL |
694 | return; |
695 | } | |
696 | let view = tree.up(); | |
697 | let controller = view.controller; | |
68e2ea99 | 698 | controller.onNotesEdit(view, rec.data, rec.parentNode.id === 'root'); |
c9725bb8 TL |
699 | }, |
700 | }, | |
701 | }, | |
3e395378 DC |
702 | { |
703 | header: gettext('Actions'), | |
704 | xtype: 'actioncolumn', | |
705 | dataIndex: 'text', | |
e373dcc5 | 706 | width: 140, |
3e395378 DC |
707 | items: [ |
708 | { | |
709 | handler: 'onVerify', | |
40492a56 | 710 | getTip: (v, m, rec) => Ext.String.format(gettext("Verify '{0}'"), v), |
db67e4fe | 711 | getClass: (v, m, rec) => rec.data.leaf ? 'pmx-hidden' : 'pve-icon-verify-lettering', |
4e0faf5e | 712 | isActionDisabled: (v, r, c, i, rec) => !!rec.data.leaf, |
61db0851 DW |
713 | }, |
714 | { | |
715 | handler: 'onChangeOwner', | |
716 | getClass: (v, m, rec) => rec.parentNode.id ==='root' ? 'fa fa-user' : 'pmx-hidden', | |
717 | getTip: (v, m, rec) => Ext.String.format(gettext("Change owner of '{0}'"), v), | |
4e0faf5e | 718 | isActionDisabled: (v, r, c, i, rec) => rec.parentNode.id !=='root', |
61db0851 | 719 | }, |
3e395378 DC |
720 | { |
721 | handler: 'onPrune', | |
40492a56 | 722 | getTip: (v, m, rec) => Ext.String.format(gettext("Prune '{0}'"), v), |
3e395378 | 723 | getClass: (v, m, rec) => rec.parentNode.id ==='root' ? 'fa fa-scissors' : 'pmx-hidden', |
4e0faf5e | 724 | isActionDisabled: (v, r, c, i, rec) => rec.parentNode.id !=='root', |
3e395378 DC |
725 | }, |
726 | { | |
727 | handler: 'onForget', | |
7ba99fef DC |
728 | getTip: (v, m, rec) => rec.parentNode.id !=='root' |
729 | ? Ext.String.format(gettext("Permanently forget snapshot '{0}'"), v) | |
730 | : Ext.String.format(gettext("Permanently forget group '{0}'"), v), | |
b32cf6a1 | 731 | getClass: (v, m, rec) => !rec.data.leaf ? 'fa critical fa-trash-o' : 'pmx-hidden', |
4e0faf5e | 732 | isActionDisabled: (v, r, c, i, rec) => !!rec.data.leaf, |
3e395378 DC |
733 | }, |
734 | { | |
735 | handler: 'downloadFile', | |
40492a56 | 736 | getTip: (v, m, rec) => Ext.String.format(gettext("Download '{0}'"), v), |
3e395378 | 737 | getClass: (v, m, rec) => rec.data.leaf && rec.data.filename ? 'fa fa-download' : 'pmx-hidden', |
4e0faf5e | 738 | isActionDisabled: (v, r, c, i, rec) => !rec.data.leaf || !rec.data.filename || rec.data['crypt-mode'] > 2, |
3e395378 DC |
739 | }, |
740 | { | |
741 | handler: 'openPxarBrowser', | |
742 | tooltip: gettext('Browse'), | |
743 | getClass: (v, m, rec) => { | |
744 | let data = rec.data; | |
745 | if (data.leaf && data.filename && data.filename.endsWith('pxar.didx')) { | |
746 | return 'fa fa-folder-open-o'; | |
747 | } | |
748 | return 'pmx-hidden'; | |
749 | }, | |
4e0faf5e | 750 | isActionDisabled: (v, r, c, i, rec) => { |
3e395378 DC |
751 | let data = rec.data; |
752 | return !(data.leaf && | |
753 | data.filename && | |
754 | data.filename.endsWith('pxar.didx') && | |
c96b0de4 | 755 | data['crypt-mode'] < 3); |
c6e07769 | 756 | }, |
3e395378 | 757 | }, |
c6e07769 | 758 | ], |
3e395378 | 759 | }, |
04b0ca8b DC |
760 | { |
761 | xtype: 'datecolumn', | |
762 | header: gettext('Backup Time'), | |
763 | sortable: true, | |
764 | dataIndex: 'backup-time', | |
765 | format: 'Y-m-d H:i:s', | |
c6e07769 | 766 | width: 150, |
04b0ca8b DC |
767 | }, |
768 | { | |
769 | header: gettext("Size"), | |
770 | sortable: true, | |
771 | dataIndex: 'size', | |
a7a5f56d | 772 | renderer: (v, meta, record) => { |
3e395378 DC |
773 | if (record.data.text === 'client.log.blob' && v === undefined) { |
774 | return ''; | |
775 | } | |
a7a5f56d TL |
776 | if (v === undefined || v === null) { |
777 | meta.tdCls = "x-grid-row-loading"; | |
778 | return ''; | |
779 | } | |
780 | return Proxmox.Utils.format_size(v); | |
781 | }, | |
04b0ca8b DC |
782 | }, |
783 | { | |
784 | xtype: 'numbercolumn', | |
785 | format: '0', | |
786 | header: gettext("Count"), | |
787 | sortable: true, | |
46388e6a TL |
788 | width: 75, |
789 | align: 'right', | |
04b0ca8b DC |
790 | dataIndex: 'count', |
791 | }, | |
792 | { | |
793 | header: gettext("Owner"), | |
794 | sortable: true, | |
795 | dataIndex: 'owner', | |
796 | }, | |
e005f953 DC |
797 | { |
798 | header: gettext('Encrypted'), | |
2774566b | 799 | dataIndex: 'crypt-mode', |
3e395378 | 800 | renderer: (v, meta, record) => { |
71282dd9 SR |
801 | if (record.data.size === undefined || record.data.size === null) { |
802 | return ''; | |
803 | } | |
3e395378 DC |
804 | if (v === -1) { |
805 | return ''; | |
806 | } | |
807 | let iconCls = PBS.Utils.cryptIconCls[v] || ''; | |
808 | let iconTxt = ""; | |
809 | if (iconCls) { | |
810 | iconTxt = `<i class="fa fa-fw fa-${iconCls}"></i> `; | |
811 | } | |
6982a547 FG |
812 | let tip; |
813 | if (v !== PBS.Utils.cryptmap.indexOf('none') && record.data.fingerprint !== undefined) { | |
814 | tip = "Key: " + PBS.Utils.renderKeyID(record.data.fingerprint); | |
815 | } | |
816 | let txt = (iconTxt + PBS.Utils.cryptText[v]) || Proxmox.Utils.unknownText; | |
817 | if (record.parentNode.id === 'root' || tip === undefined) { | |
818 | return txt; | |
819 | } else { | |
820 | return `<span data-qtip="${tip}">${txt}</span>`; | |
821 | } | |
c6e07769 | 822 | }, |
04b0ca8b | 823 | }, |
7b212c1f TL |
824 | { |
825 | header: gettext('Verify State'), | |
826 | sortable: true, | |
827 | dataIndex: 'verification', | |
423df9b1 | 828 | width: 120, |
e9558f29 DC |
829 | sorter: (arec, brec) => { |
830 | let a = arec.data.verification || { ok: 0, outdated: 0, failed: 0 }; | |
831 | let b = brec.data.verification || { ok: 0, outdated: 0, failed: 0 }; | |
832 | if (a.failed === b.failed) { | |
833 | if (a.none === b.none) { | |
834 | if (a.outdated === b.outdated) { | |
835 | return b.ok - a.ok; | |
836 | } else { | |
837 | return b.outdated - a.outdated; | |
838 | } | |
839 | } else { | |
840 | return b.none - a.none; | |
841 | } | |
842 | } else { | |
843 | return b.failed - a.failed; | |
844 | } | |
845 | }, | |
7b212c1f | 846 | renderer: (v, meta, record) => { |
423df9b1 TL |
847 | let i = (cls, txt) => `<i class="fa fa-fw fa-${cls}"></i> ${txt}`; |
848 | if (v === undefined || v === null) { | |
849 | return record.data.leaf ? '' : i('question-circle-o warning', gettext('None')); | |
7b212c1f | 850 | } |
423df9b1 | 851 | let tip, iconCls, txt; |
7b212c1f | 852 | if (record.parentNode.id === 'root') { |
423df9b1 TL |
853 | if (v.failed === 0) { |
854 | if (v.none === 0) { | |
855 | if (v.outdated > 0) { | |
856 | tip = 'All OK, but some snapshots were not verified in last 30 days'; | |
857 | iconCls = 'check warning'; | |
858 | txt = gettext('All OK (old)'); | |
859 | } else { | |
860 | tip = 'All snapshots verified at least once in last 30 days'; | |
861 | iconCls = 'check good'; | |
862 | txt = gettext('All OK'); | |
863 | } | |
864 | } else if (v.ok === 0) { | |
865 | tip = `${v.none} not verified yet`; | |
866 | iconCls = 'question-circle-o warning'; | |
867 | txt = gettext('None'); | |
868 | } else { | |
869 | tip = `${v.ok} OK, ${v.none} not verified yet`; | |
870 | iconCls = 'check faded'; | |
871 | txt = `${v.ok} OK`; | |
872 | } | |
873 | } else { | |
874 | tip = `${v.ok} OK, ${v.failed} failed, ${v.none} not verified yet`; | |
875 | iconCls = 'times critical'; | |
876 | txt = v.ok === 0 && v.none === 0 | |
877 | ? gettext('All failed') | |
878 | : `${v.failed} failed`; | |
879 | } | |
880 | } else if (!v.state) { | |
881 | return record.data.leaf ? '' : gettext('None'); | |
882 | } else { | |
883 | let verify_time = Proxmox.Utils.render_timestamp(v.lastTime); | |
884 | tip = `Last verify task started on ${verify_time}`; | |
885 | txt = v.state; | |
886 | iconCls = 'times critical'; | |
887 | if (v.state === 'ok') { | |
888 | iconCls = 'check good'; | |
889 | let now = Date.now() / 1000; | |
890 | if (now - v.lastTime > 30 * 24 * 60 * 60) { | |
891 | tip = `Last verify task over 30 days ago: ${verify_time}`; | |
892 | iconCls = 'check warning'; | |
893 | } | |
894 | } | |
7b212c1f TL |
895 | } |
896 | return `<span data-qtip="${tip}"> | |
423df9b1 | 897 | <i class="fa fa-fw fa-${iconCls}"></i> ${txt} |
7b212c1f TL |
898 | </span>`; |
899 | }, | |
900 | listeners: { | |
901 | dblclick: function(view, el, row, col, ev, rec) { | |
902 | let data = rec.data || {}; | |
903 | let verify = data.verification; | |
904 | if (verify && verify.upid && rec.parentNode.id !== 'root') { | |
905 | let win = Ext.create('Proxmox.window.TaskViewer', { | |
906 | upid: verify.upid, | |
907 | }); | |
908 | win.show(); | |
909 | } | |
910 | }, | |
911 | }, | |
912 | }, | |
04b0ca8b | 913 | ], |
b1127fd0 | 914 | |
04b0ca8b DC |
915 | tbar: [ |
916 | { | |
917 | text: gettext('Reload'), | |
918 | iconCls: 'fa fa-refresh', | |
919 | handler: 'reload', | |
920 | }, | |
cd92fd73 DC |
921 | '-', |
922 | { | |
923 | xtype: 'proxmoxButton', | |
924 | text: gettext('Verify All'), | |
8d6b6a04 | 925 | confirmMsg: gettext('Do you want to verify all snapshots now?'), |
cd92fd73 DC |
926 | handler: 'verifyAll', |
927 | }, | |
afbf2e10 DC |
928 | { |
929 | xtype: 'proxmoxButton', | |
930 | text: gettext('Prune All'), | |
931 | handler: 'pruneAll', | |
932 | }, | |
6d55603d DC |
933 | '->', |
934 | { | |
935 | xtype: 'tbtext', | |
936 | html: gettext('Search'), | |
937 | }, | |
938 | { | |
939 | xtype: 'textfield', | |
940 | reference: 'searchbox', | |
c3b1da9e | 941 | emptyText: gettext('group, date or owner'), |
6d55603d DC |
942 | triggers: { |
943 | clear: { | |
944 | cls: 'pmx-clear-trigger', | |
945 | weight: -1, | |
946 | hidden: true, | |
947 | handler: function() { | |
948 | this.triggers.clear.setVisible(false); | |
949 | this.setValue(''); | |
950 | }, | |
c6e07769 | 951 | }, |
6d55603d DC |
952 | }, |
953 | listeners: { | |
954 | change: { | |
955 | fn: 'search', | |
956 | buffer: 500, | |
957 | }, | |
958 | }, | |
c6e07769 | 959 | }, |
04b0ca8b | 960 | ], |
ca23a97f | 961 | }); |