]>
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
) {
26 data
.files
.forEach(file
=> {
27 if (file
.filename
=== 'index.json.blob') return; // is never encrypted
28 let mode
= PBS
.Utils
.cryptmap
.indexOf(file
['crypt-mode']);
30 crypt
[file
['crypt-mode']]++;
35 return PBS
.Utils
.calculateCryptMode(crypt
);
39 name
: 'matchesFilter',
46 Ext
.define('PBS.DataStoreContent', {
47 extend
: 'Ext.tree.Panel',
48 alias
: 'widget.pbsDataStoreContent',
52 title
: gettext('Content'),
55 xclass
: 'Ext.app.ViewController',
57 init: function(view
) {
58 if (!view
.datastore
) {
59 throw "no datastore specified";
62 this.store
= Ext
.create('Ext.data.Store', {
63 model
: 'pbs-data-store-snapshots',
64 groupField
: 'backup-group',
66 this.store
.on('load', this.onLoad
, this);
68 view
.getStore().setSorters([
73 Proxmox
.Utils
.monStoreErrors(view
, this.store
);
74 this.reload(); // initial load
78 let view
= this.getView();
80 if (!view
.store
|| !this.store
) {
81 console
.warn('cannot reload, no store(s)');
85 let url
= `/api2/json/admin/datastore/${view.datastore}/snapshots`;
88 timeout
: 300*1000, // 5 minutes, we should make that api call faster
95 getRecordGroups: function(records
) {
98 for (const item
of records
) {
99 var btype
= item
.data
["backup-type"];
100 let group
= btype
+ "/" + item
.data
["backup-id"];
102 if (groups
[group
] !== undefined) {
107 if (btype
=== 'vm') {
109 } else if (btype
=== 'ct') {
111 } else if (btype
=== 'host') {
114 console
.warn(`got unknown backup-type '${btype}'`);
115 continue; // FIXME: auto render? what do?
121 iconCls
: "fa " + cls
,
123 backup_type
: item
.data
["backup-type"],
124 backup_id
: item
.data
["backup-id"],
132 onLoad: function(store
, records
, success
, operation
) {
134 let view
= this.getView();
137 Proxmox
.Utils
.setErrorMask(view
, Proxmox
.Utils
.getResponseErrorMessage(operation
.getError()));
141 let groups
= this.getRecordGroups(records
);
146 view
.getSelection().some(function(item
) {
147 let id
= item
.data
.text
;
148 if (item
.data
.leaf
) {
149 id
= item
.parentNode
.data
.text
+ id
;
155 view
.getRootNode().cascadeBy({
157 if (item
.isExpanded() && !item
.data
.leaf
) {
158 let id
= item
.data
.text
;
167 for (const item
of records
) {
168 let group
= item
.data
["backup-type"] + "/" + item
.data
["backup-id"];
169 let children
= groups
[group
].children
;
171 let data
= item
.data
;
173 data
.text
= group
+ '/' + PBS
.Utils
.render_datetime_utc(data
["backup-time"]);
175 data
.cls
= 'no-leaf-icons';
176 data
.matchesFilter
= true;
178 data
.expanded
= !!expanded
[data
.text
];
181 for (const file
of data
.files
) {
182 file
.text
= file
.filename
;
183 file
['crypt-mode'] = PBS
.Utils
.cryptmap
.indexOf(file
['crypt-mode']);
185 file
.matchesFilter
= true;
187 data
.children
.push(file
);
193 let nowSeconds
= Date
.now() / 1000;
195 for (const [name
, group
] of Object
.entries(groups
)) {
209 for (let item
of group
.children
) {
210 crypt
[PBS
.Utils
.cryptmap
[item
['crypt-mode']]]++;
211 if (item
["backup-time"] > last_backup
&& item
.size
!== null) {
212 last_backup
= item
["backup-time"];
213 group
["backup-time"] = last_backup
;
214 group
.files
= item
.files
;
215 group
.size
= item
.size
;
216 group
.owner
= item
.owner
;
217 verify
.lastFailed
= item
.verification
&& item
.verification
.state
!== 'ok';
219 if (!item
.verification
) {
222 if (item
.verification
.state
=== 'ok') {
227 let task
= Proxmox
.Utils
.parse_task_upid(item
.verification
.upid
);
228 item
.verification
.lastTime
= task
.starttime
;
229 if (nowSeconds
- task
.starttime
> 30 * 24 * 60 * 60) {
234 group
.verification
= verify
;
235 group
.count
= group
.children
.length
;
236 group
.matchesFilter
= true;
237 crypt
.count
= group
.count
;
238 group
['crypt-mode'] = PBS
.Utils
.calculateCryptMode(crypt
);
239 group
.expanded
= !!expanded
[name
];
240 children
.push(group
);
248 if (selected
!== undefined) {
249 let selection
= view
.getRootNode().findChildBy(function(item
) {
250 let id
= item
.data
.text
;
251 if (item
.data
.leaf
) {
252 id
= item
.parentNode
.data
.text
+ id
;
254 return selected
=== id
;
257 view
.setSelection(selection
);
258 view
.getView().focusRow(selection
);
262 Proxmox
.Utils
.setErrorMask(view
, false);
263 if (view
.getStore().getFilters().length
> 0) {
264 let searchBox
= me
.lookup("searchbox");
265 let searchvalue
= searchBox
.getValue();
266 me
.search(searchBox
, searchvalue
);
270 onPrune: function(view
, rI
, cI
, item
, e
, rec
) {
271 view
= this.getView();
273 if (!(rec
&& rec
.data
)) return;
275 if (rec
.parentNode
.id
!== 'root') return;
277 if (!view
.datastore
) return;
279 let win
= Ext
.create('PBS.DataStorePrune', {
280 datastore
: view
.datastore
,
281 backup_type
: data
.backup_type
,
282 backup_id
: data
.backup_id
,
284 win
.on('destroy', this.reload
, this);
288 verifyAll: function() {
289 var view
= this.getView();
291 Proxmox
.Utils
.API2Request({
292 url
: `/admin/datastore/${view.datastore}/verify`,
294 failure: function(response
) {
295 Ext
.Msg
.alert(gettext('Error'), response
.htmlStatus
);
297 success: function(response
, options
) {
298 Ext
.create('Proxmox.window.TaskViewer', {
299 upid
: response
.result
.data
,
305 onVerify: function(view
, rI
, cI
, item
, e
, rec
) {
309 if (!view
.datastore
) return;
311 if (!(rec
&& rec
.data
)) return;
316 if (rec
.parentNode
.id
!== 'root') {
318 "backup-type": data
["backup-type"],
319 "backup-id": data
["backup-id"],
320 "backup-time": (data
['backup-time'].getTime()/1000).toFixed(0),
324 "backup-type": data
.backup_type
,
325 "backup-id": data
.backup_id
,
329 Proxmox
.Utils
.API2Request({
331 url
: `/admin/datastore/${view.datastore}/verify`,
333 failure: function(response
) {
334 Ext
.Msg
.alert(gettext('Error'), response
.htmlStatus
);
336 success: function(response
, options
) {
337 Ext
.create('Proxmox.window.TaskViewer', {
338 upid
: response
.result
.data
,
339 taskDone
: () => me
.reload(),
345 onForget: function(view
, rI
, cI
, item
, e
, rec
) {
347 view
= this.getView();
349 if (!(rec
&& rec
.data
)) return;
351 if (!view
.datastore
) return;
354 title
: gettext('Confirm'),
355 icon
: Ext
.Msg
.WARNING
,
356 message
: Ext
.String
.format(gettext('Are you sure you want to remove snapshot {0}'), `'${data.text}'`),
357 buttons
: Ext
.Msg
.YESNO
,
359 callback: function(btn
) {
364 Proxmox
.Utils
.API2Request({
366 "backup-type": data
["backup-type"],
367 "backup-id": data
["backup-id"],
368 "backup-time": (data
['backup-time'].getTime()/1000).toFixed(0),
370 url
: `/admin/datastore/${view.datastore}/snapshots`,
373 failure: function(response
, opts
) {
374 Ext
.Msg
.alert(gettext('Error'), response
.htmlStatus
);
376 callback
: me
.reload
.bind(me
),
382 downloadFile: function(tV
, rI
, cI
, item
, e
, rec
) {
384 let view
= me
.getView();
386 if (!(rec
&& rec
.data
)) return;
387 let data
= rec
.parentNode
.data
;
389 let file
= rec
.data
.filename
;
391 'backup-id': data
['backup-id'],
392 'backup-type': data
['backup-type'],
393 'backup-time': (data
['backup-time'].getTime()/1000).toFixed(0),
397 let idx
= file
.lastIndexOf('.');
398 let filename
= file
.slice(0, idx
);
399 let atag
= document
.createElement('a');
400 params
['file-name'] = file
;
401 atag
.download
= filename
;
402 let url
= new URL(`/api2/json/admin/datastore/${view.datastore}/download-decoded`,
403 window
.location
.origin
);
404 for (const [key
, value
] of Object
.entries(params
)) {
405 url
.searchParams
.append(key
, value
);
407 atag
.href
= url
.href
;
411 openPxarBrowser: function(tv
, rI
, Ci
, item
, e
, rec
) {
413 let view
= me
.getView();
415 if (!(rec
&& rec
.data
)) return;
416 let data
= rec
.parentNode
.data
;
418 let id
= data
['backup-id'];
419 let time
= data
['backup-time'];
420 let type
= data
['backup-type'];
421 let timetext
= PBS
.Utils
.render_datetime_utc(data
["backup-time"]);
423 Ext
.create('PBS.window.FileBrowser', {
424 title
: `${type}/${id}/${timetext}`,
425 datastore
: view
.datastore
,
427 'backup-time': (time
.getTime()/1000).toFixed(0),
429 archive
: rec
.data
.filename
,
433 filter: function(item
, value
) {
434 if (item
.data
.text
.indexOf(value
) !== -1) {
438 if (item
.data
.owner
&& item
.data
.owner
.indexOf(value
) !== -1) {
445 search: function(tf
, value
) {
447 let view
= me
.getView();
448 let store
= view
.getStore();
449 if (!value
&& value
!== 0) {
451 store
.getRoot().collapseChildren(true);
452 tf
.triggers
.clear
.setVisible(false);
455 tf
.triggers
.clear
.setVisible(true);
456 if (value
.length
< 2) return;
457 Proxmox
.Utils
.setErrorMask(view
, true);
458 // we do it a little bit later for the error mask to work
459 setTimeout(function() {
461 store
.getRoot().collapseChildren(true);
464 store
.getRoot().cascadeBy({
465 before: function(item
) {
466 if (me
.filter(item
, value
)) {
467 item
.set('matchesFilter', true);
468 if (item
.parentNode
&& item
.parentNode
.id
!== 'root') {
469 item
.parentNode
.childmatches
= true;
475 after: function(item
) {
476 if (me
.filter(item
, value
) || item
.id
=== 'root' || item
.childmatches
) {
477 item
.set('matchesFilter', true);
478 if (item
.parentNode
&& item
.parentNode
.id
!== 'root') {
479 item
.parentNode
.childmatches
= true;
481 if (item
.childmatches
) {
485 item
.set('matchesFilter', false);
487 delete item
.childmatches
;
492 store
.filter((item
) => !!item
.get('matchesFilter'));
493 Proxmox
.Utils
.setErrorMask(view
, false);
499 getRowClass: function(record
, index
) {
500 let verify
= record
.get('verification');
501 if (verify
&& verify
.lastFailed
) {
502 return 'proxmox-invalid-row';
511 header
: gettext("Backup Group"),
516 header
: gettext('Actions'),
517 xtype
: 'actioncolumn',
522 tooltip
: gettext('Verify'),
523 getClass
: (v
, m
, rec
) => rec
.data
.leaf
? 'pmx-hidden' : 'pve-icon-verify-lettering',
524 isDisabled
: (v
, r
, c
, i
, rec
) => !!rec
.data
.leaf
,
528 tooltip
: gettext('Prune'),
529 getClass
: (v
, m
, rec
) => rec
.parentNode
.id
==='root' ? 'fa fa-scissors' : 'pmx-hidden',
530 isDisabled
: (v
, r
, c
, i
, rec
) => rec
.parentNode
.id
!=='root',
534 tooltip
: gettext('Forget Snapshot'),
535 getClass
: (v
, m
, rec
) => !rec
.data
.leaf
&& rec
.parentNode
.id
!== 'root' ? 'fa critical fa-trash-o' : 'pmx-hidden',
536 isDisabled
: (v
, r
, c
, i
, rec
) => rec
.data
.leaf
|| rec
.parentNode
.id
=== 'root',
539 handler
: 'downloadFile',
540 tooltip
: gettext('Download'),
541 getClass
: (v
, m
, rec
) => rec
.data
.leaf
&& rec
.data
.filename
? 'fa fa-download' : 'pmx-hidden',
542 isDisabled
: (v
, r
, c
, i
, rec
) => !rec
.data
.leaf
|| !rec
.data
.filename
|| rec
.data
['crypt-mode'] > 2,
545 handler
: 'openPxarBrowser',
546 tooltip
: gettext('Browse'),
547 getClass
: (v
, m
, rec
) => {
549 if (data
.leaf
&& data
.filename
&& data
.filename
.endsWith('pxar.didx')) {
550 return 'fa fa-folder-open-o';
554 isDisabled
: (v
, r
, c
, i
, rec
) => {
556 return !(data
.leaf
&&
558 data
.filename
.endsWith('pxar.didx') &&
559 data
['crypt-mode'] < 3);
566 header
: gettext('Backup Time'),
568 dataIndex
: 'backup-time',
569 format
: 'Y-m-d H:i:s',
573 header
: gettext("Size"),
576 renderer
: (v
, meta
, record
) => {
577 if (record
.data
.text
=== 'client.log.blob' && v
=== undefined) {
580 if (v
=== undefined || v
=== null) {
581 meta
.tdCls
= "x-grid-row-loading";
584 return Proxmox
.Utils
.format_size(v
);
588 xtype
: 'numbercolumn',
590 header
: gettext("Count"),
597 header
: gettext("Owner"),
602 header
: gettext('Encrypted'),
603 dataIndex
: 'crypt-mode',
604 renderer
: (v
, meta
, record
) => {
605 if (record
.data
.size
=== undefined || record
.data
.size
=== null) {
611 let iconCls
= PBS
.Utils
.cryptIconCls
[v
] || '';
614 iconTxt
= `<i class="fa fa-fw fa-${iconCls}"></i> `;
616 return (iconTxt
+ PBS
.Utils
.cryptText
[v
]) || Proxmox
.Utils
.unknownText
;
620 header
: gettext('Verify State'),
622 dataIndex
: 'verification',
624 renderer
: (v
, meta
, record
) => {
625 let i
= (cls
, txt
) => `<i class="fa fa-fw fa-${cls}"></i> ${txt}`;
626 if (v
=== undefined || v
=== null) {
627 return record
.data
.leaf
? '' : i('question-circle-o warning', gettext('None'));
629 let tip
, iconCls
, txt
;
630 if (record
.parentNode
.id
=== 'root') {
631 if (v
.failed
=== 0) {
633 if (v
.outdated
> 0) {
634 tip
= 'All OK, but some snapshots were not verified in last 30 days';
635 iconCls
= 'check warning';
636 txt
= gettext('All OK (old)');
638 tip
= 'All snapshots verified at least once in last 30 days';
639 iconCls
= 'check good';
640 txt
= gettext('All OK');
642 } else if (v
.ok
=== 0) {
643 tip
= `${v.none} not verified yet`;
644 iconCls
= 'question-circle-o warning';
645 txt
= gettext('None');
647 tip
= `${v.ok} OK, ${v.none} not verified yet`;
648 iconCls
= 'check faded';
652 tip
= `${v.ok} OK, ${v.failed} failed, ${v.none} not verified yet`;
653 iconCls
= 'times critical';
654 txt
= v
.ok
=== 0 && v
.none
=== 0
655 ? gettext('All failed')
656 : `${v.failed} failed`;
658 } else if (!v
.state
) {
659 return record
.data
.leaf
? '' : gettext('None');
661 let verify_time
= Proxmox
.Utils
.render_timestamp(v
.lastTime
);
662 tip
= `Last verify task started on ${verify_time}`;
664 iconCls
= 'times critical';
665 if (v
.state
=== 'ok') {
666 iconCls
= 'check good';
667 let now
= Date
.now() / 1000;
668 if (now
- v
.lastTime
> 30 * 24 * 60 * 60) {
669 tip
= `Last verify task over 30 days ago: ${verify_time}`;
670 iconCls
= 'check warning';
674 return `<span data-qtip="${tip}">
675 <i class="fa fa-fw fa-${iconCls}"></i> ${txt}
679 dblclick: function(view
, el
, row
, col
, ev
, rec
) {
680 let data
= rec
.data
|| {};
681 let verify
= data
.verification
;
682 if (verify
&& verify
.upid
&& rec
.parentNode
.id
!== 'root') {
683 let win
= Ext
.create('Proxmox.window.TaskViewer', {
695 text
: gettext('Reload'),
696 iconCls
: 'fa fa-refresh',
701 xtype
: 'proxmoxButton',
702 text
: gettext('Verify All'),
703 confirmMsg
: gettext('Do you want to verify all snapshots now?'),
704 handler
: 'verifyAll',
709 html
: gettext('Search'),
713 reference
: 'searchbox',
714 emptyText
: gettext('group, date or owner'),
717 cls
: 'pmx-clear-trigger',
720 handler: function() {
721 this.triggers
.clear
.setVisible(false);