dateFormat: 'timestamp'
},
'files',
- { name: 'size', type: 'int' },
+ 'owner',
+ 'verification',
+ { name: 'size', type: 'int', allowNull: true, },
+ {
+ name: 'crypt-mode',
+ type: 'boolean',
+ calculate: function(data) {
+ let encrypted = 0;
+ let crypt = {
+ none: 0,
+ mixed: 0,
+ 'sign-only': 0,
+ encrypt: 0,
+ count: 0,
+ };
+ let signed = 0;
+ data.files.forEach(file => {
+ if (file.filename === 'index.json.blob') return; // is never encrypted
+ let mode = PBS.Utils.cryptmap.indexOf(file['crypt-mode']);
+ if (mode !== -1) {
+ crypt[file['crypt-mode']]++;
+ crypt.count++;
+ }
+ });
+
+ return PBS.Utils.calculateCryptMode(crypt);
+ }
+ },
+ {
+ name: 'matchesFilter',
+ type: 'boolean',
+ defaultValue: true,
+ },
]
});
this.store = Ext.create('Ext.data.Store', {
model: 'pbs-data-store-snapshots',
- sorters: 'backup-group',
groupField: 'backup-group',
});
this.store.on('load', this.onLoad, this);
- Proxmox.Utils.monStoreErrors(view, view.store, true);
+ view.getStore().setSorters([
+ 'backup-group',
+ 'text',
+ 'backup-time'
+ ]);
+ Proxmox.Utils.monStoreErrors(view, this.store);
this.reload(); // initial load
},
let url = `/api2/json/admin/datastore/${view.datastore}/snapshots`;
this.store.setProxy({
type: 'proxmox',
+ timeout: 300*1000, // 5 minutes, we should make that api call faster
url: url
});
} else if (btype === 'host') {
cls = 'fa-building';
} else {
- console.warn(`got unkown backup-type '${btype}'`);
+ console.warn(`got unknown backup-type '${btype}'`);
continue; // FIXME: auto render? what do?
}
return groups;
},
- onLoad: function(store, records, success) {
+ onLoad: function(store, records, success, operation) {
+ let me = this;
let view = this.getView();
if (!success) {
+ Proxmox.Utils.setErrorMask(view, Proxmox.Utils.getResponseErrorMessage(operation.getError()));
return;
}
let groups = this.getRecordGroups(records);
+ let selected;
+ let expanded = {};
+
+ view.getSelection().some(function(item) {
+ let id = item.data.text;
+ if (item.data.leaf) {
+ id = item.parentNode.data.text + id;
+ }
+ selected = id;
+ return true;
+ });
+
+ view.getRootNode().cascadeBy({
+ before: item => {
+ if (item.isExpanded() && !item.data.leaf) {
+ let id = item.data.text;
+ expanded[id] = true;
+ return true;
+ }
+ return false;
+ },
+ after: () => {},
+ });
+
for (const item of records) {
let group = item.data["backup-type"] + "/" + item.data["backup-id"];
let children = groups[group].children;
let data = item.data;
data.text = group + '/' + PBS.Utils.render_datetime_utc(data["backup-time"]);
- data.leaf = true;
+ data.leaf = false;
data.cls = 'no-leaf-icons';
+ data.matchesFilter = true;
+
+ data.expanded = !!expanded[data.text];
+
+ data.children = [];
+ for (const file of data.files) {
+ file.text = file.filename,
+ file['crypt-mode'] = PBS.Utils.cryptmap.indexOf(file['crypt-mode']);
+ file.leaf = true;
+ file.matchesFilter = true;
+
+ data.children.push(file);
+ }
children.push(data);
}
let children = [];
- for (const [_key, group] of Object.entries(groups)) {
+ for (const [name, group] of Object.entries(groups)) {
let last_backup = 0;
+ let crypt = {
+ none: 0,
+ mixed: 0,
+ 'sign-only': 0,
+ encrypt: 0,
+ };
for (const item of group.children) {
- if (item["backup-time"] > last_backup) {
+ crypt[PBS.Utils.cryptmap[item['crypt-mode']]]++;
+ if (item["backup-time"] > last_backup && item.size !== null) {
last_backup = item["backup-time"];
group["backup-time"] = last_backup;
group.files = item.files;
group.size = item.size;
+ group.owner = item.owner;
}
+ if (item.verification &&
+ (!group.verification || group.verification.state !== 'failed')) {
+ group.verification = item.verification;
+ }
+
}
group.count = group.children.length;
+ group.matchesFilter = true;
+ crypt.count = group.count;
+ group['crypt-mode'] = PBS.Utils.calculateCryptMode(crypt);
+ group.expanded = !!expanded[name];
children.push(group);
}
expanded: true,
children: children
});
+
+ if (selected !== undefined) {
+ let selection = view.getRootNode().findChildBy(function(item) {
+ let id = item.data.text;
+ if (item.data.leaf) {
+ id = item.parentNode.data.text + id;
+ }
+ return selected === id;
+ }, undefined, true);
+ if (selection) {
+ view.setSelection(selection);
+ view.getView().focusRow(selection);
+ }
+ }
+
+ Proxmox.Utils.setErrorMask(view, false);
+ if (view.getStore().getFilters().length > 0) {
+ let searchBox = me.lookup("searchbox");
+ let searchvalue = searchBox.getValue();;
+ me.search(searchBox, searchvalue);
+ }
},
- onPrune: function() {
+ onPrune: function(view, rI, cI, item, e, rec) {
var view = this.getView();
- let rec = view.selModel.getSelection()[0];
if (!(rec && rec.data)) return;
let data = rec.data;
- if (data.leaf) return;
+ if (rec.parentNode.id !== 'root') return;
if (!view.datastore) return;
});
win.on('destroy', this.reload, this);
win.show();
- }
- },
+ },
+
+ onVerify: function(view, rI, cI, item, e, rec) {
+ var view = this.getView();
+
+ if (!view.datastore) return;
+
+ if (!(rec && rec.data)) return;
+ let data = rec.data;
+
+ let params;
+
+ if (rec.parentNode.id !== 'root') {
+ params = {
+ "backup-type": data["backup-type"],
+ "backup-id": data["backup-id"],
+ "backup-time": (data['backup-time'].getTime()/1000).toFixed(0),
+ };
+ } else {
+ params = {
+ "backup-type": data.backup_type,
+ "backup-id": data.backup_id,
+ };
+ }
+
+ Proxmox.Utils.API2Request({
+ params: params,
+ url: `/admin/datastore/${view.datastore}/verify`,
+ method: 'POST',
+ failure: function(response) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ success: function(response, options) {
+ Ext.create('Proxmox.window.TaskViewer', {
+ upid: response.result.data,
+ }).show();
+ },
+ });
+ },
+
+ onForget: function(view, rI, cI, item, e, rec) {
+ let me = this;
+ var view = this.getView();
+
+ if (!(rec && rec.data)) return;
+ let data = rec.data;
+ if (!view.datastore) return;
+
+ Ext.Msg.show({
+ title: gettext('Confirm'),
+ icon: Ext.Msg.WARNING,
+ message: Ext.String.format(gettext('Are you sure you want to remove snapshot {0}'), `'${data.text}'`),
+ buttons: Ext.Msg.YESNO,
+ defaultFocus: 'no',
+ callback: function(btn) {
+ if (btn !== 'yes') {
+ return;
+ }
+
+ Proxmox.Utils.API2Request({
+ params: {
+ "backup-type": data["backup-type"],
+ "backup-id": data["backup-id"],
+ "backup-time": (data['backup-time'].getTime()/1000).toFixed(0),
+ },
+ url: `/admin/datastore/${view.datastore}/snapshots`,
+ method: 'DELETE',
+ waitMsgTarget: view,
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ callback: me.reload.bind(me),
+ });
+ },
+ });
+ },
+
+ downloadFile: function(tV, rI, cI, item, e, rec) {
+ let me = this;
+ let view = me.getView();
+
+ if (!(rec && rec.data)) return;
+ let data = rec.parentNode.data;
+
+ let file = rec.data.filename;
+ let params = {
+ 'backup-id': data['backup-id'],
+ 'backup-type': data['backup-type'],
+ 'backup-time': (data['backup-time'].getTime()/1000).toFixed(0),
+ 'file-name': file,
+ };
+
+ let idx = file.lastIndexOf('.');
+ let filename = file.slice(0, idx);
+ let atag = document.createElement('a');
+ params['file-name'] = file;
+ atag.download = filename;
+ let url = new URL(`/api2/json/admin/datastore/${view.datastore}/download-decoded`, window.location.origin);
+ for (const [key, value] of Object.entries(params)) {
+ url.searchParams.append(key, value);
+ }
+ atag.href = url.href;
+ atag.click();
+ },
+
+ openPxarBrowser: function(tv, rI, Ci, item, e, rec) {
+ let me = this;
+ let view = me.getView();
+
+ if (!(rec && rec.data)) return;
+ let data = rec.parentNode.data;
+
+ let id = data['backup-id'];
+ let time = data['backup-time'];
+ let type = data['backup-type'];
+ let timetext = PBS.Utils.render_datetime_utc(data["backup-time"]);
+
+ Ext.create('PBS.window.FileBrowser', {
+ title: `${type}/${id}/${timetext}`,
+ datastore: view.datastore,
+ 'backup-id': id,
+ 'backup-time': (time.getTime()/1000).toFixed(0),
+ 'backup-type': type,
+ archive: rec.data.filename,
+ }).show();
+ },
+
+ filter: function(item, value) {
+ if (item.data.text.indexOf(value) !== -1) {
+ return true;
+ }
- initComponent: function() {
- var me = this;
+ if (item.data.owner && item.data.owner.indexOf(value) !== -1) {
+ return true;
+ }
- var sm = Ext.create('Ext.selection.RowModel', {});
+ return false;
+ },
- var prune_btn = new Proxmox.button.Button({
- text: gettext('Prune'),
- disabled: true,
- selModel: sm,
- enableFn: function(record) { return !record.data.leaf; },
- handler: 'onPrune',
- });
+ search: function(tf, value) {
+ let me = this;
+ let view = me.getView();
+ let store = view.getStore();
+ if (!value && value !== 0) {
+ store.clearFilter();
+ store.getRoot().collapseChildren(true);
+ tf.triggers.clear.setVisible(false);
+ return;
+ }
+ tf.triggers.clear.setVisible(true);
+ if (value.length < 2) return;
+ Proxmox.Utils.setErrorMask(view, true);
+ // we do it a little bit later for the error mask to work
+ setTimeout(function() {
+ store.clearFilter();
+ store.getRoot().collapseChildren(true);
+
+ store.beginUpdate();
+ store.getRoot().cascadeBy({
+ before: function(item) {
+ if(me.filter(item, value)) {
+ item.set('matchesFilter', true);
+ if (item.parentNode && item.parentNode.id !== 'root') {
+ item.parentNode.childmatches = true;
+ }
+ return false;
+ }
+ return true;
+ },
+ after: function(item) {
+ if (me.filter(item, value) || item.id === 'root' || item.childmatches) {
+ item.set('matchesFilter', true);
+ if (item.parentNode && item.parentNode.id !== 'root') {
+ item.parentNode.childmatches = true;
+ }
+ if (item.childmatches) {
+ item.expand();
+ }
+ } else {
+ item.set('matchesFilter', false);
+ }
+ delete item.childmatches;
+ },
+ });
+ store.endUpdate();
+
+ store.filter((item) => !!item.get('matchesFilter'));
+ Proxmox.Utils.setErrorMask(view, false);
+ }, 10);
+ },
+ },
- Ext.apply(me, {
- selModel: sm,
- columns: [
+ columns: [
+ {
+ xtype: 'treecolumn',
+ header: gettext("Backup Group"),
+ dataIndex: 'text',
+ flex: 1
+ },
+ {
+ header: gettext('Actions'),
+ xtype: 'actioncolumn',
+ dataIndex: 'text',
+ items: [
{
- xtype: 'treecolumn',
- header: gettext("Backup Group"),
- dataIndex: 'text',
- flex: 1
+ handler: 'onVerify',
+ tooltip: gettext('Verify'),
+ getClass: (v, m, rec) => rec.data.leaf ? 'pmx-hidden' : 'fa fa-search',
+ isDisabled: (v, r, c, i, rec) => !!rec.data.leaf,
},
{
- xtype: 'datecolumn',
- header: gettext('Backup Time'),
- sortable: true,
- dataIndex: 'backup-time',
- format: 'Y-m-d H:i:s',
- width: 150
+ handler: 'onPrune',
+ tooltip: gettext('Prune'),
+ getClass: (v, m, rec) => rec.parentNode.id ==='root' ? 'fa fa-scissors' : 'pmx-hidden',
+ isDisabled: (v, r, c, i, rec) => rec.parentNode.id !=='root',
},
{
- header: gettext("Size"),
- sortable: true,
- dataIndex: 'size',
- renderer: Proxmox.Utils.format_size,
+ handler: 'onForget',
+ tooltip: gettext('Forget Snapshot'),
+ getClass: (v, m, rec) => !rec.data.leaf && rec.parentNode.id !== 'root' ? 'fa critical fa-trash-o' : 'pmx-hidden',
+ isDisabled: (v, r, c, i, rec) => rec.data.leaf || rec.parentNode.id === 'root',
},
{
- xtype: 'numbercolumn',
- format: '0',
- header: gettext("Count"),
- sortable: true,
- dataIndex: 'count',
+ handler: 'downloadFile',
+ tooltip: gettext('Download'),
+ getClass: (v, m, rec) => rec.data.leaf && rec.data.filename ? 'fa fa-download' : 'pmx-hidden',
+ isDisabled: (v, r, c, i, rec) => !rec.data.leaf || !rec.data.filename || rec.data['crypt-mode'] > 2,
},
{
- header: gettext("Files"),
- sortable: false,
- dataIndex: 'files',
- flex: 2
+ handler: 'openPxarBrowser',
+ tooltip: gettext('Browse'),
+ getClass: (v, m, rec) => {
+ let data = rec.data;
+ if (data.leaf && data.filename && data.filename.endsWith('pxar.didx')) {
+ return 'fa fa-folder-open-o';
+ }
+ return 'pmx-hidden';
+ },
+ isDisabled: (v, r, c, i, rec) => {
+ let data = rec.data;
+ return !(data.leaf &&
+ data.filename &&
+ data.filename.endsWith('pxar.didx') &&
+ data['crypt-mode'] < 3);
+ }
+ },
+ ]
+ },
+ {
+ xtype: 'datecolumn',
+ header: gettext('Backup Time'),
+ sortable: true,
+ dataIndex: 'backup-time',
+ format: 'Y-m-d H:i:s',
+ width: 150
+ },
+ {
+ header: gettext("Size"),
+ sortable: true,
+ dataIndex: 'size',
+ renderer: (v, meta, record) => {
+ if (record.data.text === 'client.log.blob' && v === undefined) {
+ return '';
}
- ],
-
- tbar: [
- {
- text: gettext('Reload'),
- iconCls: 'fa fa-refresh',
- handler: 'reload',
+ if (v === undefined || v === null) {
+ meta.tdCls = "x-grid-row-loading";
+ return '';
+ }
+ return Proxmox.Utils.format_size(v);
+ },
+ },
+ {
+ xtype: 'numbercolumn',
+ format: '0',
+ header: gettext("Count"),
+ sortable: true,
+ dataIndex: 'count',
+ },
+ {
+ header: gettext("Owner"),
+ sortable: true,
+ dataIndex: 'owner',
+ },
+ {
+ header: gettext('Encrypted'),
+ dataIndex: 'crypt-mode',
+ renderer: (v, meta, record) => {
+ if (record.data.size === undefined || record.data.size === null) {
+ return '';
+ }
+ if (v === -1) {
+ return '';
+ }
+ let iconCls = PBS.Utils.cryptIconCls[v] || '';
+ let iconTxt = "";
+ if (iconCls) {
+ iconTxt = `<i class="fa fa-fw fa-${iconCls}"></i> `;
+ }
+ return (iconTxt + PBS.Utils.cryptText[v]) || Proxmox.Utils.unknownText
+ }
+ },
+ {
+ header: gettext('Verify State'),
+ sortable: true,
+ dataIndex: 'verification',
+ renderer: (v, meta, record) => {
+ if (v === undefined || v === null || !v.state) {
+ //meta.tdCls = "x-grid-row-loading";
+ return record.data.leaf ? '' : gettext('None');
+ }
+ let task = Proxmox.Utils.parse_task_upid(v.upid);
+ let verify_time = Proxmox.Utils.render_timestamp(task.starttime);
+ let iconCls = v.state === 'ok' ? 'check good' : 'times critical';
+ let tip = `Verify task started on ${verify_time}`;
+ if (record.parentNode.id === 'root') {
+ tip = v.state === 'ok'
+ ? 'All verification OK in backup group'
+ : 'At least one failed verification in backup group!';
+ }
+ return `<span data-qtip="${tip}">
+ <i class="fa fa-fw fa-${iconCls}"></i> ${v.state}
+ </span>`;
+ },
+ listeners: {
+ dblclick: function(view, el, row, col, ev, rec) {
+ let data = rec.data || {};
+ let verify = data.verification;
+ if (verify && verify.upid && rec.parentNode.id !== 'root') {
+ let win = Ext.create('Proxmox.window.TaskViewer', {
+ upid: verify.upid,
+ });
+ win.show();
+ }
},
- prune_btn
- ],
- });
+ },
+ },
+ ],
- me.callParent();
- },
+ tbar: [
+ {
+ text: gettext('Reload'),
+ iconCls: 'fa fa-refresh',
+ handler: 'reload',
+ },
+ '->',
+ {
+ xtype: 'tbtext',
+ html: gettext('Search'),
+ },
+ {
+ xtype: 'textfield',
+ reference: 'searchbox',
+ triggers: {
+ clear: {
+ cls: 'pmx-clear-trigger',
+ weight: -1,
+ hidden: true,
+ handler: function() {
+ this.triggers.clear.setVisible(false);
+ this.setValue('');
+ },
+ }
+ },
+ listeners: {
+ change: {
+ fn: 'search',
+ buffer: 500,
+ },
+ },
+ }
+ ],
});