]>
git.proxmox.com Git - proxmox-backup.git/blob - www/DataStoreContent.js
1 Ext
.define('pbs-data-store-snapshots', {
2 extend
: 'Ext.data.Model',
9 dateFormat
: 'timestamp'
14 { name
: 'size', type
: 'int', allowNull
: true, },
18 calculate: function(data
) {
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']);
32 crypt
[file
['crypt-mode']]++;
37 return PBS
.Utils
.calculateCryptMode(crypt
);
41 name
: 'matchesFilter',
48 Ext
.define('PBS.DataStoreContent', {
49 extend
: 'Ext.tree.Panel',
50 alias
: 'widget.pbsDataStoreContent',
54 title
: gettext('Content'),
57 xclass
: 'Ext.app.ViewController',
59 init: function(view
) {
60 if (!view
.datastore
) {
61 throw "no datastore specified";
64 this.store
= Ext
.create('Ext.data.Store', {
65 model
: 'pbs-data-store-snapshots',
66 groupField
: 'backup-group',
68 this.store
.on('load', this.onLoad
, this);
70 view
.getStore().setSorters([
75 Proxmox
.Utils
.monStoreErrors(view
, this.store
);
76 this.reload(); // initial load
80 let view
= this.getView();
82 if (!view
.store
|| !this.store
) {
83 console
.warn('cannot reload, no store(s)');
87 let url
= `/api2/json/admin/datastore/${view.datastore}/snapshots`;
90 timeout
: 300*1000, // 5 minutes, we should make that api call faster
97 getRecordGroups: function(records
) {
100 for (const item
of records
) {
101 var btype
= item
.data
["backup-type"];
102 let group
= btype
+ "/" + item
.data
["backup-id"];
104 if (groups
[group
] !== undefined) {
109 if (btype
=== 'vm') {
111 } else if (btype
=== 'ct') {
113 } else if (btype
=== 'host') {
116 console
.warn(`got unknown backup-type '${btype}'`);
117 continue; // FIXME: auto render? what do?
123 iconCls
: "fa " + cls
,
125 backup_type
: item
.data
["backup-type"],
126 backup_id
: item
.data
["backup-id"],
134 onLoad: function(store
, records
, success
, operation
) {
136 let view
= this.getView();
139 Proxmox
.Utils
.setErrorMask(view
, Proxmox
.Utils
.getResponseErrorMessage(operation
.getError()));
143 let groups
= this.getRecordGroups(records
);
148 view
.getSelection().some(function(item
) {
149 let id
= item
.data
.text
;
150 if (item
.data
.leaf
) {
151 id
= item
.parentNode
.data
.text
+ id
;
157 view
.getRootNode().cascadeBy({
159 if (item
.isExpanded() && !item
.data
.leaf
) {
160 let id
= item
.data
.text
;
169 for (const item
of records
) {
170 let group
= item
.data
["backup-type"] + "/" + item
.data
["backup-id"];
171 let children
= groups
[group
].children
;
173 let data
= item
.data
;
175 data
.text
= group
+ '/' + PBS
.Utils
.render_datetime_utc(data
["backup-time"]);
177 data
.cls
= 'no-leaf-icons';
178 data
.matchesFilter
= true;
180 data
.expanded
= !!expanded
[data
.text
];
183 for (const file
of data
.files
) {
184 file
.text
= file
.filename
;
185 file
['crypt-mode'] = PBS
.Utils
.cryptmap
.indexOf(file
['crypt-mode']);
187 file
.matchesFilter
= true;
189 data
.children
.push(file
);
196 for (const [name
, group
] of Object
.entries(groups
)) {
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 verify
.lastFailed
= item
.verification
&& item
.verification
.state
!== 'ok';
214 if (item
.verification
&&
215 (!group
.verification
|| group
.verification
.state
!== 'failed')) {
216 group
.verification
= item
.verification
;
220 group
.count
= group
.children
.length
;
221 group
.matchesFilter
= true;
222 crypt
.count
= group
.count
;
223 group
['crypt-mode'] = PBS
.Utils
.calculateCryptMode(crypt
);
224 group
.expanded
= !!expanded
[name
];
225 children
.push(group
);
233 if (selected
!== undefined) {
234 let selection
= view
.getRootNode().findChildBy(function(item
) {
235 let id
= item
.data
.text
;
236 if (item
.data
.leaf
) {
237 id
= item
.parentNode
.data
.text
+ id
;
239 return selected
=== id
;
242 view
.setSelection(selection
);
243 view
.getView().focusRow(selection
);
247 Proxmox
.Utils
.setErrorMask(view
, false);
248 if (view
.getStore().getFilters().length
> 0) {
249 let searchBox
= me
.lookup("searchbox");
250 let searchvalue
= searchBox
.getValue();;
251 me
.search(searchBox
, searchvalue
);
255 onPrune: function(view
, rI
, cI
, item
, e
, rec
) {
256 var view
= this.getView();
258 if (!(rec
&& rec
.data
)) return;
260 if (rec
.parentNode
.id
!== 'root') return;
262 if (!view
.datastore
) return;
264 let win
= Ext
.create('PBS.DataStorePrune', {
265 datastore
: view
.datastore
,
266 backup_type
: data
.backup_type
,
267 backup_id
: data
.backup_id
,
269 win
.on('destroy', this.reload
, this);
273 onVerify: function(view
, rI
, cI
, item
, e
, rec
) {
277 if (!view
.datastore
) return;
279 if (!(rec
&& rec
.data
)) return;
284 if (rec
.parentNode
.id
!== 'root') {
286 "backup-type": data
["backup-type"],
287 "backup-id": data
["backup-id"],
288 "backup-time": (data
['backup-time'].getTime()/1000).toFixed(0),
292 "backup-type": data
.backup_type
,
293 "backup-id": data
.backup_id
,
297 Proxmox
.Utils
.API2Request({
299 url
: `/admin/datastore/${view.datastore}/verify`,
301 failure: function(response
) {
302 Ext
.Msg
.alert(gettext('Error'), response
.htmlStatus
);
304 success: function(response
, options
) {
305 Ext
.create('Proxmox.window.TaskViewer', {
306 upid
: response
.result
.data
,
307 taskDone
: () => me
.reload(),
313 onForget: function(view
, rI
, cI
, item
, e
, rec
) {
315 var view
= this.getView();
317 if (!(rec
&& rec
.data
)) return;
319 if (!view
.datastore
) return;
322 title
: gettext('Confirm'),
323 icon
: Ext
.Msg
.WARNING
,
324 message
: Ext
.String
.format(gettext('Are you sure you want to remove snapshot {0}'), `'${data.text}'`),
325 buttons
: Ext
.Msg
.YESNO
,
327 callback: function(btn
) {
332 Proxmox
.Utils
.API2Request({
334 "backup-type": data
["backup-type"],
335 "backup-id": data
["backup-id"],
336 "backup-time": (data
['backup-time'].getTime()/1000).toFixed(0),
338 url
: `/admin/datastore/${view.datastore}/snapshots`,
341 failure: function(response
, opts
) {
342 Ext
.Msg
.alert(gettext('Error'), response
.htmlStatus
);
344 callback
: me
.reload
.bind(me
),
350 downloadFile: function(tV
, rI
, cI
, item
, e
, rec
) {
352 let view
= me
.getView();
354 if (!(rec
&& rec
.data
)) return;
355 let data
= rec
.parentNode
.data
;
357 let file
= rec
.data
.filename
;
359 'backup-id': data
['backup-id'],
360 'backup-type': data
['backup-type'],
361 'backup-time': (data
['backup-time'].getTime()/1000).toFixed(0),
365 let idx
= file
.lastIndexOf('.');
366 let filename
= file
.slice(0, idx
);
367 let atag
= document
.createElement('a');
368 params
['file-name'] = file
;
369 atag
.download
= filename
;
370 let url
= new URL(`/api2/json/admin/datastore/${view.datastore}/download-decoded`, window
.location
.origin
);
371 for (const [key
, value
] of Object
.entries(params
)) {
372 url
.searchParams
.append(key
, value
);
374 atag
.href
= url
.href
;
378 openPxarBrowser: function(tv
, rI
, Ci
, item
, e
, rec
) {
380 let view
= me
.getView();
382 if (!(rec
&& rec
.data
)) return;
383 let data
= rec
.parentNode
.data
;
385 let id
= data
['backup-id'];
386 let time
= data
['backup-time'];
387 let type
= data
['backup-type'];
388 let timetext
= PBS
.Utils
.render_datetime_utc(data
["backup-time"]);
390 Ext
.create('PBS.window.FileBrowser', {
391 title
: `${type}/${id}/${timetext}`,
392 datastore
: view
.datastore
,
394 'backup-time': (time
.getTime()/1000).toFixed(0),
396 archive
: rec
.data
.filename
,
400 filter: function(item
, value
) {
401 if (item
.data
.text
.indexOf(value
) !== -1) {
405 if (item
.data
.owner
&& item
.data
.owner
.indexOf(value
) !== -1) {
412 search: function(tf
, value
) {
414 let view
= me
.getView();
415 let store
= view
.getStore();
416 if (!value
&& value
!== 0) {
418 store
.getRoot().collapseChildren(true);
419 tf
.triggers
.clear
.setVisible(false);
422 tf
.triggers
.clear
.setVisible(true);
423 if (value
.length
< 2) return;
424 Proxmox
.Utils
.setErrorMask(view
, true);
425 // we do it a little bit later for the error mask to work
426 setTimeout(function() {
428 store
.getRoot().collapseChildren(true);
431 store
.getRoot().cascadeBy({
432 before: function(item
) {
433 if(me
.filter(item
, value
)) {
434 item
.set('matchesFilter', true);
435 if (item
.parentNode
&& item
.parentNode
.id
!== 'root') {
436 item
.parentNode
.childmatches
= true;
442 after: function(item
) {
443 if (me
.filter(item
, value
) || item
.id
=== 'root' || item
.childmatches
) {
444 item
.set('matchesFilter', true);
445 if (item
.parentNode
&& item
.parentNode
.id
!== 'root') {
446 item
.parentNode
.childmatches
= true;
448 if (item
.childmatches
) {
452 item
.set('matchesFilter', false);
454 delete item
.childmatches
;
459 store
.filter((item
) => !!item
.get('matchesFilter'));
460 Proxmox
.Utils
.setErrorMask(view
, false);
466 getRowClass: function(record
, index
) {
467 let verify
= record
.get('verification');
468 if (verify
&& verify
.lastFailed
) {
469 return 'proxmox-invalid-row';
477 header
: gettext("Backup Group"),
482 header
: gettext('Actions'),
483 xtype
: 'actioncolumn',
488 tooltip
: gettext('Verify'),
489 getClass
: (v
, m
, rec
) => rec
.data
.leaf
? 'pmx-hidden' : 'fa fa-search',
490 isDisabled
: (v
, r
, c
, i
, rec
) => !!rec
.data
.leaf
,
494 tooltip
: gettext('Prune'),
495 getClass
: (v
, m
, rec
) => rec
.parentNode
.id
==='root' ? 'fa fa-scissors' : 'pmx-hidden',
496 isDisabled
: (v
, r
, c
, i
, rec
) => rec
.parentNode
.id
!=='root',
500 tooltip
: gettext('Forget Snapshot'),
501 getClass
: (v
, m
, rec
) => !rec
.data
.leaf
&& rec
.parentNode
.id
!== 'root' ? 'fa critical fa-trash-o' : 'pmx-hidden',
502 isDisabled
: (v
, r
, c
, i
, rec
) => rec
.data
.leaf
|| rec
.parentNode
.id
=== 'root',
505 handler
: 'downloadFile',
506 tooltip
: gettext('Download'),
507 getClass
: (v
, m
, rec
) => rec
.data
.leaf
&& rec
.data
.filename
? 'fa fa-download' : 'pmx-hidden',
508 isDisabled
: (v
, r
, c
, i
, rec
) => !rec
.data
.leaf
|| !rec
.data
.filename
|| rec
.data
['crypt-mode'] > 2,
511 handler
: 'openPxarBrowser',
512 tooltip
: gettext('Browse'),
513 getClass
: (v
, m
, rec
) => {
515 if (data
.leaf
&& data
.filename
&& data
.filename
.endsWith('pxar.didx')) {
516 return 'fa fa-folder-open-o';
520 isDisabled
: (v
, r
, c
, i
, rec
) => {
522 return !(data
.leaf
&&
524 data
.filename
.endsWith('pxar.didx') &&
525 data
['crypt-mode'] < 3);
532 header
: gettext('Backup Time'),
534 dataIndex
: 'backup-time',
535 format
: 'Y-m-d H:i:s',
539 header
: gettext("Size"),
542 renderer
: (v
, meta
, record
) => {
543 if (record
.data
.text
=== 'client.log.blob' && v
=== undefined) {
546 if (v
=== undefined || v
=== null) {
547 meta
.tdCls
= "x-grid-row-loading";
550 return Proxmox
.Utils
.format_size(v
);
554 xtype
: 'numbercolumn',
556 header
: gettext("Count"),
563 header
: gettext("Owner"),
568 header
: gettext('Encrypted'),
569 dataIndex
: 'crypt-mode',
570 renderer
: (v
, meta
, record
) => {
571 if (record
.data
.size
=== undefined || record
.data
.size
=== null) {
577 let iconCls
= PBS
.Utils
.cryptIconCls
[v
] || '';
580 iconTxt
= `<i class="fa fa-fw fa-${iconCls}"></i> `;
582 return (iconTxt
+ PBS
.Utils
.cryptText
[v
]) || Proxmox
.Utils
.unknownText
586 header
: gettext('Verify State'),
588 dataIndex
: 'verification',
589 renderer
: (v
, meta
, record
) => {
590 if (v
=== undefined || v
=== null || !v
.state
) {
591 //meta.tdCls = "x-grid-row-loading";
592 return record
.data
.leaf
? '' : gettext('None');
594 let task
= Proxmox
.Utils
.parse_task_upid(v
.upid
);
595 let verify_time
= Proxmox
.Utils
.render_timestamp(task
.starttime
);
596 let iconCls
= v
.state
=== 'ok' ? 'check good' : 'times critical';
597 let tip
= `Verify task started on ${verify_time}`;
598 if (record
.parentNode
.id
=== 'root') {
599 tip
= v
.state
=== 'ok'
600 ? 'All verification OK in backup group'
601 : 'At least one failed verification in backup group!';
603 return `<span data-qtip="${tip}">
604 <i class="fa fa-fw fa-${iconCls}"></i> ${v.state}
608 dblclick: function(view
, el
, row
, col
, ev
, rec
) {
609 let data
= rec
.data
|| {};
610 let verify
= data
.verification
;
611 if (verify
&& verify
.upid
&& rec
.parentNode
.id
!== 'root') {
612 let win
= Ext
.create('Proxmox.window.TaskViewer', {
624 text
: gettext('Reload'),
625 iconCls
: 'fa fa-refresh',
631 html
: gettext('Search'),
635 reference
: 'searchbox',
636 emptyText
: gettext('group, date or owner'),
639 cls
: 'pmx-clear-trigger',
642 handler: function() {
643 this.triggers
.clear
.setVisible(false);