From 06694509392b752365dbd2e8e02cd416ed4058c9 Mon Sep 17 00:00:00 2001 From: Dietmar Maurer Date: Mon, 30 Jan 2017 13:40:51 +0100 Subject: [PATCH] add TimeView, TimeEdit and TaskViewer --- Makefile | 7 +- Utils.js | 55 ++++++++ data/ProxmoxProxy.js | 8 ++ grid/ObjectGrid.js | 131 +++++++++++++++++++ node/TimeEdit.js | 38 ++++++ node/TimeView.js | 56 +++++++++ window/Edit.js | 293 +++++++++++++++++++++++++++++++++++++++++++ window/TaskViewer.js | 224 +++++++++++++++++++++++++++++++++ 8 files changed, 811 insertions(+), 1 deletion(-) create mode 100644 grid/ObjectGrid.js create mode 100644 node/TimeEdit.js create mode 100644 node/TimeView.js create mode 100644 window/Edit.js create mode 100644 window/TaskViewer.js diff --git a/Makefile b/Makefile index 46e3dde..7793761 100644 --- a/Makefile +++ b/Makefile @@ -18,7 +18,12 @@ JSSRC= \ data/UpdateStore.js \ data/DiffStore.js \ data/ObjectStore.js \ - data/TimezoneStore.js + data/TimezoneStore.js \ + grid/ObjectGrid.js \ + window/Edit.js \ + window/TaskViewer.js \ + node/TimeEdit.js \ + node/TimeView.js all: diff --git a/Utils.js b/Utils.js index 2960ce7..fa27fe0 100644 --- a/Utils.js +++ b/Utils.js @@ -182,6 +182,61 @@ Ext.define('Proxmox.Utils', { utilities: { Ext.Ajax.request(newopts); }, + assemble_field_data: function(values, data) { + if (Ext.isObject(data)) { + Ext.Object.each(data, function(name, val) { + if (values.hasOwnProperty(name)) { + var bucket = values[name]; + if (!Ext.isArray(bucket)) { + bucket = values[name] = [bucket]; + } + if (Ext.isArray(val)) { + values[name] = bucket.concat(val); + } else { + bucket.push(val); + } + } else { + values[name] = val; + } + }); + } + }, + + dialog_title: function(subject, create, isAdd) { + if (create) { + if (isAdd) { + return gettext('Add') + ': ' + subject; + } else { + return gettext('Create') + ': ' + subject; + } + } else { + return gettext('Edit') + ': ' + subject; + } + }, + + parse_task_upid: function(upid) { + var task = {}; + + var res = upid.match(/^UPID:(\S+):([0-9A-Fa-f]{8}):([0-9A-Fa-f]{8,9}):([0-9A-Fa-f]{8}):([^:\s]+):([^:\s]*):([^:\s]+):$/); + if (!res) { + throw "unable to parse upid '" + upid + "'"; + } + task.node = res[1]; + task.pid = parseInt(res[2], 16); + task.pstart = parseInt(res[3], 16); + task.starttime = parseInt(res[4], 16); + task.type = res[5]; + task.id = res[6]; + task.user = res[7]; + + return task; + }, + + render_timestamp: function(value, metaData, record, rowIndex, colIndex, store) { + var servertime = new Date(value * 1000); + return Ext.Date.format(servertime, 'Y-m-d H:i:s'); + }, + }, singleton: true, diff --git a/data/ProxmoxProxy.js b/data/ProxmoxProxy.js index 6fe1303..ff6fbee 100644 --- a/data/ProxmoxProxy.js +++ b/data/ProxmoxProxy.js @@ -26,4 +26,12 @@ Ext.define('Proxmox.RestProxy', { this.callParent([config]); } +}, function() { + + Ext.define('KeyValue', { + extend: "Ext.data.Model", + fields: [ 'key', 'value' ], + idProperty: 'key' + }); + }); diff --git a/grid/ObjectGrid.js b/grid/ObjectGrid.js new file mode 100644 index 0000000..7f221eb --- /dev/null +++ b/grid/ObjectGrid.js @@ -0,0 +1,131 @@ +/* Renders a list of key values objets + +mandatory config parameters: +rows: an object container where each propery is a key-value object we want to render + var rows = { + keyboard: { + header: gettext('Keyboard Layout'), + editor: 'Your.KeyboardEdit', + required: true + }, + +optional: +disabled: setting this parameter to true will disable selection and focus on the +proxmoxObjectGrid as well as greying out input elements. +Useful for a readonly tabular display + +*/ + +Ext.define('Proxmox.grid.ObjectGrid', { + extend: 'Ext.grid.GridPanel', + alias: ['widget.proxmoxObjectGrid'], + disabled: false, + hideHeaders: true, + + getObjectValue: function(key, defaultValue) { + var me = this; + var rec = me.store.getById(key); + if (rec) { + return rec.data.value; + } + return defaultValue; + }, + + renderKey: function(key, metaData, record, rowIndex, colIndex, store) { + var me = this; + var rows = me.rows; + var rowdef = (rows && rows[key]) ? rows[key] : {}; + return rowdef.header || key; + }, + + renderValue: function(value, metaData, record, rowIndex, colIndex, store) { + var me = this; + var rows = me.rows; + var key = record.data.key; + var rowdef = (rows && rows[key]) ? rows[key] : {}; + + var renderer = rowdef.renderer; + if (renderer) { + return renderer(value, metaData, record, rowIndex, colIndex, store); + } + + return value; + }, + + initComponent : function() { + var me = this; + + var rows = me.rows; + + if (!me.rstore) { + if (!me.url) { + throw "no url specified"; + } + + me.rstore = Ext.create('Proxmox.data.ObjectStore', { + url: me.url, + interval: me.interval, + extraParams: me.extraParams, + rows: me.rows + }); + } + + var rstore = me.rstore; + + var store = Ext.create('Proxmox.data.DiffStore', { rstore: rstore, + sorters: [], + filters: [] + }); + + if (rows) { + Ext.Object.each(rows, function(key, rowdef) { + if (Ext.isDefined(rowdef.defaultValue)) { + store.add({ key: key, value: rowdef.defaultValue }); + } else if (rowdef.required) { + store.add({ key: key, value: undefined }); + } + }); + } + + if (me.sorterFn) { + store.sorters.add(Ext.create('Ext.util.Sorter', { + sorterFn: me.sorterFn + })); + } + + store.filters.add(Ext.create('Ext.util.Filter', { + filterFn: function(item) { + if (rows) { + var rowdef = rows[item.data.key]; + if (!rowdef || (rowdef.visible === false)) { + return false; + } + } + return true; + } + })); + + Proxmox.Utils.monStoreErrors(me, rstore); + + Ext.applyIf(me, { + store: store, + stateful: false, + columns: [ + { + header: gettext('Name'), + width: me.cwidth1 || 200, + dataIndex: 'key', + renderer: me.renderKey + }, + { + flex: 1, + header: gettext('Value'), + dataIndex: 'value', + renderer: me.renderValue + } + ] + }); + + me.callParent(); + } +}); diff --git a/node/TimeEdit.js b/node/TimeEdit.js new file mode 100644 index 0000000..605210d --- /dev/null +++ b/node/TimeEdit.js @@ -0,0 +1,38 @@ +Ext.define('Proxmox.node.TimeEdit', { + extend: 'Proxmox.window.Edit', + alias: ['widget.proxmoxNodeTimeEdit'], + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + Ext.applyIf(me, { + subject: gettext('Time zone'), + url: "/api2/extjs/nodes/" + me.nodename + "/time", + fieldDefaults: { + labelWidth: 70 + }, + width: 400, + items: { + xtype: 'combo', + fieldLabel: gettext('Time zone'), + name: 'timezone', + queryMode: 'local', + store: Ext.create('Proxmox.data.TimezoneStore'), + valueField: 'zone', + displayField: 'zone', + triggerAction: 'all', + forceSelection: true, + editable: false, + allowBlank: false + } + }); + + me.callParent(); + + me.load(); + } +}); diff --git a/node/TimeView.js b/node/TimeView.js new file mode 100644 index 0000000..6c79e2b --- /dev/null +++ b/node/TimeView.js @@ -0,0 +1,56 @@ +Ext.define('Proxmox.node.TimeView', { + extend: 'Proxmox.grid.ObjectGrid', + alias: ['widget.proxmoxNodeTimeView'], + + initComponent : function() { + var me = this; + + if (!me.nodename) { + throw "no node name specified"; + } + + var tzoffset = (new Date()).getTimezoneOffset()*60000; + var renderlocaltime = function(value) { + var servertime = new Date((value * 1000) + tzoffset); + return Ext.Date.format(servertime, 'Y-m-d H:i:s'); + }; + + var run_editor = function() { + var win = Ext.create('Proxmox.node.TimeEdit', { + nodename: me.nodename + }); + win.show(); + }; + + Ext.apply(me, { + url: "/api2/json/nodes/" + me.nodename + "/time", + cwidth1: 150, + interval: 1000, + rows: { + timezone: { + header: gettext('Time zone'), + required: true + }, + localtime: { + header: gettext('Server time'), + required: true, + renderer: renderlocaltime + } + }, + tbar: [ + { + text: gettext("Edit"), + handler: run_editor + } + ], + listeners: { + itemdblclick: run_editor + } + }); + + me.callParent(); + + me.on('activate', me.rstore.startUpdate); + me.on('destroy', me.rstore.stopUpdate); + } +}); diff --git a/window/Edit.js b/window/Edit.js new file mode 100644 index 0000000..967f9d7 --- /dev/null +++ b/window/Edit.js @@ -0,0 +1,293 @@ +// fixme: how can we avoid those lint errors? +/*jslint confusion: true */ +Ext.define('Proxmox.window.Edit', { + extend: 'Ext.window.Window', + alias: 'widget.proxmoxWindowEdit', + + resizable: false, + + // use this tio atimatically generate a title like + // Create: + subject: undefined, + + // set create to true if you want a Create button (instead + // OK and RESET) + create: false, + + // set to true if you want an Add button (instead of Create) + isAdd: false, + + // set to true if you want an Remove button (instead of Create) + isRemove: false, + + backgroundDelay: 0, + + showProgress: false, + + isValid: function() { + var me = this; + + var form = me.formPanel.getForm(); + return form.isValid(); + }, + + getValues: function(dirtyOnly) { + var me = this; + + var values = {}; + + var form = me.formPanel.getForm(); + + form.getFields().each(function(field) { + if (!field.up('inputpanel') && (!dirtyOnly || field.isDirty())) { + Proxmox.Utils.assemble_field_data(values, field.getSubmitData()); + } + }); + + Ext.Array.each(me.query('inputpanel'), function(panel) { + Proxmox.Utils.assemble_field_data(values, panel.getValues(dirtyOnly)); + }); + + return values; + }, + + setValues: function(values) { + var me = this; + + var form = me.formPanel.getForm(); + + Ext.iterate(values, function(fieldId, val) { + var field = form.findField(fieldId); + if (field && !field.up('inputpanel')) { + field.setValue(val); + if (form.trackResetOnLoad) { + field.resetOriginalValue(); + } + } + }); + + Ext.Array.each(me.query('inputpanel'), function(panel) { + panel.setValues(values); + }); + }, + + submit: function() { + var me = this; + + var form = me.formPanel.getForm(); + + var values = me.getValues(); + Ext.Object.each(values, function(name, val) { + if (values.hasOwnProperty(name)) { + if (Ext.isArray(val) && !val.length) { + values[name] = ''; + } + } + }); + + if (me.digest) { + values.digest = me.digest; + } + + if (me.backgroundDelay) { + values.background_delay = me.backgroundDelay; + } + + var url = me.url; + if (me.method === 'DELETE') { + url = url + "?" + Ext.Object.toQueryString(values); + values = undefined; + } + + Proxmox.Utils.API2Request({ + url: url, + waitMsgTarget: me, + method: me.method || (me.backgroundDelay ? 'POST' : 'PUT'), + params: values, + failure: function(response, options) { + if (response.result && response.result.errors) { + form.markInvalid(response.result.errors); + } + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + }, + success: function(response, options) { + var hasProgressBar = (me.backgroundDelay || me.showProgress) && + response.result.data ? true : false; + + if (hasProgressBar) { + // stay around so we can trigger our close events + // when background action is completed + me.hide(); + + var upid = response.result.data; + var win = Ext.create('PVE.window.TaskProgress', { + upid: upid, + listeners: { + destroy: function () { + me.close(); + } + } + }); + win.show(); + } else { + me.close(); + } + } + }); + }, + + load: function(options) { + var me = this; + + var form = me.formPanel.getForm(); + + options = options || {}; + + var newopts = Ext.apply({ + waitMsgTarget: me + }, options); + + var createWrapper = function(successFn) { + Ext.apply(newopts, { + url: me.url, + method: 'GET', + success: function(response, opts) { + form.clearInvalid(); + me.digest = response.result.data.digest; + if (successFn) { + successFn(response, opts); + } else { + me.setValues(response.result.data); + } + // hack: fix ExtJS bug + Ext.Array.each(me.query('radiofield'), function(f) { + f.resetOriginalValue(); + }); + }, + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus, function() { + me.close(); + }); + } + }); + }; + + createWrapper(options.success); + + Proxmox.Utils.API2Request(newopts); + }, + + initComponent : function() { + var me = this; + + if (!me.url) { + throw "no url specified"; + } + + var items = Ext.isArray(me.items) ? me.items : [ me.items ]; + + me.items = undefined; + + me.formPanel = Ext.create('Ext.form.Panel', { + url: me.url, + method: me.method || 'PUT', + trackResetOnLoad: true, + bodyPadding: 10, + border: false, + defaults: { + border: false + }, + fieldDefaults: Ext.apply({}, me.fieldDefaults, { + labelWidth: 100, + anchor: '100%' + }), + items: items + }); + + var form = me.formPanel.getForm(); + + var submitText; + if (me.create) { + if (me.isAdd) { + submitText = gettext('Add'); + } else if (me.isRemove) { + submitText = gettext('Remove'); + } else { + submitText = gettext('Create'); + } + } else { + submitText = gettext('OK'); + } + + var submitBtn = Ext.create('Ext.Button', { + text: submitText, + disabled: !me.create, + handler: function() { + me.submit(); + } + }); + + var resetBtn = Ext.create('Ext.Button', { + text: 'Reset', + disabled: true, + handler: function(){ + form.reset(); + } + }); + + var set_button_status = function() { + var valid = form.isValid(); + var dirty = form.isDirty(); + submitBtn.setDisabled(!valid || !(dirty || me.create)); + resetBtn.setDisabled(!dirty); + }; + + form.on('dirtychange', set_button_status); + form.on('validitychange', set_button_status); + + var colwidth = 300; + if (me.fieldDefaults && me.fieldDefaults.labelWidth) { + colwidth += me.fieldDefaults.labelWidth - 100; + } + + + var twoColumn = items[0].column1 || items[0].column2; + + if (me.subject && !me.title) { + me.title = Proxmox.Utils.dialog_title(me.subject, me.create, me.isAdd); + } + + if (me.create) { + me.buttons = [ submitBtn ] ; + } else { + me.buttons = [ submitBtn, resetBtn ]; + } + + if (items[0].onlineHelp) { + var helpButton = Ext.create('PVE.button.Help'); + me.buttons.unshift(helpButton, '->'); + Ext.GlobalEvents.fireEvent('pveShowHelp', items[0].onlineHelp); + } + + Ext.applyIf(me, { + modal: true, + width: twoColumn ? colwidth*2 : colwidth, + border: false, + items: [ me.formPanel ] + }); + + me.callParent(); + + // always mark invalid fields + me.on('afterlayout', function() { + // on touch devices, the isValid function + // triggers a layout, which triggers an isValid + // and so on + // to prevent this we disable the layouting here + // and enable it afterwards + me.suspendLayout = true; + me.isValid(); + me.suspendLayout = false; + }); + } +}); diff --git a/window/TaskViewer.js b/window/TaskViewer.js new file mode 100644 index 0000000..597404f --- /dev/null +++ b/window/TaskViewer.js @@ -0,0 +1,224 @@ +Ext.define('Proxmox.window.TaskProgress', { + extend: 'Ext.window.Window', + alias: 'widget.proxmoxTaskProgress', + + initComponent: function() { + var me = this; + + if (!me.upid) { + throw "no task specified"; + } + + var task = Proxmox.Utils.parse_task_upid(me.upid); + + var statstore = Ext.create('Proxmox.data.ObjectStore', { + url: "/api2/json/nodes/" + task.node + "/tasks/" + me.upid + "/status", + interval: 1000, + rows: { + status: { defaultValue: 'unknown' }, + exitstatus: { defaultValue: 'unknown' } + } + }); + + me.on('destroy', statstore.stopUpdate); + + var getObjectValue = function(key, defaultValue) { + var rec = statstore.getById(key); + if (rec) { + return rec.data.value; + } + return defaultValue; + }; + + var pbar = Ext.create('Ext.ProgressBar', { text: 'running...' }); + + me.mon(statstore, 'load', function() { + var status = getObjectValue('status'); + if (status === 'stopped') { + var exitstatus = getObjectValue('exitstatus'); + if (exitstatus == 'OK') { + pbar.reset(); + pbar.updateText("Done!"); + Ext.Function.defer(me.close, 1000, me); + } else { + me.close(); + Ext.Msg.alert('Task failed', exitstatus); + } + } + }); + + // fixme: ?? + //var descr = Proxmox.Utils.format_task_description(task.type, task.id); + + Ext.apply(me, { + title: "Task: " + me.upid, + width: 300, + layout: 'auto', + modal: true, + bodyPadding: 5, + items: pbar, + buttons: [ + { + text: gettext('Details'), + handler: function() { + var win = Ext.create('Proxmox.window.TaskViewer', { + upid: me.upid + }); + win.show(); + me.close(); + } + } + ] + }); + + me.callParent(); + + statstore.startUpdate(); + + pbar.wait(); + } +}); + +// fixme: how can we avoid those lint errors? +/*jslint confusion: true */ + +Ext.define('Proxmox.window.TaskViewer', { + extend: 'Ext.window.Window', + alias: 'widget.proxmoxTaskViewer', + + initComponent: function() { + var me = this; + + if (!me.upid) { + throw "no task specified"; + } + + var task = Proxmox.Utils.parse_task_upid(me.upid); + + var statgrid; + + var rows = { + status: { + header: gettext('Status'), + defaultValue: 'unknown', + renderer: function(value) { + if (value != 'stopped') { + return value; + } + var es = statgrid.getObjectValue('exitstatus'); + if (es) { + return value + ': ' + es; + } + } + }, + exitstatus: { + visible: false + }, + type: { + header: gettext('Task type'), + required: true + }, + user: { + header: gettext('User name'), + required: true + }, + node: { + header: gettext('Node'), + required: true + }, + pid: { + header: gettext('Process ID'), + required: true + }, + starttime: { + header: gettext('Start Time'), + required: true, + renderer: Proxmox.Utils.render_timestamp + }, + upid: { + header: gettext('Unique task ID') + } + }; + + var statstore = Ext.create('Proxmox.data.ObjectStore', { + url: "/api2/json/nodes/" + task.node + "/tasks/" + me.upid + "/status", + interval: 1000, + rows: rows + }); + + me.on('destroy', statstore.stopUpdate); + + var stop_task = function() { + Proxmox.Utils.API2Request({ + url: "/nodes/" + task.node + "/tasks/" + me.upid, + waitMsgTarget: me, + method: 'DELETE', + failure: function(response, opts) { + Ext.Msg.alert(gettext('Error'), response.htmlStatus); + } + }); + }; + + var stop_btn1 = new Ext.Button({ + text: gettext('Stop'), + disabled: true, + handler: stop_task + }); + + var stop_btn2 = new Ext.Button({ + text: gettext('Stop'), + disabled: true, + handler: stop_task + }); + + statgrid = Ext.create('Proxmox.grid.ObjectGrid', { + title: gettext('Status'), + layout: 'fit', + tbar: [ stop_btn1 ], + rstore: statstore, + rows: rows, + border: false + }); + + var logView = Ext.create('Proxmox.panel.LogView', { + title: gettext('Output'), + tbar: [ stop_btn2 ], + border: false, + url: "/api2/extjs/nodes/" + task.node + "/tasks/" + me.upid + "/log" + }); + + me.mon(statstore, 'load', function() { + var status = statgrid.getObjectValue('status'); + + if (status === 'stopped') { + logView.requestUpdate(undefined, true); + logView.scrollToEnd = false; + statstore.stopUpdate(); + } + + stop_btn1.setDisabled(status !== 'running'); + stop_btn2.setDisabled(status !== 'running'); + }); + + statstore.startUpdate(); + + Ext.apply(me, { + // fixme: better title + title: "Task viewer: " + me.upid, + width: 800, + height: 400, + layout: 'fit', + modal: true, + items: [{ + xtype: 'tabpanel', + region: 'center', + items: [ logView, statgrid ] + }] + }); + + me.callParent(); + + logView.fireEvent('show', logView); + } +}); + -- 2.39.2