diff --git a/www/css/ext6-pve.css b/www/css/ext6-pve.css index 6680477b..67bc865d 100644 --- a/www/css/ext6-pve.css +++ b/www/css/ext6-pve.css @@ -532,3 +532,8 @@ table.osds td:first-of-type { .pve-invalid-row { background-color: #f3d6d7; } + +.pve-static-mask div.x-mask-msg-text { + padding: 10px; + background-image: none; +} diff --git a/www/manager6/Makefile b/www/manager6/Makefile index 7eb6b211..6c3efe0d 100644 --- a/www/manager6/Makefile +++ b/www/manager6/Makefile @@ -198,6 +198,7 @@ JSSRC= \ ha/Groups.js \ ha/Fencing.js \ dc/Summary.js \ + grid/Replication.js \ dc/Health.js \ dc/Guests.js \ dc/OptionView.js \ diff --git a/www/manager6/grid/Replication.js b/www/manager6/grid/Replication.js new file mode 100644 index 00000000..99c7ccc5 --- /dev/null +++ b/www/manager6/grid/Replication.js @@ -0,0 +1,486 @@ +Ext.define('PVE.window.ReplicaEdit', { + extend: 'PVE.window.Edit', + xtype: 'pveReplicaEdit', + + subject: gettext('Replication Job'), + + + url: '/cluster/replication', + method: 'POST', + + initComponent: function() { + var me = this; + + var vmid = me.pveSelNode.data.vmid; + var nodename = me.pveSelNode.data.node; + + var items = []; + + items.push({ + xtype: (me.isCreate && !vmid)?'pveGuestIDSelector':'displayfield', + name: 'guest', + fieldLabel: 'CT/VM ID', + value: vmid || '' + }); + + items.push( + { + xtype: me.isCreate ? 'pveNodeSelector':'displayfield', + name: 'target', + disallowedNodes: [nodename], + allowBlank: false, + onlineValidator: true, + fieldLabel: gettext("Target") + }, + { + xtype: 'pveCalendarEvent', + fieldLabel: gettext('Schedule'), + emptyText: '*/15', + name: 'schedule' + }, + { + xtype: 'numberfield', + fieldLabel: gettext('Rate (MB/s)'), + step: 1, + minValue: 1, + emptyText: gettext('unlimited'), + name: 'rate' + }, + { + xtype: 'textfield', + fieldLabel: gettext('Comment'), + name: 'comment' + } + ); + + me.items = [ + { + xtype: 'inputpanel', + itemId: 'ipanel', + + onGetValues: function(values) { + var me = this.up('window'); + + PVE.Utils.delete_if_default(values, 'rate', '', me.isCreate); + PVE.Utils.delete_if_default(values, 'schedule', '*/15', me.isCreate); + PVE.Utils.delete_if_default(values, 'comment', '', me.isCreate); + + if (me.isCreate) { + values.type = 'local'; + var id = -1; + if (me.highestids[values.guest] !== undefined) { + id = me.highestids[values.guest]; + } + id++; + values.id = values.guest + '-' + id.toString(); + delete values.guest; + } + return values; + }, + items: items + } + ]; + + me.callParent(); + + if (me.isCreate) { + me.load({ + success: function(response) { + var jobs = response.result.data; + var highestids = {}; + Ext.Array.forEach(jobs, function(job) { + var match = /^([0-9]+)\-([0-9]+)$/.exec(job.id); + if (match) { + var vmid = parseInt(match[1],10); + var id = parseInt(match[2],10); + if (highestids[vmid] < id || + highestids[vmid] === undefined) { + highestids[vmid] = id; + } + } + }); + + me.highestids = highestids; + } + }); + + } else { + me.load({ + success: function(response, options) { + me.setValues(response.result.data); + me.digest = response.result.data.digest; + } + }); + } + } +}); + +Ext.define('PVE.grid.ReplicaView', { + extend: 'Ext.grid.Panel', + xtype: 'pveReplicaView', + + // not here yet: + //onlineHelp: 'todo', + + stateful: true, + stateId: 'grid-pve-replication-status', + + controller: { + xclass: 'Ext.app.ViewController', + + addJob: function(button,event,rec) { + var me = this.getView(); + var controller = this; + var win = Ext.create('PVE.window.ReplicaEdit', { + isCreate: true, + method: 'POST', + pveSelNode: me.pveSelNode + }); + win.on('destroy', function() { controller.reload(); }); + win.show(); + }, + + editJob: function(button,event,rec) { + var me = this.getView(); + var controller = this; + var data = rec.data; + var win = Ext.create('PVE.window.ReplicaEdit', { + url: '/cluster/replication/' + data.id, + method: 'PUT', + pveSelNode: me.pveSelNode + }); + win.on('destroy', function() { controller.reload(); }); + win.show(); + }, + + removeJob: function(button,event,rec) { + var me = this.getView(); + var controller = this; + PVE.Utils.API2Request({ + url: '/api2/extjs/cluster/replication/' + rec.data.id, + waitMsgTarget: me, + method: 'DELETE', + callback: function() { controller.reload(); }, + failure: function (response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + } + }); + }, + + showLog: function(button, event, rec) { + var me = this.getView(); + var controller = this; + var logView = Ext.create('PVE.panel.LogView', { + border: false, + url: "/api2/extjs/nodes/" + me.nodename + "/replication/" + rec.data.id + "/log" + }); + var win = Ext.create('Ext.window.Window', { + items: [ logView ], + layout: 'fit', + width: 800, + height: 400, + modal: true, + title: gettext("Replication Log") + }); + var task = { + run: function() { + logView.requestUpdate(); + }, + interval: 1000 + }; + Ext.TaskManager.start(task); + win.on('destroy', function() { + Ext.TaskManager.stop(task); + controller.reload(); + }); + win.show(); + }, + + reload: function() { + var me = this.getView(); + me.rstore.load(); + }, + + dblClick: function(grid, record, item) { + var me = this; + me.editJob(undefined, undefined, record); + }, + + // check for cluster + // currently replication is for cluster only, so we disable the whole + // component + checkPrerequisites: function() { + var me = this.getView(); + if (PVE.data.ResourceStore.getNodes().length < 2) { + me.mask(gettext("Replication needs at least two nodes"), ['pve-static-mask']); + } + }, + + control: { + '#': { + itemdblclick: 'dblClick', + afterlayout: 'checkPrerequisites' + } + } + }, + + tbar: [ + { + text: gettext('Add'), + itemId: 'addButton', + handler: 'addJob' + }, + { + xtype: 'pveButton', + text: gettext('Edit'), + itemId: 'editButton', + handler: 'editJob', + disabled: true + }, + { + xtype: 'pveButton', + text: gettext('Remove'), + itemId: 'removeButton', + handler: 'removeJob', + dangerous: true, + confirmMsg: function(rec) { + var msg = Ext.String.format(gettext('Are you sure you want to remove replication job {0}'), + "'" + rec.id + "'" + '
' + + gettext('(Note: Removal of replication job has to be replicated)')); + return msg; + }, + disabled: true + }, + { + xtype: 'pveButton', + text: gettext('Log'), + itemId: 'logButton', + handler: 'showLog', + disabled: true + } + ], + + initComponent: function() { + var me = this; + var mode = ''; + var url = '/cluster/replication'; + + me.nodename = me.pveSelNode.data.node; + me.vmid = me.pveSelNode.data.vmid; + + me.columns = [ + { + text: gettext('ID'), + dataIndex: 'id', + width: 60, + hidden: true + }, + { + text: gettext('Guest'), + dataIndex: 'guest', + width: 75 + }, + { + text: gettext('Job'), + dataIndex: 'jobnum', + width: 65 + }, + { + text: gettext('Target'), + dataIndex: 'target' + } + ]; + + if (!me.nodename) { + mode = 'dc'; + me.stateId = 'grid-pve-replication-dc'; + } else if (!me.vmid) { + mode = 'node'; + url = '/nodes/' + me.nodename + '/replication'; + } else { + mode = 'vm'; + url = '/nodes/' + me.nodename + '/replication' + '?guest=' + me.vmid; + } + + if (mode !== 'dc') { + me.columns.push( + { + text: gettext('Status'), + dataIndex: 'state', + width: 60, + renderer: function(value, metadata, record) { + + if (record.data.pid) { + metadata.tdCls = 'x-grid-row-loading'; + return ''; + } + + var states = []; + + if (record.data.remove_job) { + states.push(''); + } + + if (record.data.error) { + states.push(''); + } + + if (states.length > 0) { + return states.join(','); + } + + return ''; + } + }, + { + text: gettext('Status Text'), + dataIndex: 'error', + minWidth: 100, + flex: 1, + renderer: function(value, metadata, record) { + var states = []; + + if (record.data.remove_job) { + states.push(gettext("Removal Scheduled")); + } + + if (record.data.error) { + states.push(record.data.error); + } + + if (states.length > 0) { + return states.join(', '); + } + + return '-'; + } + }, + { + text: gettext('Last Sync'), + dataIndex: 'last_sync', + renderer: function(value, metadata, record) { + if (!value) { + return '-'; + } + + if (record.data.pid) { + return gettext('syncing'); + } + + return PVE.Utils.render_timestamp(value); + } + }, + { + text: gettext('Duration'), + dataIndex: 'duration', + renderer: PVE.Utils.render_duration + }, + { + text: gettext('Next Sync'), + dataIndex: 'next_sync', + renderer: function(value) { + if (!value) { + return '-'; + } + + var now = new Date(); + var next = new Date(value*1000); + + if (next < now) { + return gettext('now'); + } + + return PVE.Utils.render_timestamp(value); + } + } + ); + } + + me.columns.push( + { + text: gettext('Schedule'), + dataIndex: 'schedule' + }, + { + text: gettext('Rate'), + dataIndex: 'rate', + renderer: function(value) { + if (!value) { + return gettext('unlimited'); + } + + return value.toString() + ' MB/s'; + }, + hidden: true + }, + { + text: gettext('Comment'), + dataIndex: 'comment', + renderer: Ext.htmlEncode + } + ); + + me.rstore = Ext.create('PVE.data.UpdateStore', { + storeid: 'pve-replica-' + me.nodename + me.vmid, + model: (mode === 'dc')? 'pve-replication' : 'pve-replication-state', + interval: 3000, + proxy: { + type: 'pve', + url: "/api2/json" + url + } + }); + + me.store = Ext.create('PVE.data.DiffStore', { + rstore: me.rstore, + sorters: [ + { + property: 'guest' + }, + { + property: 'jobnum' + } + ] + }); + + me.callParent(); + + // we cannot access the log in the datacenter, because + // we do not know where/if the jobs runs + if (mode === 'dc') { + me.down('#logButton').setHidden(true); + } + + // if we set the warning mask, we do not want to load + // or set the mask on store errors + if (PVE.data.ResourceStore.getNodes().length < 2) { + return; + } + + PVE.Utils.monStoreErrors(me, me.rstore); + + me.on('destroy', me.rstore.stopUpdate); + me.rstore.startUpdate(); + } +}, function() { + + Ext.define('pve-replication', { + extend: 'Ext.data.Model', + fields: [ + 'id', 'target', 'comment', 'rate', 'type', + { name: 'guest', type: 'integer' }, + { name: 'jobnum', type: 'integer' }, + { name: 'schedule', defaultValue: '*/15' } + ] + }); + + Ext.define('pve-replication-state', { + extend: 'pve-replication', + fields: [ + 'last_sync', 'next_sync', 'error', 'duration', 'state', + 'fail_count', 'remove_job', 'pid' + ] + }); + +});