ui: move datastore related files into own folder
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
This commit is contained in:
794
www/datastore/Content.js
Normal file
794
www/datastore/Content.js
Normal file
@ -0,0 +1,794 @@
|
||||
Ext.define('pbs-data-store-snapshots', {
|
||||
extend: 'Ext.data.Model',
|
||||
fields: [
|
||||
'backup-type',
|
||||
'backup-id',
|
||||
{
|
||||
name: 'backup-time',
|
||||
type: 'date',
|
||||
dateFormat: 'timestamp',
|
||||
},
|
||||
'comment',
|
||||
'files',
|
||||
'owner',
|
||||
'verification',
|
||||
{ name: 'size', type: 'int', allowNull: true },
|
||||
{
|
||||
name: 'crypt-mode',
|
||||
type: 'boolean',
|
||||
calculate: function(data) {
|
||||
let crypt = {
|
||||
none: 0,
|
||||
mixed: 0,
|
||||
'sign-only': 0,
|
||||
encrypt: 0,
|
||||
count: 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,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
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 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: Ext.emptyFn,
|
||||
});
|
||||
|
||||
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.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 nowSeconds = Date.now() / 1000;
|
||||
let children = [];
|
||||
for (const [name, group] of Object.entries(groups)) {
|
||||
let last_backup = 0;
|
||||
let crypt = {
|
||||
none: 0,
|
||||
mixed: 0,
|
||||
'sign-only': 0,
|
||||
encrypt: 0,
|
||||
};
|
||||
let verify = {
|
||||
outdated: 0,
|
||||
none: 0,
|
||||
failed: 0,
|
||||
ok: 0,
|
||||
};
|
||||
for (let 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;
|
||||
verify.lastFailed = item.verification && item.verification.state !== 'ok';
|
||||
}
|
||||
if (!item.verification) {
|
||||
verify.none++;
|
||||
} else {
|
||||
if (item.verification.state === 'ok') {
|
||||
verify.ok++;
|
||||
} else {
|
||||
verify.failed++;
|
||||
}
|
||||
let task = Proxmox.Utils.parse_task_upid(item.verification.upid);
|
||||
item.verification.lastTime = task.starttime;
|
||||
if (nowSeconds - task.starttime > 30 * 24 * 60 * 60) {
|
||||
verify.outdated++;
|
||||
}
|
||||
}
|
||||
}
|
||||
group.verification = verify;
|
||||
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);
|
||||
}
|
||||
|
||||
view.setRootNode({
|
||||
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(view, rI, cI, item, e, rec) {
|
||||
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();
|
||||
},
|
||||
|
||||
verifyAll: function() {
|
||||
var view = this.getView();
|
||||
|
||||
Proxmox.Utils.API2Request({
|
||||
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();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
onVerify: function(view, rI, cI, item, e, rec) {
|
||||
let me = this;
|
||||
view = me.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,
|
||||
taskDone: () => me.reload(),
|
||||
}).show();
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
onNotesEdit: function(view, data) {
|
||||
let me = this;
|
||||
|
||||
let url = `/admin/datastore/${view.datastore}/notes`;
|
||||
Ext.create('PBS.window.NotesEdit', {
|
||||
url: url,
|
||||
autoShow: true,
|
||||
apiCallDone: () => me.reload(), // FIXME: do something more efficient?
|
||||
extraRequestParams: {
|
||||
"backup-type": data["backup-type"],
|
||||
"backup-id": data["backup-id"],
|
||||
"backup-time": (data['backup-time'].getTime()/1000).toFixed(0),
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
onForget: function(view, rI, cI, item, e, rec) {
|
||||
let me = this;
|
||||
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;
|
||||
}
|
||||
|
||||
if (item.data.owner && item.data.owner.indexOf(value) !== -1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
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);
|
||||
},
|
||||
},
|
||||
|
||||
viewConfig: {
|
||||
getRowClass: function(record, index) {
|
||||
let verify = record.get('verification');
|
||||
if (verify && verify.lastFailed) {
|
||||
return 'proxmox-invalid-row';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
|
||||
columns: [
|
||||
{
|
||||
xtype: 'treecolumn',
|
||||
header: gettext("Backup Group"),
|
||||
dataIndex: 'text',
|
||||
flex: 1,
|
||||
},
|
||||
{
|
||||
text: gettext('Comment'),
|
||||
dataIndex: 'comment',
|
||||
flex: 1,
|
||||
renderer: (v, meta, record) => {
|
||||
let data = record.data;
|
||||
if (!data || data.leaf || record.parentNode.id === 'root') {
|
||||
return '';
|
||||
}
|
||||
if (v === undefined || v === null) {
|
||||
v = '';
|
||||
}
|
||||
v = Ext.String.htmlEncode(v);
|
||||
let icon = 'fa fa-fw fa-pencil pointer';
|
||||
|
||||
return `<span class="snapshot-comment-column">${v}</span>
|
||||
<i data-qtip="${gettext('Edit')}" style="float: right;" class="${icon}"></i>`;
|
||||
},
|
||||
listeners: {
|
||||
afterrender: function(component) {
|
||||
// a bit of a hack, but relatively easy, cheap and works out well.
|
||||
// more efficient to use one handler for the whole column than for each icon
|
||||
component.on('click', function(tree, cell, rowI, colI, e, rec) {
|
||||
let el = e.target;
|
||||
if (el.tagName !== "I" || !el.classList.contains("fa-pencil")) {
|
||||
return;
|
||||
}
|
||||
let view = tree.up();
|
||||
let controller = view.controller;
|
||||
controller.onNotesEdit(view, rec.data);
|
||||
});
|
||||
},
|
||||
dblclick: function(tree, el, row, col, ev, rec) {
|
||||
let data = rec.data || {};
|
||||
if (data.leaf || rec.parentNode.id === 'root') {
|
||||
return;
|
||||
}
|
||||
let view = tree.up();
|
||||
let controller = view.controller;
|
||||
controller.onNotesEdit(view, rec.data);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
header: gettext('Actions'),
|
||||
xtype: 'actioncolumn',
|
||||
dataIndex: 'text',
|
||||
items: [
|
||||
{
|
||||
handler: 'onVerify',
|
||||
getTip: (v, m, rec) => Ext.String.format(gettext("Verify '{0}'"), v),
|
||||
getClass: (v, m, rec) => rec.data.leaf ? 'pmx-hidden' : 'pve-icon-verify-lettering',
|
||||
isDisabled: (v, r, c, i, rec) => !!rec.data.leaf,
|
||||
},
|
||||
{
|
||||
handler: 'onPrune',
|
||||
getTip: (v, m, rec) => Ext.String.format(gettext("Prune '{0}'"), v),
|
||||
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',
|
||||
getTip: (v, m, rec) => Ext.String.format(gettext("Permanently forget snapshot '{0}'"), v),
|
||||
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',
|
||||
getTip: (v, m, rec) => Ext.String.format(gettext("Download '{0}'"), v),
|
||||
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'] < 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 '';
|
||||
}
|
||||
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,
|
||||
width: 75,
|
||||
align: 'right',
|
||||
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',
|
||||
width: 120,
|
||||
renderer: (v, meta, record) => {
|
||||
let i = (cls, txt) => `<i class="fa fa-fw fa-${cls}"></i> ${txt}`;
|
||||
if (v === undefined || v === null) {
|
||||
return record.data.leaf ? '' : i('question-circle-o warning', gettext('None'));
|
||||
}
|
||||
let tip, iconCls, txt;
|
||||
if (record.parentNode.id === 'root') {
|
||||
if (v.failed === 0) {
|
||||
if (v.none === 0) {
|
||||
if (v.outdated > 0) {
|
||||
tip = 'All OK, but some snapshots were not verified in last 30 days';
|
||||
iconCls = 'check warning';
|
||||
txt = gettext('All OK (old)');
|
||||
} else {
|
||||
tip = 'All snapshots verified at least once in last 30 days';
|
||||
iconCls = 'check good';
|
||||
txt = gettext('All OK');
|
||||
}
|
||||
} else if (v.ok === 0) {
|
||||
tip = `${v.none} not verified yet`;
|
||||
iconCls = 'question-circle-o warning';
|
||||
txt = gettext('None');
|
||||
} else {
|
||||
tip = `${v.ok} OK, ${v.none} not verified yet`;
|
||||
iconCls = 'check faded';
|
||||
txt = `${v.ok} OK`;
|
||||
}
|
||||
} else {
|
||||
tip = `${v.ok} OK, ${v.failed} failed, ${v.none} not verified yet`;
|
||||
iconCls = 'times critical';
|
||||
txt = v.ok === 0 && v.none === 0
|
||||
? gettext('All failed')
|
||||
: `${v.failed} failed`;
|
||||
}
|
||||
} else if (!v.state) {
|
||||
return record.data.leaf ? '' : gettext('None');
|
||||
} else {
|
||||
let verify_time = Proxmox.Utils.render_timestamp(v.lastTime);
|
||||
tip = `Last verify task started on ${verify_time}`;
|
||||
txt = v.state;
|
||||
iconCls = 'times critical';
|
||||
if (v.state === 'ok') {
|
||||
iconCls = 'check good';
|
||||
let now = Date.now() / 1000;
|
||||
if (now - v.lastTime > 30 * 24 * 60 * 60) {
|
||||
tip = `Last verify task over 30 days ago: ${verify_time}`;
|
||||
iconCls = 'check warning';
|
||||
}
|
||||
}
|
||||
}
|
||||
return `<span data-qtip="${tip}">
|
||||
<i class="fa fa-fw fa-${iconCls}"></i> ${txt}
|
||||
</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();
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
tbar: [
|
||||
{
|
||||
text: gettext('Reload'),
|
||||
iconCls: 'fa fa-refresh',
|
||||
handler: 'reload',
|
||||
},
|
||||
'-',
|
||||
{
|
||||
xtype: 'proxmoxButton',
|
||||
text: gettext('Verify All'),
|
||||
confirmMsg: gettext('Do you want to verify all snapshots now?'),
|
||||
handler: 'verifyAll',
|
||||
},
|
||||
'->',
|
||||
{
|
||||
xtype: 'tbtext',
|
||||
html: gettext('Search'),
|
||||
},
|
||||
{
|
||||
xtype: 'textfield',
|
||||
reference: 'searchbox',
|
||||
emptyText: gettext('group, date or owner'),
|
||||
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,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
104
www/datastore/Notes.js
Normal file
104
www/datastore/Notes.js
Normal file
@ -0,0 +1,104 @@
|
||||
Ext.define('PBS.DataStoreNotes', {
|
||||
extend: 'Ext.panel.Panel',
|
||||
xtype: 'pbsDataStoreNotes',
|
||||
mixins: ['Proxmox.Mixin.CBind'],
|
||||
|
||||
title: gettext("Comment"),
|
||||
bodyStyle: 'white-space:pre',
|
||||
bodyPadding: 10,
|
||||
scrollable: true,
|
||||
animCollapse: false,
|
||||
|
||||
cbindData: function(initalConfig) {
|
||||
let me = this;
|
||||
me.url = `/api2/extjs/config/datastore/${me.datastore}`;
|
||||
return { };
|
||||
},
|
||||
|
||||
run_editor: function() {
|
||||
let me = this;
|
||||
let win = Ext.create('Proxmox.window.Edit', {
|
||||
title: gettext('Comment'),
|
||||
width: 600,
|
||||
resizable: true,
|
||||
layout: 'fit',
|
||||
defaultButton: undefined,
|
||||
items: {
|
||||
xtype: 'textfield',
|
||||
name: 'comment',
|
||||
value: '',
|
||||
hideLabel: true,
|
||||
},
|
||||
url: me.url,
|
||||
listeners: {
|
||||
destroy: function() {
|
||||
me.load();
|
||||
},
|
||||
},
|
||||
}).show();
|
||||
win.load();
|
||||
},
|
||||
|
||||
setNotes: function(value) {
|
||||
let me = this;
|
||||
var data = value || '';
|
||||
me.update(Ext.htmlEncode(data));
|
||||
|
||||
if (me.collapsible && me.collapseMode === 'auto') {
|
||||
me.setCollapsed(data === '');
|
||||
}
|
||||
},
|
||||
|
||||
load: function() {
|
||||
var me = this;
|
||||
|
||||
Proxmox.Utils.API2Request({
|
||||
url: me.url,
|
||||
waitMsgTarget: me,
|
||||
failure: function(response, opts) {
|
||||
me.update(gettext('Error') + " " + response.htmlStatus);
|
||||
me.setCollapsed(false);
|
||||
},
|
||||
success: function(response, opts) {
|
||||
me.setNotes(response.result.data.comment);
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
listeners: {
|
||||
render: function(c) {
|
||||
var me = this;
|
||||
me.getEl().on('dblclick', me.run_editor, me);
|
||||
},
|
||||
afterlayout: function() {
|
||||
let me = this;
|
||||
if (me.collapsible && !me.getCollapsed() && me.collapseMode === 'always') {
|
||||
me.setCollapsed(true);
|
||||
me.collapseMode = ''; // only once, on initial load!
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
tools: [{
|
||||
type: 'gear',
|
||||
handler: function() {
|
||||
this.up('panel').run_editor();
|
||||
},
|
||||
}],
|
||||
|
||||
collapsible: true,
|
||||
collapseDirection: 'right',
|
||||
|
||||
initComponent: function() {
|
||||
var me = this;
|
||||
|
||||
me.callParent();
|
||||
|
||||
let sp = Ext.state.Manager.getProvider();
|
||||
me.collapseMode = sp.get('notes-collapse', 'never');
|
||||
|
||||
if (me.collapseMode === 'auto') {
|
||||
me.setCollapsed(true);
|
||||
}
|
||||
},
|
||||
});
|
96
www/datastore/Panel.js
Normal file
96
www/datastore/Panel.js
Normal file
@ -0,0 +1,96 @@
|
||||
Ext.define('PBS.DataStorePanel', {
|
||||
extend: 'Ext.tab.Panel',
|
||||
alias: 'widget.pbsDataStorePanel',
|
||||
mixins: ['Proxmox.Mixin.CBind'],
|
||||
|
||||
cbindData: function(initalConfig) {
|
||||
let me = this;
|
||||
return {
|
||||
aclPath: `/datastore/${me.datastore}`,
|
||||
};
|
||||
},
|
||||
|
||||
stateId: 'pbs-datastore-panel',
|
||||
stateful: true,
|
||||
|
||||
stateEvents: ['tabchange'],
|
||||
|
||||
applyState: function(state) {
|
||||
let me = this;
|
||||
if (state.tab !== undefined) {
|
||||
me.setActiveTab(state.tab);
|
||||
}
|
||||
},
|
||||
|
||||
getState: function() {
|
||||
let me = this;
|
||||
return {
|
||||
tab: me.getActiveTab().getItemId(),
|
||||
};
|
||||
},
|
||||
|
||||
border: false,
|
||||
defaults: {
|
||||
border: false,
|
||||
},
|
||||
|
||||
items: [
|
||||
{
|
||||
xtype: 'pbsDataStoreSummary',
|
||||
title: gettext('Summary'),
|
||||
itemId: 'summary',
|
||||
iconCls: 'fa fa-book',
|
||||
cbind: {
|
||||
datastore: '{datastore}',
|
||||
},
|
||||
},
|
||||
{
|
||||
xtype: 'pbsDataStoreContent',
|
||||
itemId: 'content',
|
||||
iconCls: 'fa fa-th',
|
||||
cbind: {
|
||||
datastore: '{datastore}',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: gettext('Prune & GC'),
|
||||
xtype: 'pbsDataStorePruneAndGC',
|
||||
itemId: 'prunegc',
|
||||
iconCls: 'fa fa-trash-o',
|
||||
cbind: {
|
||||
datastore: '{datastore}',
|
||||
},
|
||||
},
|
||||
{
|
||||
iconCls: 'fa fa-refresh',
|
||||
itemId: 'syncjobs',
|
||||
xtype: 'pbsSyncJobView',
|
||||
cbind: {
|
||||
datastore: '{datastore}',
|
||||
},
|
||||
},
|
||||
{
|
||||
iconCls: 'fa fa-check-circle',
|
||||
itemId: 'verifyjobs',
|
||||
xtype: 'pbsVerifyJobView',
|
||||
cbind: {
|
||||
datastore: '{datastore}',
|
||||
},
|
||||
},
|
||||
{
|
||||
itemId: 'acl',
|
||||
xtype: 'pbsACLView',
|
||||
iconCls: 'fa fa-unlock',
|
||||
aclExact: true,
|
||||
cbind: {
|
||||
aclPath: '{aclPath}',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
initComponent: function() {
|
||||
let me = this;
|
||||
me.title = `${gettext("Datastore")}: ${me.datastore}`;
|
||||
me.callParent();
|
||||
},
|
||||
});
|
192
www/datastore/Prune.js
Normal file
192
www/datastore/Prune.js
Normal file
@ -0,0 +1,192 @@
|
||||
Ext.define('pbs-prune-list', {
|
||||
extend: 'Ext.data.Model',
|
||||
fields: [
|
||||
'backup-type',
|
||||
'backup-id',
|
||||
{
|
||||
name: 'backup-time',
|
||||
type: 'date',
|
||||
dateFormat: 'timestamp',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
Ext.define('PBS.DataStorePruneInputPanel', {
|
||||
extend: 'Proxmox.panel.InputPanel',
|
||||
alias: 'widget.pbsDataStorePruneInputPanel',
|
||||
mixins: ['Proxmox.Mixin.CBind'],
|
||||
|
||||
onGetValues: function(values) {
|
||||
var me = this;
|
||||
|
||||
values["backup-type"] = me.backup_type;
|
||||
values["backup-id"] = me.backup_id;
|
||||
return values;
|
||||
},
|
||||
|
||||
controller: {
|
||||
xclass: 'Ext.app.ViewController',
|
||||
|
||||
init: function(view) {
|
||||
if (!view.url) {
|
||||
throw "no url specified";
|
||||
}
|
||||
if (!view.backup_type) {
|
||||
throw "no backup_type specified";
|
||||
}
|
||||
if (!view.backup_id) {
|
||||
throw "no backup_id specified";
|
||||
}
|
||||
|
||||
this.reload(); // initial load
|
||||
},
|
||||
|
||||
reload: function() {
|
||||
var view = this.getView();
|
||||
|
||||
let params = view.getValues();
|
||||
params["dry-run"] = true;
|
||||
|
||||
Proxmox.Utils.API2Request({
|
||||
url: view.url,
|
||||
method: "POST",
|
||||
params: params,
|
||||
callback: function() {
|
||||
// for easy breakpoint setting
|
||||
},
|
||||
failure: function(response, opts) {
|
||||
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
|
||||
},
|
||||
success: function(response, options) {
|
||||
var data = response.result.data;
|
||||
view.prune_store.setData(data);
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
control: {
|
||||
field: { change: 'reload' },
|
||||
},
|
||||
},
|
||||
|
||||
column1: [
|
||||
{
|
||||
xtype: 'proxmoxintegerfield',
|
||||
name: 'keep-last',
|
||||
allowBlank: true,
|
||||
fieldLabel: gettext('keep-last'),
|
||||
minValue: 1,
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxintegerfield',
|
||||
name: 'keep-hourly',
|
||||
allowBlank: true,
|
||||
fieldLabel: gettext('keep-hourly'),
|
||||
minValue: 1,
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxintegerfield',
|
||||
name: 'keep-daily',
|
||||
allowBlank: true,
|
||||
fieldLabel: gettext('keep-daily'),
|
||||
minValue: 1,
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxintegerfield',
|
||||
name: 'keep-weekly',
|
||||
allowBlank: true,
|
||||
fieldLabel: gettext('keep-weekly'),
|
||||
minValue: 1,
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxintegerfield',
|
||||
name: 'keep-monthly',
|
||||
allowBlank: true,
|
||||
fieldLabel: gettext('keep-monthly'),
|
||||
minValue: 1,
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxintegerfield',
|
||||
name: 'keep-yearly',
|
||||
allowBlank: true,
|
||||
fieldLabel: gettext('keep-yearly'),
|
||||
minValue: 1,
|
||||
},
|
||||
],
|
||||
|
||||
|
||||
initComponent: function() {
|
||||
var me = this;
|
||||
|
||||
me.prune_store = Ext.create('Ext.data.Store', {
|
||||
model: 'pbs-prune-list',
|
||||
sorters: { property: 'backup-time', direction: 'DESC' },
|
||||
});
|
||||
|
||||
me.column2 = [
|
||||
{
|
||||
xtype: 'grid',
|
||||
height: 200,
|
||||
store: me.prune_store,
|
||||
columns: [
|
||||
{
|
||||
header: gettext('Backup Time'),
|
||||
sortable: true,
|
||||
dataIndex: 'backup-time',
|
||||
renderer: function(value, metaData, record) {
|
||||
let text = Ext.Date.format(value, 'Y-m-d H:i:s');
|
||||
if (record.data.keep) {
|
||||
return text;
|
||||
} else {
|
||||
return '<div style="text-decoration: line-through;">'+ text +'</div>';
|
||||
}
|
||||
},
|
||||
flex: 1,
|
||||
},
|
||||
{
|
||||
text: "keep",
|
||||
dataIndex: 'keep',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
me.callParent();
|
||||
},
|
||||
});
|
||||
|
||||
Ext.define('PBS.DataStorePrune', {
|
||||
extend: 'Proxmox.window.Edit',
|
||||
|
||||
method: 'POST',
|
||||
submitText: "Prune",
|
||||
|
||||
isCreate: true,
|
||||
|
||||
initComponent: function() {
|
||||
var me = this;
|
||||
|
||||
if (!me.datastore) {
|
||||
throw "no datastore specified";
|
||||
}
|
||||
if (!me.backup_type) {
|
||||
throw "no backup_type specified";
|
||||
}
|
||||
if (!me.backup_id) {
|
||||
throw "no backup_id specified";
|
||||
}
|
||||
|
||||
Ext.apply(me, {
|
||||
url: '/api2/extjs/admin/datastore/' + me.datastore + "/prune",
|
||||
title: "Prune Datastore '" + me.datastore + "'",
|
||||
items: [{
|
||||
xtype: 'pbsDataStorePruneInputPanel',
|
||||
url: '/api2/extjs/admin/datastore/' + me.datastore + "/prune",
|
||||
backup_type: me.backup_type,
|
||||
backup_id: me.backup_id,
|
||||
}],
|
||||
});
|
||||
|
||||
me.callParent();
|
||||
},
|
||||
});
|
164
www/datastore/PruneAndGC.js
Normal file
164
www/datastore/PruneAndGC.js
Normal file
@ -0,0 +1,164 @@
|
||||
Ext.define('PBS.DataStorePruneAndGC', {
|
||||
extend: 'Proxmox.grid.ObjectGrid',
|
||||
alias: 'widget.pbsDataStorePruneAndGC',
|
||||
mixins: ['Proxmox.Mixin.CBind'],
|
||||
|
||||
cbindData: function(initial) {
|
||||
let me = this;
|
||||
|
||||
me.datastore = encodeURIComponent(me.datastore);
|
||||
me.url = `/api2/json/config/datastore/${me.datastore}`;
|
||||
me.editorConfig = {
|
||||
url: `/api2/extjs/config/datastore/${me.datastore}`,
|
||||
};
|
||||
return {};
|
||||
},
|
||||
|
||||
controller: {
|
||||
xclass: 'Ext.app.ViewController',
|
||||
|
||||
edit: function() { this.getView().run_editor(); },
|
||||
|
||||
garbageCollect: function() {
|
||||
let me = this;
|
||||
let view = me.getView();
|
||||
Proxmox.Utils.API2Request({
|
||||
url: `/admin/datastore/${view.datastore}/gc`,
|
||||
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();
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
tbar: [
|
||||
{
|
||||
xtype: 'proxmoxButton',
|
||||
text: gettext('Edit'),
|
||||
disabled: true,
|
||||
handler: 'edit',
|
||||
},
|
||||
'-',
|
||||
{
|
||||
xtype: 'proxmoxButton',
|
||||
text: gettext('Start Garbage Collection'),
|
||||
selModel: null,
|
||||
handler: 'garbageCollect',
|
||||
},
|
||||
],
|
||||
|
||||
listeners: {
|
||||
activate: function() { this.rstore.startUpdate(); },
|
||||
destroy: function() { this.rstore.stopUpdate(); },
|
||||
deactivate: function() { this.rstore.stopUpdate(); },
|
||||
itemdblclick: 'edit',
|
||||
},
|
||||
|
||||
rows: {
|
||||
"gc-schedule": {
|
||||
required: true,
|
||||
defaultValue: Proxmox.Utils.NoneText,
|
||||
header: gettext('Garbage Collection Schedule'),
|
||||
editor: {
|
||||
xtype: 'proxmoxWindowEdit',
|
||||
title: gettext('GC Schedule'),
|
||||
items: {
|
||||
xtype: 'pbsCalendarEvent',
|
||||
name: 'gc-schedule',
|
||||
fieldLabel: gettext("GC Schedule"),
|
||||
emptyText: Proxmox.Utils.noneText,
|
||||
deleteEmpty: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
"prune-schedule": {
|
||||
required: true,
|
||||
defaultValue: Proxmox.Utils.NoneText,
|
||||
header: gettext('Prune Schedule'),
|
||||
editor: {
|
||||
xtype: 'proxmoxWindowEdit',
|
||||
title: gettext('Prune Schedule'),
|
||||
items: {
|
||||
xtype: 'pbsCalendarEvent',
|
||||
name: 'prune-schedule',
|
||||
fieldLabel: gettext("Prune Schedule"),
|
||||
emptyText: Proxmox.Utils.noneText,
|
||||
deleteEmpty: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
"keep-last": {
|
||||
required: true,
|
||||
header: gettext('Keep Last'),
|
||||
editor: {
|
||||
xtype: 'proxmoxWindowEdit',
|
||||
title: gettext('Prune Options'),
|
||||
items: {
|
||||
xtype: 'pbsPruneInputPanel',
|
||||
isCreate: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
"keep-hourly": {
|
||||
required: true,
|
||||
header: gettext('Keep Hourly'),
|
||||
editor: {
|
||||
xtype: 'proxmoxWindowEdit',
|
||||
title: gettext('Prune Options'),
|
||||
items: {
|
||||
xtype: 'pbsPruneInputPanel',
|
||||
},
|
||||
},
|
||||
},
|
||||
"keep-daily": {
|
||||
required: true,
|
||||
header: gettext('Keep Daily'),
|
||||
editor: {
|
||||
xtype: 'proxmoxWindowEdit',
|
||||
title: gettext('Prune Options'),
|
||||
items: {
|
||||
xtype: 'pbsPruneInputPanel',
|
||||
},
|
||||
},
|
||||
},
|
||||
"keep-weekly": {
|
||||
required: true,
|
||||
header: gettext('Keep Weekly'),
|
||||
editor: {
|
||||
xtype: 'proxmoxWindowEdit',
|
||||
title: gettext('Prune Options'),
|
||||
items: {
|
||||
xtype: 'pbsPruneInputPanel',
|
||||
},
|
||||
},
|
||||
},
|
||||
"keep-monthly": {
|
||||
required: true,
|
||||
header: gettext('Keep Monthly'),
|
||||
editor: {
|
||||
xtype: 'proxmoxWindowEdit',
|
||||
title: gettext('Prune Options'),
|
||||
items: {
|
||||
xtype: 'pbsPruneInputPanel',
|
||||
},
|
||||
},
|
||||
},
|
||||
"keep-yearly": {
|
||||
required: true,
|
||||
header: gettext('Keep Yearly'),
|
||||
editor: {
|
||||
xtype: 'proxmoxWindowEdit',
|
||||
title: gettext('Prune Options'),
|
||||
items: {
|
||||
xtype: 'pbsPruneInputPanel',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
301
www/datastore/Summary.js
Normal file
301
www/datastore/Summary.js
Normal file
@ -0,0 +1,301 @@
|
||||
Ext.define('pve-rrd-datastore', {
|
||||
extend: 'Ext.data.Model',
|
||||
fields: [
|
||||
'used',
|
||||
'total',
|
||||
'read_ios',
|
||||
'read_bytes',
|
||||
'write_ios',
|
||||
'write_bytes',
|
||||
'io_ticks',
|
||||
{
|
||||
name: 'io_delay', calculate: function(data) {
|
||||
let ios = 0;
|
||||
if (data.read_ios !== undefined) { ios += data.read_ios; }
|
||||
if (data.write_ios !== undefined) { ios += data.write_ios; }
|
||||
if (data.io_ticks === undefined) {
|
||||
return undefined;
|
||||
} else if (ios === 0) {
|
||||
return 0;
|
||||
}
|
||||
return (data.io_ticks*1000.0)/ios;
|
||||
},
|
||||
},
|
||||
{ type: 'date', dateFormat: 'timestamp', name: 'time' },
|
||||
],
|
||||
});
|
||||
|
||||
Ext.define('PBS.DataStoreInfo', {
|
||||
extend: 'Ext.panel.Panel',
|
||||
alias: 'widget.pbsDataStoreInfo',
|
||||
|
||||
viewModel: {
|
||||
data: {
|
||||
countstext: '',
|
||||
usage: {},
|
||||
stillbad: 0,
|
||||
removedbytes: 0,
|
||||
mountpoint: "",
|
||||
},
|
||||
},
|
||||
|
||||
controller: {
|
||||
xclass: 'Ext.app.ViewController',
|
||||
|
||||
onLoad: function(store, data, success) {
|
||||
if (!success) return;
|
||||
let me = this;
|
||||
let vm = me.getViewModel();
|
||||
|
||||
let counts = store.getById('counts').data.value;
|
||||
let total = store.getById('total').data.value;
|
||||
let used = store.getById('used').data.value;
|
||||
|
||||
let percent = 100*used/total;
|
||||
if (total === 0) {
|
||||
percent = 0;
|
||||
}
|
||||
let used_percent = `${percent.toFixed(2)}%`;
|
||||
|
||||
let usage = used_percent + ' (' +
|
||||
Ext.String.format(
|
||||
gettext('{0} of {1}'),
|
||||
Proxmox.Utils.format_size(used),
|
||||
Proxmox.Utils.format_size(total),
|
||||
) + ')';
|
||||
vm.set('usagetext', usage);
|
||||
vm.set('usage', used/total);
|
||||
|
||||
let gcstatus = store.getById('gc-status').data.value;
|
||||
|
||||
let dedup = 1.0;
|
||||
if (gcstatus['disk-bytes'] > 0) {
|
||||
dedup = (gcstatus['index-data-bytes'] || 0)/gcstatus['disk-bytes'];
|
||||
}
|
||||
|
||||
let countstext = function(count) {
|
||||
count = count || {};
|
||||
return `${count.groups || 0} ${gettext('Groups')}, ${count.snapshots || 0} ${gettext('Snapshots')}`;
|
||||
};
|
||||
|
||||
vm.set('ctcount', countstext(counts.ct));
|
||||
vm.set('vmcount', countstext(counts.vm));
|
||||
vm.set('hostcount', countstext(counts.host));
|
||||
vm.set('deduplication', dedup.toFixed(2));
|
||||
vm.set('stillbad', gcstatus['still-bad']);
|
||||
vm.set('removedbytes', Proxmox.Utils.format_size(gcstatus['removed-bytes']));
|
||||
},
|
||||
|
||||
startStore: function() { this.store.startUpdate(); },
|
||||
stopStore: function() { this.store.stopUpdate(); },
|
||||
|
||||
init: function(view) {
|
||||
let me = this;
|
||||
let datastore = encodeURIComponent(view.datastore);
|
||||
me.store = Ext.create('Proxmox.data.ObjectStore', {
|
||||
interval: 5*1000,
|
||||
url: `/api2/json/admin/datastore/${datastore}/status`,
|
||||
});
|
||||
me.store.on('load', me.onLoad, me);
|
||||
},
|
||||
},
|
||||
|
||||
listeners: {
|
||||
activate: 'startStore',
|
||||
destroy: 'stopStore',
|
||||
deactivate: 'stopStore',
|
||||
},
|
||||
|
||||
defaults: {
|
||||
xtype: 'pmxInfoWidget',
|
||||
},
|
||||
|
||||
bodyPadding: 20,
|
||||
|
||||
items: [
|
||||
{
|
||||
iconCls: 'fa fa-fw fa-hdd-o',
|
||||
title: gettext('Usage'),
|
||||
bind: {
|
||||
data: {
|
||||
usage: '{usage}',
|
||||
text: '{usagetext}',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
xtype: 'box',
|
||||
html: `<b>${gettext('Backup Count')}</b>`,
|
||||
padding: '10 0 5 0',
|
||||
},
|
||||
{
|
||||
iconCls: 'fa fa-fw fa-cube',
|
||||
title: gettext('CT'),
|
||||
printBar: false,
|
||||
bind: {
|
||||
data: {
|
||||
text: '{ctcount}',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
iconCls: 'fa fa-fw fa-building',
|
||||
title: gettext('Host'),
|
||||
printBar: false,
|
||||
bind: {
|
||||
data: {
|
||||
text: '{hostcount}',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
iconCls: 'fa fa-fw fa-desktop',
|
||||
title: gettext('VM'),
|
||||
printBar: false,
|
||||
bind: {
|
||||
data: {
|
||||
text: '{vmcount}',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
xtype: 'box',
|
||||
html: `<b>${gettext('Stats from last Garbage Collection')}</b>`,
|
||||
padding: '10 0 5 0',
|
||||
},
|
||||
{
|
||||
iconCls: 'fa fa-fw fa-compress',
|
||||
title: gettext('Deduplication Factor'),
|
||||
printBar: false,
|
||||
bind: {
|
||||
data: {
|
||||
text: '{deduplication}',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
iconCls: 'fa fa-fw fa-trash-o',
|
||||
title: gettext('Removed Bytes'),
|
||||
printBar: false,
|
||||
bind: {
|
||||
data: {
|
||||
text: '{removedbytes}',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
iconCls: 'fa critical fa-fw fa-exclamation-triangle',
|
||||
title: gettext('Bad Chunks'),
|
||||
printBar: false,
|
||||
bind: {
|
||||
data: {
|
||||
text: '{stillbad}',
|
||||
},
|
||||
visible: '{stillbad}',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
Ext.define('PBS.DataStoreSummary', {
|
||||
extend: 'Ext.panel.Panel',
|
||||
alias: 'widget.pbsDataStoreSummary',
|
||||
mixins: ['Proxmox.Mixin.CBind'],
|
||||
|
||||
layout: 'column',
|
||||
scrollable: true,
|
||||
|
||||
bodyPadding: 5,
|
||||
defaults: {
|
||||
columnWidth: 1,
|
||||
padding: 5,
|
||||
},
|
||||
|
||||
tbar: ['->', { xtype: 'proxmoxRRDTypeSelector' }],
|
||||
|
||||
items: [
|
||||
{
|
||||
xtype: 'container',
|
||||
height: 300,
|
||||
layout: {
|
||||
type: 'hbox',
|
||||
align: 'stretch',
|
||||
},
|
||||
items: [
|
||||
{
|
||||
xtype: 'pbsDataStoreInfo',
|
||||
flex: 1,
|
||||
padding: '0 10 0 0',
|
||||
cbind: {
|
||||
title: '{datastore}',
|
||||
datastore: '{datastore}',
|
||||
},
|
||||
},
|
||||
{
|
||||
xtype: 'pbsDataStoreNotes',
|
||||
flex: 1,
|
||||
cbind: {
|
||||
datastore: '{datastore}',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxRRDChart',
|
||||
title: gettext('Storage usage (bytes)'),
|
||||
fields: ['total', 'used'],
|
||||
fieldTitles: [gettext('Total'), gettext('Storage usage')],
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxRRDChart',
|
||||
title: gettext('Transfer Rate (bytes/second)'),
|
||||
fields: ['read_bytes', 'write_bytes'],
|
||||
fieldTitles: [gettext('Read'), gettext('Write')],
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxRRDChart',
|
||||
title: gettext('Input/Output Operations per Second (IOPS)'),
|
||||
fields: ['read_ios', 'write_ios'],
|
||||
fieldTitles: [gettext('Read'), gettext('Write')],
|
||||
},
|
||||
{
|
||||
xtype: 'proxmoxRRDChart',
|
||||
title: gettext('IO Delay (ms)'),
|
||||
fields: ['io_delay'],
|
||||
fieldTitles: [gettext('IO Delay')],
|
||||
},
|
||||
],
|
||||
|
||||
listeners: {
|
||||
activate: function() { this.rrdstore.startUpdate(); },
|
||||
deactivate: function() { this.rrdstore.stopUpdate(); },
|
||||
destroy: function() { this.rrdstore.stopUpdate(); },
|
||||
},
|
||||
|
||||
initComponent: function() {
|
||||
let me = this;
|
||||
|
||||
me.rrdstore = Ext.create('Proxmox.data.RRDStore', {
|
||||
rrdurl: "/api2/json/admin/datastore/" + me.datastore + "/rrd",
|
||||
model: 'pve-rrd-datastore',
|
||||
});
|
||||
|
||||
me.callParent();
|
||||
|
||||
Proxmox.Utils.API2Request({
|
||||
url: `/config/datastore/${me.datastore}`,
|
||||
waitMsgTarget: me.down('pbsDataStoreInfo'),
|
||||
success: function(response) {
|
||||
let path = Ext.htmlEncode(response.result.data.path);
|
||||
me.down('pbsDataStoreInfo').setTitle(`${me.datastore} (${path})`);
|
||||
me.down('pbsDataStoreNotes').setNotes(response.result.data.comment);
|
||||
},
|
||||
});
|
||||
|
||||
me.query('proxmoxRRDChart').forEach((chart) => {
|
||||
chart.setStore(me.rrdstore);
|
||||
});
|
||||
|
||||
me.down('pbsDataStoreInfo').relayEvents(me, ['activate', 'deactivate']);
|
||||
},
|
||||
});
|
Reference in New Issue
Block a user