ui: move datastore related files into own folder

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
This commit is contained in:
Thomas Lamprecht
2020-11-06 08:11:06 +01:00
parent ae8635c307
commit 6465d809cd
6 changed files with 0 additions and 0 deletions

794
www/datastore/Content.js Normal file
View 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
View 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
View 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
View 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
View 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
View 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']);
},
});