diff --git a/www/Makefile b/www/Makefile index f9d8f8e4..192341ec 100644 --- a/www/Makefile +++ b/www/Makefile @@ -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 \ diff --git a/www/datastore/DataStoreList.js b/www/datastore/DataStoreList.js new file mode 100644 index 00000000..71dd5fdb --- /dev/null +++ b/www/datastore/DataStoreList.js @@ -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', + }, + ], +}); diff --git a/www/datastore/DataStoreListSummary.js b/www/datastore/DataStoreListSummary.js new file mode 100644 index 00000000..a9018a7c --- /dev/null +++ b/www/datastore/DataStoreListSummary.js @@ -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}', + }, + }, + ], +});