]> git.proxmox.com Git - proxmox-widget-toolkit.git/commitdiff
add TimeView, TimeEdit and TaskViewer
authorDietmar Maurer <dietmar@proxmox.com>
Mon, 30 Jan 2017 12:40:51 +0000 (13:40 +0100)
committerDietmar Maurer <dietmar@proxmox.com>
Mon, 30 Jan 2017 12:40:51 +0000 (13:40 +0100)
Makefile
Utils.js
data/ProxmoxProxy.js
grid/ObjectGrid.js [new file with mode: 0644]
node/TimeEdit.js [new file with mode: 0644]
node/TimeView.js [new file with mode: 0644]
window/Edit.js [new file with mode: 0644]
window/TaskViewer.js [new file with mode: 0644]

index 46e3dde0e1fdb79dd24a9d1b2db5a467ac9c4773..7793761e37c6c0d8165af449ae06a038ac14cd24 100644 (file)
--- 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:
 
index 2960ce7eb180715268d58f125cfd6ab9aedcc3d8..fa27fe063cbbab411b2488cbcbe3b16514ef98bb 100644 (file)
--- 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,
index 6fe13038926080fe88f320ce2f91423e1893a4ab..ff6fbee3234f65f7139f69af9192c9287ea63c17 100644 (file)
@@ -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 (file)
index 0000000..7f221eb
--- /dev/null
@@ -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 (file)
index 0000000..605210d
--- /dev/null
@@ -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 (file)
index 0000000..6c79e2b
--- /dev/null
@@ -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 (file)
index 0000000..967f9d7
--- /dev/null
@@ -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>
+    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 (file)
index 0000000..597404f
--- /dev/null
@@ -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);
+    }
+});
+