pve-manager/www/manager6/ceph/Status.js
Thomas Lamprecht 3527924585 ui: ceph status: use two column layout for 1600+ window width
Looks already OK at that size, and one gets a better overview.

We have a slightly complex layout here (to columns which should be
above each other) so we cannot just use the generic helper, but
that's OK here - it *is* a special view.

Note, not all people use full-sized windows all the time, so the
widths here must not only be considered in terms of display
resolutions...

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-04-26 12:31:11 +02:00

417 lines
9.9 KiB
JavaScript

Ext.define('PVE.node.CephStatus', {
extend: 'Ext.panel.Panel',
alias: 'widget.pveNodeCephStatus',
onlineHelp: 'chapter_pveceph',
scrollable: true,
bodyPadding: 5,
layout: {
type: 'column',
},
defaults: {
padding: 5,
},
items: [
{
xtype: 'panel',
title: gettext('Health'),
bodyPadding: 10,
plugins: 'responsive',
responsiveConfig: {
'width < 1600': {
minHeight: 230,
columnWidth: 1,
},
'width >= 1600': {
minHeight: 500,
columnWidth: 0.5,
},
},
layout: {
type: 'hbox',
align: 'stretch',
},
items: [
{
xtype: 'container',
layout: {
type: 'vbox',
align: 'stretch',
},
flex: 1,
items: [
{
flex: 1,
itemId: 'overallhealth',
xtype: 'pveHealthWidget',
title: gettext('Status'),
},
{
itemId: 'versioninfo',
xtype: 'displayfield',
fieldLabel: gettext('Ceph Version'),
value: "",
autoEl: {
tag: 'div',
'data-qtip': gettext('The newest version installed in the Cluster.'),
},
padding: '10 0 0 0',
style: {
'text-align': 'center',
},
},
],
},
{
flex: 2,
itemId: 'warnings',
stateful: true,
stateId: 'ceph-status-warnings',
xtype: 'grid',
// we load the store manually, to show an emptyText specify an empty intermediate store
store: {
trackRemoved: false,
data: [],
},
emptyText: gettext('No Warnings/Errors'),
columns: [
{
dataIndex: 'severity',
header: gettext('Severity'),
align: 'center',
width: 70,
renderer: function(value) {
let health = PVE.Utils.map_ceph_health[value];
let icon = PVE.Utils.get_health_icon(health);
return `<i class="fa fa-fw ${icon}'"></i>`;
},
sorter: {
sorterFn: function(a, b) {
let health = ['HEALTH_ERR', 'HEALTH_WARN', 'HEALTH_OK'];
return health.indexOf(b.data.severity) - health.indexOf(a.data.severity);
},
},
},
{
dataIndex: 'summary',
header: gettext('Summary'),
flex: 1,
},
{
xtype: 'actioncolumn',
width: 40,
align: 'center',
tooltip: gettext('Detail'),
items: [
{
iconCls: 'x-fa fa-info-circle',
handler: function(grid, rowindex, colindex, item, e, record) {
var win = Ext.create('Ext.window.Window', {
title: gettext('Detail'),
resizable: true,
modal: true,
width: 650,
height: 400,
layout: {
type: 'fit',
},
items: [{
scrollable: true,
padding: 10,
xtype: 'box',
html: [
'<span>' + Ext.htmlEncode(record.data.summary) + '</span>',
'<pre>' + Ext.htmlEncode(record.data.detail) + '</pre>',
],
}],
});
win.show();
},
},
],
},
],
},
],
},
{
xtype: 'pveCephStatusDetail',
itemId: 'statusdetail',
plugins: 'responsive',
responsiveConfig: {
'width < 1600': {
columnWidth: 1,
minHeight: 250,
},
'width >= 1600': {
columnWidth: 0.5,
minHeight: 300,
},
},
title: gettext('Status'),
},
{
title: gettext('Services'),
xtype: 'pveCephServices',
itemId: 'services',
plugins: 'responsive',
layout: {
type: 'hbox',
align: 'stretch',
},
responsiveConfig: {
'width < 1600': {
columnWidth: 1,
minHeight: 200,
},
'width >= 1600': {
columnWidth: 0.5,
minHeight: 200,
},
},
},
{
xtype: 'panel',
title: gettext('Performance'),
columnWidth: 1,
bodyPadding: 5,
layout: {
type: 'hbox',
align: 'center',
},
items: [
{
flex: 1,
xtype: 'container',
items: [
{
xtype: 'proxmoxGauge',
itemId: 'space',
title: gettext('Usage'),
},
{
flex: 1,
border: false,
},
{
xtype: 'container',
itemId: 'recovery',
hidden: true,
padding: 25,
items: [
{
itemId: 'recoverychart',
xtype: 'pveRunningChart',
title: gettext('Recovery') +'/ '+ gettext('Rebalance'),
renderer: PVE.Utils.render_bandwidth,
height: 100,
},
{
xtype: 'progressbar',
itemId: 'recoveryprogress',
},
],
},
],
},
{
flex: 2,
xtype: 'container',
defaults: {
padding: 0,
height: 100,
},
items: [
{
itemId: 'reads',
xtype: 'pveRunningChart',
title: gettext('Reads'),
renderer: PVE.Utils.render_bandwidth,
},
{
itemId: 'writes',
xtype: 'pveRunningChart',
title: gettext('Writes'),
renderer: PVE.Utils.render_bandwidth,
},
{
itemId: 'readiops',
xtype: 'pveRunningChart',
title: 'IOPS: ' + gettext('Reads'),
renderer: Ext.util.Format.numberRenderer('0,000'),
},
{
itemId: 'writeiops',
xtype: 'pveRunningChart',
title: 'IOPS: ' + gettext('Writes'),
renderer: Ext.util.Format.numberRenderer('0,000'),
},
],
},
],
},
],
generateCheckData: function(health) {
var result = [];
let checks = health.checks || {};
Object.keys(checks).sort().forEach(key => {
let check = checks[key];
result.push({
id: key,
summary: check.summary.message,
detail: check.detail.reduce((acc, v) => `${acc}\n${v.message}`, ''),
severity: check.severity,
});
});
return result;
},
updateAll: function(store, records, success) {
if (!success || records.length === 0) {
return;
}
var me = this;
var rec = records[0];
me.status = rec.data;
// add health panel
me.down('#overallhealth').updateHealth(PVE.Utils.render_ceph_health(rec.data.health || {}));
// add errors to gridstore
me.down('#warnings').getStore().loadRawData(me.generateCheckData(rec.data.health || {}), false);
// update services
me.getComponent('services').updateAll(me.metadata || {}, rec.data);
// update detailstatus panel
me.getComponent('statusdetail').updateAll(me.metadata || {}, rec.data);
// add performance data
let pgmap = rec.data.pgmap;
let used = pgmap.bytes_used;
let total = pgmap.bytes_total;
var text = Ext.String.format(gettext('{0} of {1}'),
Proxmox.Utils.render_size(used),
Proxmox.Utils.render_size(total),
);
// update the usage widget
me.down('#space').updateValue(used/total, text);
let readiops = pgmap.read_op_per_sec;
let writeiops = pgmap.write_op_per_sec;
let reads = pgmap.read_bytes_sec || 0;
let writes = pgmap.write_bytes_sec || 0;
// update the graphs
me.reads.addDataPoint(reads);
me.writes.addDataPoint(writes);
me.readiops.addDataPoint(readiops);
me.writeiops.addDataPoint(writeiops);
let degraded = pgmap.degraded_objects || 0;
let misplaced = pgmap.misplaced_objects || 0;
let unfound = pgmap.unfound_objects || 0;
let unhealthy = degraded + unfound + misplaced;
// update recovery
if (pgmap.recovering_objects_per_sec !== undefined || unhealthy > 0) {
let toRecover = pgmap.misplaced_total || pgmap.unfound_total || pgmap.degraded_total || 0;
if (toRecover === 0) {
return; // FIXME: unexpected return and leaves things possible visible when it shouldn't?
}
let recovered = toRecover - unhealthy || 0;
let speed = pgmap.recovering_bytes_per_sec || 0;
let recoveryRatio = recovered / total;
let txt = `${(recoveryRatio * 100).toFixed(2)}%`;
if (speed > 0) {
let obj_per_sec = speed / (4 * 1024 * 1024); // 4 MiB per Object
let duration = Proxmox.Utils.format_duration_human(unhealthy/obj_per_sec);
let speedTxt = PVE.Utils.render_bandwidth(speed);
txt += ` (${speedTxt} - ${duration} left)`;
}
me.down('#recovery').setVisible(true);
me.down('#recoveryprogress').updateValue(recoveryRatio);
me.down('#recoveryprogress').updateText(txt);
me.down('#recoverychart').addDataPoint(speed);
} else {
me.down('#recovery').setVisible(false);
me.down('#recoverychart').addDataPoint(0);
}
},
initComponent: function() {
var me = this;
var nodename = me.pveSelNode.data.node;
me.callParent();
var baseurl = '/api2/json' + (nodename ? '/nodes/' + nodename : '/cluster') + '/ceph';
me.store = Ext.create('Proxmox.data.UpdateStore', {
storeid: 'ceph-status-' + (nodename || 'cluster'),
interval: 5000,
proxy: {
type: 'proxmox',
url: baseurl + '/status',
},
});
me.metadatastore = Ext.create('Proxmox.data.UpdateStore', {
storeid: 'ceph-metadata-' + (nodename || 'cluster'),
interval: 15*1000,
proxy: {
type: 'proxmox',
url: '/api2/json/cluster/ceph/metadata',
},
});
// save references for the updatefunction
me.iops = me.down('#iops');
me.readiops = me.down('#readiops');
me.writeiops = me.down('#writeiops');
me.reads = me.down('#reads');
me.writes = me.down('#writes');
// manages the "install ceph?" overlay
PVE.Utils.monitor_ceph_installed(me, me.store, nodename);
me.mon(me.store, 'load', me.updateAll, me);
me.mon(me.metadatastore, 'load', function(store, records, success) {
if (!success || records.length < 1) {
return;
}
me.metadata = records[0].data;
// update services
me.getComponent('services').updateAll(me.metadata, me.status || {});
// update detailstatus panel
me.getComponent('statusdetail').updateAll(me.metadata, me.status || {});
let maxversion = [];
let maxversiontext = "";
for (const [_nodename, data] of Object.entries(me.metadata.node)) {
let version = data.version.parts;
if (PVE.Utils.compare_ceph_versions(version, maxversion) > 0) {
maxversion = version;
maxversiontext = data.version.str;
}
}
me.down('#versioninfo').setValue(maxversiontext);
}, me);
me.on('destroy', me.store.stopUpdate);
me.on('destroy', me.metadatastore.stopUpdate);
me.store.startUpdate();
me.metadatastore.startUpdate();
},
});