ui: add Panels necessary for Datastores Overview

a panel for a single datastore that gets updated from an external caller
shows the usage, estimated full date, history and task summary grid

a panel that dynamically generates the panel above for each datastore

and a tabpanel that includes the panel above, as well as a global
syncview, verifiyview and aclview

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
This commit is contained in:
Dominik Csapak 2020-11-09 16:01:29 +01:00 committed by Thomas Lamprecht
parent 63c07d950c
commit 2371c1e371
3 changed files with 369 additions and 0 deletions

View File

@ -56,6 +56,8 @@ JSSRC= \
datastore/Content.js \
datastore/OptionView.js \
datastore/Panel.js \
datastore/DataStoreListSummary.js \
datastore/DataStoreList.js \
ServerStatus.js \
ServerAdministration.js \
Dashboard.js \

View File

@ -0,0 +1,229 @@
// Overview over all datastores
Ext.define('PBS.datastore.DataStoreList', {
extend: 'Ext.panel.Panel',
alias: 'widget.pbsDataStoreList',
title: gettext('Summary'),
scrollable: true,
bodyPadding: 5,
defaults: {
xtype: 'pbsDataStoreListSummary',
padding: 5,
},
datastores: {},
tasks: {},
updateTasks: function(taskStore, records, success) {
let me = this;
if (!success) {
return;
}
for (const store of Object.keys(me.tasks)) {
me.tasks[store] = {};
}
records.forEach(record => {
let task = record.data;
if (!task.worker_id) {
return;
}
let type = task.worker_type;
if (type === 'syncjob') {
type = 'sync';
}
if (type.startsWith('verif')) {
type = 'verify';
}
let datastore = PBS.Utils.parse_datastore_worker_id(type, task.worker_id);
if (!datastore) {
return;
}
if (!me.tasks[datastore]) {
me.tasks[datastore] = {};
}
if (!me.tasks[datastore][type]) {
me.tasks[datastore][type] = {};
}
if (me.tasks[datastore][type] && task.status) {
let parsed = Proxmox.Utils.parse_task_status(task.status);
if (!me.tasks[datastore][type][parsed]) {
me.tasks[datastore][type][parsed] = 0;
}
me.tasks[datastore][type][parsed]++;
}
});
for (const [store, panel] of Object.entries(me.datastores)) {
panel.setTasks(me.tasks[store], me.since);
}
},
updateStores: function(usageStore, records, success) {
let me = this;
if (!success) {
return;
}
let found = {};
records.forEach((rec) => {
found[rec.data.store] = true;
me.addSorted(rec.data);
});
for (const [store, panel] of Object.entries(me.datastores)) {
if (!found[store]) {
me.remove(panel);
}
}
},
addSorted: function(data) {
let me = this;
let i = 0;
let datastores = Object
.keys(me.datastores)
.sort((a, b) => a.localeCompare(b));
for (const datastore of datastores) {
let result = datastore.localeCompare(data.store);
if (result === 0) {
me.datastores[datastore].setStatus(data);
return;
} else if (result > 0) {
break;
}
i++;
}
me.datastores[data.store] = me.insert(i, {
datastore: data.store,
});
me.datastores[data.store].setStatus(data);
me.datastores[data.store].setTasks(me.tasks[data.store], me.since);
},
initComponent: function() {
let me = this;
me.items = [];
me.datastores = {};
// todo make configurable?
me.since = (Date.now()/1000 - 30 * 24*3600).toFixed(0);
me.usageStore = Ext.create('Proxmox.data.UpdateStore', {
storeid: 'datastore-overview-usage',
interval: 5000,
proxy: {
type: 'proxmox',
url: '/api2/json/status/datastore-usage',
},
listeners: {
load: {
fn: me.updateStores,
scope: me,
},
},
});
me.taskStore = Ext.create('Proxmox.data.UpdateStore', {
storeid: 'datastore-overview-tasks',
interval: 15000,
model: 'proxmox-tasks',
proxy: {
type: 'proxmox',
url: '/api2/json/nodes/localhost/tasks',
extraParams: {
limit: 0,
since: me.since,
},
},
listeners: {
load: {
fn: me.updateTasks,
scope: me,
},
},
});
me.callParent();
Proxmox.Utils.monStoreErrors(me, me.usageStore);
Proxmox.Utils.monStoreErrors(me, me.taskStore);
me.on('activate', function() {
me.usageStore.startUpdate();
me.taskStore.startUpdate();
});
me.on('destroy', function() {
me.usageStore.stopUpdate();
me.taskStore.stopUpdate();
});
me.on('deactivate', function() {
me.usageStore.stopUpdate();
me.taskStore.stopUpdate();
});
},
});
Ext.define('PBS.datastore.DataStores', {
extend: 'Ext.tab.Panel',
alias: 'widget.pbsDataStores',
title: gettext('Datastores'),
stateId: 'pbs-datastores-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: 'pbsDataStoreList',
iconCls: 'fa fa-book',
},
{
iconCls: 'fa fa-refresh',
itemId: 'syncjobs',
xtype: 'pbsSyncJobView',
},
{
iconCls: 'fa fa-check-circle',
itemId: 'verifyjobs',
xtype: 'pbsVerifyJobView',
},
{
itemId: 'acl',
xtype: 'pbsACLView',
iconCls: 'fa fa-unlock',
aclPath: '/datastore',
},
],
});

