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, mountpoint: "", }, }, controller: { xclass: 'Ext.app.ViewController', onLoad: function(store, data, success) { let me = this; if (!success) { Proxmox.Utils.API2Request({ url: `/config/datastore/${me.view.datastore}`, success: function(response) { const config = response.result.data; if (config['maintenance-mode']) { const [_type, msg] = config['maintenance-mode'].split(/,(.+)/); const message = msg ? ': ' + msg.split("=")[1] .replace(/^"(.*)"$/, '$1') .replaceAll('\\"', '"') : ''; me.view.el.mask( `${gettext('Datastore is in maintenance mode')}${message}`, 'fa pbs-maintenance-mask', ); } else { me.view.el.mask(gettext('Datastore is not available')); } }, }); return; } me.view.el.unmask(); 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 usage = PBS.Utils.render_size_usage(used, total); vm.set('usagetext', usage); vm.set('usage', used/total); let gcstatus = store.getById('gc-status').data.value; let dedup = PBS.Utils.calculate_dedup_factor(gcstatus); 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']); }, 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/?verbose=true`, }); me.store.on('load', me.onLoad, me); }, }, listeners: { activate: 'startStore', beforedestroy: '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: `${gettext('Backup Count')}`, 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: `${gettext('Stats from last Garbage Collection')}`, padding: '10 0 5 0', }, { iconCls: 'fa fa-fw fa-compress', title: gettext('Deduplication Factor'), printBar: false, bind: { data: { text: '{deduplication}', }, }, }, { 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', itemId: 'ioDelayChart', hidden: true, 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(); }, resize: function(panel) { Proxmox.Utils.updateColumns(panel); }, }, 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(); let sp = Ext.state.Manager.getProvider(); me.mon(sp, 'statechange', function(provider, key, value) { if (key !== 'summarycolumns') { return; } if (!me.rendered) { return; } Proxmox.Utils.updateColumns(me); }); 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); }, failure: function(response) { // fallback if e.g. we have no permissions to the config let rec = Ext.getStore('pbs-datastore-list') .findRecord('store', me.datastore, 0, false, true, true); if (rec) { me.down('pbsDataStoreNotes').setNotes(rec.data.comment || ""); } }, }); me.mon(me.rrdstore, 'load', function(store, records, success) { me.down('#ioDelayChart').setVisible(!success || records[0]?.data?.io_ticks !== undefined); }, undefined, { single: true }); me.query('proxmoxRRDChart').forEach((chart) => { chart.setStore(me.rrdstore); }); me.down('pbsDataStoreInfo').relayEvents(me, ['activate', 'deactivate']); }, });