3e395378bc
instead of having the files as a column, put the files into the tree as a third level with this, we can move the actions into an action column and remove the top buttons (except reload) clicking the download action now downloads directly, so we would not need the download window anymore clicking the browse action, opens the pxar browser like before, but expands and selects (&focus) the selected pxar file also changes the icon of 'signed' to the one to locked but color codes them (singed => greyed out, encrypted => green), similar to what browsers do/did for certificates Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
453 lines
11 KiB
JavaScript
453 lines
11 KiB
JavaScript
Ext.define('pbs-data-store-snapshots', {
|
|
extend: 'Ext.data.Model',
|
|
fields: [
|
|
'backup-type',
|
|
'backup-id',
|
|
{
|
|
name: 'backup-time',
|
|
type: 'date',
|
|
dateFormat: 'timestamp'
|
|
},
|
|
'files',
|
|
'owner',
|
|
{ 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);
|
|
}
|
|
}
|
|
]
|
|
});
|
|
|
|
Ext.define('PBS.DataStoreContent', {
|
|
extend: 'Ext.tree.Panel',
|
|
alias: 'widget.pbsDataStoreContent',
|
|
|
|
rootVisible: false,
|
|
|
|
title: gettext('Content'),
|
|
|
|
controller: {
|
|
xclass: 'Ext.app.ViewController',
|
|
|
|
init: function(view) {
|
|
if (!view.datastore) {
|
|
throw "no datastore specified";
|
|
}
|
|
|
|
this.store = Ext.create('Ext.data.Store', {
|
|
model: 'pbs-data-store-snapshots',
|
|
groupField: 'backup-group',
|
|
});
|
|
this.store.on('load', this.onLoad, this);
|
|
|
|
view.getStore().setSorters([
|
|
'backup-group',
|
|
'text',
|
|
'backup-time'
|
|
]);
|
|
Proxmox.Utils.monStoreErrors(view, this.store);
|
|
this.reload(); // initial load
|
|
},
|
|
|
|
reload: function() {
|
|
let view = this.getView();
|
|
|
|
if (!view.store || !this.store) {
|
|
console.warn('cannot reload, no store(s)');
|
|
return;
|
|
}
|
|
|
|
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
|
|
});
|
|
|
|
this.store.load();
|
|
},
|
|
|
|
getRecordGroups: function(records) {
|
|
let groups = {};
|
|
|
|
for (const item of records) {
|
|
var btype = item.data["backup-type"];
|
|
let group = btype + "/" + item.data["backup-id"];
|
|
|
|
if (groups[group] !== undefined) {
|
|
continue;
|
|
}
|
|
|
|
var cls = '';
|
|
if (btype === 'vm') {
|
|
cls = 'fa-desktop';
|
|
} else if (btype === 'ct') {
|
|
cls = 'fa-cube';
|
|
} else if (btype === 'host') {
|
|
cls = 'fa-building';
|
|
} else {
|
|
console.warn(`got unknown backup-type '${btype}'`);
|
|
continue; // FIXME: auto render? what do?
|
|
}
|
|
|
|
groups[group] = {
|
|
text: group,
|
|
leaf: false,
|
|
iconCls: "fa " + cls,
|
|
expanded: false,
|
|
backup_type: item.data["backup-type"],
|
|
backup_id: item.data["backup-id"],
|
|
children: []
|
|
};
|
|
}
|
|
|
|
return groups;
|
|
},
|
|
|
|
onLoad: function(store, records, success, operation) {
|
|
let view = this.getView();
|
|
|
|
if (!success) {
|
|
Proxmox.Utils.setErrorMask(view, Proxmox.Utils.getResponseErrorMessage(operation.getError()));
|
|
return;
|
|
}
|
|
|
|
let groups = this.getRecordGroups(records);
|
|
|
|
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 = false;
|
|
data.cls = 'no-leaf-icons';
|
|
|
|
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;
|
|
|
|
data.children.push(file);
|
|
}
|
|
|
|
children.push(data);
|
|
}
|
|
|
|
let children = [];
|
|
for (const [_key, 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) {
|
|
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;
|
|
}
|
|
|
|
}
|
|
group.count = group.children.length;
|
|
crypt.count = group.count;
|
|
group['crypt-mode'] = PBS.Utils.calculateCryptMode(crypt);
|
|
children.push(group);
|
|
}
|
|
|
|
view.setRootNode({
|
|
expanded: true,
|
|
children: children
|
|
});
|
|
Proxmox.Utils.setErrorMask(view, false);
|
|
},
|
|
|
|
onPrune: function(view, rI, cI, item, e, rec) {
|
|
var view = this.getView();
|
|
|
|
if (!(rec && rec.data)) return;
|
|
let data = rec.data;
|
|
if (rec.parentNode.id !== 'root') return;
|
|
|
|
if (!view.datastore) return;
|
|
|
|
let win = Ext.create('PBS.DataStorePrune', {
|
|
datastore: view.datastore,
|
|
backup_type: data.backup_type,
|
|
backup_id: data.backup_id,
|
|
});
|
|
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();
|
|
}
|
|
},
|
|
|
|
columns: [
|
|
{
|
|
xtype: 'treecolumn',
|
|
header: gettext("Backup Group"),
|
|
dataIndex: 'text',
|
|
flex: 1
|
|
},
|
|
{
|
|
header: gettext('Actions'),
|
|
xtype: 'actioncolumn',
|
|
dataIndex: 'text',
|
|
items: [
|
|
{
|
|
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,
|
|
},
|
|
{
|
|
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',
|
|
},
|
|
{
|
|
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',
|
|
},
|
|
{
|
|
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,
|
|
},
|
|
{
|
|
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'] < 2);
|
|
}
|
|
},
|
|
]
|
|
},
|
|
{
|
|
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 '';
|
|
}
|
|
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 (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
|
|
}
|
|
},
|
|
],
|
|
|
|
tbar: [
|
|
{
|
|
text: gettext('Reload'),
|
|
iconCls: 'fa fa-refresh',
|
|
handler: 'reload',
|
|
},
|
|
],
|
|
});
|