View File

@ -0,0 +1,138 @@
// Summary Panel for a single datastore in overview
Ext.define('PBS.datastore.DataStoreListSummary', {
extend: 'Ext.panel.Panel',
alias: 'widget.pbsDataStoreListSummary',
mixins: ['Proxmox.Mixin.CBind'],
cbind: {
title: '{datastore}',
},
bodyPadding: 10,
viewModel: {
data: {
usage: "N/A",
full: "N/A",
history: [],
},
stores: {
historystore: {
data: [],
},
},
},
setTasks: function(taskdata, since) {
let me = this;
me.down('pbsTaskSummary').updateTasks(taskdata, since);
},
setStatus: function(statusData) {
let me = this;
let vm = me.getViewModel();
vm.set('usagetext', PBS.Utils.render_size_usage(statusData.used, statusData.total));
vm.set('usage', statusData.used/statusData.total);
let estimate = PBS.Utils.render_estimate(statusData['estimated-full-date']);
vm.set('full', estimate);
let last = 0;
let data = statusData.history.map((val) => {
if (val === null) {
val = last;
} else {
last = val;
}
return val;
});
let historyStore = vm.getStore('historystore');
historyStore.setData([
{
history: data,
},
]);
},
items: [
{
xtype: 'container',
layout: {
type: 'hbox',
align: 'stretch',
},
defaults: {
flex: 1,
padding: 5,
},
items: [
{
xtype: 'pmxInfoWidget',
iconCls: 'fa fa-fw fa-hdd-o',
title: gettext('Usage'),
bind: {
data: {
usage: '{usage}',
text: '{usagetext}',
},
},
},
{
xtype: 'pmxInfoWidget',
title: gettext('Estimated Full'),
printBar: false,
bind: {
data: {
usage: '0',
text: '{full}',
},
},
},
],
},
{
// we cannot autosize a sparklineline widget,
// abuse a grid with a single column/row to do it for us
xtype: 'grid',
hideHeaders: true,
minHeight: 50,
border: false,
bodyBorder: false,
rowLines: false,
disableSelection: true,
viewConfig: {
trackOver: false,
},
bind: {
store: '{historystore}',
},
columns: [{
xtype: 'widgetcolumn',
flex: 1,
dataIndex: 'history',
widget: {
xtype: 'sparklineline',
bind: '{record.history}',
spotRadius: 0,
fillColor: '#ddd',
lineColor: '#555',
lineWidth: 0,
chartRangeMin: 0,
chartRangeMax: 1,
tipTpl: '{y:number("0.00")*100}%',
height: 40,
},
}],
},
{
xtype: 'pbsTaskSummary',
border: false,
header: false,
subPanelModal: true,
bodyPadding: 0,
minHeight: 0,
cbind: {
datastore: '{datastore}',
},
},
],
});