[PATCH 0/5] Implement and use jQuery circleGauge on guests tab

This patch set improves the guest tab implementation by: Implementing the circle gauge as a fully reusable jQuery widget Using DOM updates to update the guests tab rather than string manipulation Improving the usability of the guest livetile Adam King (5): Update VM (mock)model to produce valid JSON for /vms Update tests to account for proper JSON being returned in vm stats. Morphed the old circle implementation into a reusable jquery circleGaude widget. Update the guest.html.tmpl to use the new circleGauge widget Update guests tab to update the VM List by DOM manipulation src/kimchi/mockmodel.py | 8 +- src/kimchi/model/vms.py | 2 +- tests/test_mockmodel.py | 2 +- tests/test_model.py | 2 +- ui/css/theme-default/circle.css | 26 --- ui/css/theme-default/circleGauge.css | 26 +++ ui/css/theme-default/list.css | 25 ++- ui/js/src/kimchi.circle.js | 88 --------- ui/js/src/kimchi.guest_main.js | 366 +++++++++++++++++++---------------- ui/js/widgets/circleGauge.js | 104 ++++++++++ ui/pages/guest.html.tmpl | 39 ++-- ui/pages/tabs/guests.html.tmpl | 2 +- 12 files changed, 386 insertions(+), 304 deletions(-) delete mode 100644 ui/css/theme-default/circle.css create mode 100644 ui/css/theme-default/circleGauge.css delete mode 100644 ui/js/src/kimchi.circle.js create mode 100644 ui/js/widgets/circleGauge.js -- 1.8.1.4

Current implementations of model and mock model do not produce valid JSON for /vms stats: field. Update both to produce valid JSON. Signed-off-by: Adam King <rak@linux.vnet.ibm.com> --- src/kimchi/mockmodel.py | 8 +++++--- src/kimchi/model/vms.py | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/kimchi/mockmodel.py b/src/kimchi/mockmodel.py index 8ef4431..ece016e 100644 --- a/src/kimchi/mockmodel.py +++ b/src/kimchi/mockmodel.py @@ -915,10 +915,12 @@ class MockVM(object): ifaces = [MockVMIface(net) for net in self.networks] self.storagedevices = {} self.ifaces = dict([(iface.info['mac'], iface) for iface in ifaces]) + + stats = {'cpu_utilization': 20, 'net_throughput' : 35, + 'net_throughput_peak': 100, 'io_throughput': 45, + 'io_throughput_peak': 100} self.info = {'state': 'shutoff', - 'stats': "{'cpu_utilization': 20, 'net_throughput' : 35, \ - 'net_throughput_peak': 100, 'io_throughput': 45, \ - 'io_throughput_peak': 100}", + 'stats': stats, 'uuid': self.uuid, 'memory': template_info['memory'], 'cpus': template_info['cpus'], diff --git a/src/kimchi/model/vms.py b/src/kimchi/model/vms.py index b482e80..9da6688 100644 --- a/src/kimchi/model/vms.py +++ b/src/kimchi/model/vms.py @@ -306,7 +306,7 @@ class VMModel(object): res['io_throughput_peak'] = vm_stats.get('max_disk_io', 100) return {'state': state, - 'stats': str(res), + 'stats': res, 'uuid': dom.UUIDString(), 'memory': info[2] >> 10, 'cpus': info[3], -- 1.8.1.4

On 02/20/2014 06:10 AM, Adam King wrote:
Current implementations of model and mock model do not produce valid JSON for /vms stats: field. Update both to produce valid JSON.
Signed-off-by: Adam King <rak@linux.vnet.ibm.com> --- src/kimchi/mockmodel.py | 8 +++++--- src/kimchi/model/vms.py | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-)
diff --git a/src/kimchi/mockmodel.py b/src/kimchi/mockmodel.py index 8ef4431..ece016e 100644 --- a/src/kimchi/mockmodel.py +++ b/src/kimchi/mockmodel.py @@ -915,10 +915,12 @@ class MockVM(object): ifaces = [MockVMIface(net) for net in self.networks] self.storagedevices = {} self.ifaces = dict([(iface.info['mac'], iface) for iface in ifaces]) + + stats = {'cpu_utilization': 20, 'net_throughput' : 35, + 'net_throughput_peak': 100, 'io_throughput': 45, + 'io_throughput_peak': 100} indentation to comply with PEP8
if you use VI, here is document about PEP8 check. https://github.com/kimchi-project/kimchi/wiki/PEP8-Checking-Using-Syntastic
self.info = {'state': 'shutoff', - 'stats': "{'cpu_utilization': 20, 'net_throughput' : 35, \ - 'net_throughput_peak': 100, 'io_throughput': 45, \ - 'io_throughput_peak': 100}", + 'stats': stats, 'uuid': self.uuid, 'memory': template_info['memory'], 'cpus': template_info['cpus'], diff --git a/src/kimchi/model/vms.py b/src/kimchi/model/vms.py index b482e80..9da6688 100644 --- a/src/kimchi/model/vms.py +++ b/src/kimchi/model/vms.py @@ -306,7 +306,7 @@ class VMModel(object): res['io_throughput_peak'] = vm_stats.get('max_disk_io', 100)
return {'state': state, - 'stats': str(res), + 'stats': res, 'uuid': dom.UUIDString(), 'memory': info[2] >> 10, 'cpus': info[3],
-- Thanks and best regards! Sheldon Feng(冯少合)<shaohef@linux.vnet.ibm.com> IBM Linux Technology Center

Thanks for the review Sheldon. When I ran pep8 I found many violations in mockmodel.py. Rather than combine the pep8 change with my functional update, I made all the pep8 changes and submitted a pep8 patch for mockmodel.py On 2/19/2014 9:58 PM, Sheldon wrote:
On 02/20/2014 06:10 AM, Adam King wrote:
Current implementations of model and mock model do not produce valid JSON for /vms stats: field. Update both to produce valid JSON.
Signed-off-by: Adam King <rak@linux.vnet.ibm.com> --- src/kimchi/mockmodel.py | 8 +++++--- src/kimchi/model/vms.py | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-)
diff --git a/src/kimchi/mockmodel.py b/src/kimchi/mockmodel.py index 8ef4431..ece016e 100644 --- a/src/kimchi/mockmodel.py +++ b/src/kimchi/mockmodel.py @@ -915,10 +915,12 @@ class MockVM(object): ifaces = [MockVMIface(net) for net in self.networks] self.storagedevices = {} self.ifaces = dict([(iface.info['mac'], iface) for iface in ifaces]) + + stats = {'cpu_utilization': 20, 'net_throughput' : 35, + 'net_throughput_peak': 100, 'io_throughput': 45, + 'io_throughput_peak': 100} indentation to comply with PEP8
if you use VI, here is document about PEP8 check. https://github.com/kimchi-project/kimchi/wiki/PEP8-Checking-Using-Syntastic
self.info = {'state': 'shutoff', - 'stats': "{'cpu_utilization': 20, 'net_throughput' : 35, \ - 'net_throughput_peak': 100, 'io_throughput': 45, \ - 'io_throughput_peak': 100}", + 'stats': stats, 'uuid': self.uuid, 'memory': template_info['memory'], 'cpus': template_info['cpus'], diff --git a/src/kimchi/model/vms.py b/src/kimchi/model/vms.py index b482e80..9da6688 100644 --- a/src/kimchi/model/vms.py +++ b/src/kimchi/model/vms.py @@ -306,7 +306,7 @@ class VMModel(object): res['io_throughput_peak'] = vm_stats.get('max_disk_io', 100)
return {'state': state, - 'stats': str(res), + 'stats': res, 'uuid': dom.UUIDString(), 'memory': info[2] >> 10, 'cpus': info[3],
-- Adam King <rak@linux.vnet.ibm.com> IBM C&SI

On 02/20/2014 02:04 PM, Adam King wrote:
Thanks for the review Sheldon. When I ran pep8 I found many violations in mockmodel.py. Rather than combine the pep8 change with my functional update, I made all the pep8 changes and submitted a pep8 patch for mockmodel.py
That's great you fix all the pep8 of src/kimchi/mockmodel.py And I find pep8 fix patch based on this patch. But I'm afraid some one can not apply this patch, for there are some whitespace in this patch. $ git am JSONfor_vms.eml Applying: Update VM (mock)model to produce valid JSON for /vms /home/shhfeng/work/workdir/kimchi/.git/rebase-apply/patch:14: trailing whitespace. warning: 1 line adds whitespace errors. For this whitespace, you can set your git as follow. $ git config --global color.ui auto Then git will show your whitespace in a different color.
On 2/19/2014 9:58 PM, Sheldon wrote:
On 02/20/2014 06:10 AM, Adam King wrote:
Current implementations of model and mock model do not produce valid JSON for /vms stats: field. Update both to produce valid JSON.
Signed-off-by: Adam King <rak@linux.vnet.ibm.com> --- src/kimchi/mockmodel.py | 8 +++++--- src/kimchi/model/vms.py | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-)
diff --git a/src/kimchi/mockmodel.py b/src/kimchi/mockmodel.py index 8ef4431..ece016e 100644 --- a/src/kimchi/mockmodel.py +++ b/src/kimchi/mockmodel.py @@ -915,10 +915,12 @@ class MockVM(object): ifaces = [MockVMIface(net) for net in self.networks] self.storagedevices = {} self.ifaces = dict([(iface.info['mac'], iface) for iface in ifaces]) + + stats = {'cpu_utilization': 20, 'net_throughput' : 35, + 'net_throughput_peak': 100, 'io_throughput': 45, + 'io_throughput_peak': 100} indentation to comply with PEP8
if you use VI, here is document about PEP8 check. https://github.com/kimchi-project/kimchi/wiki/PEP8-Checking-Using-Syntastic
self.info = {'state': 'shutoff', - 'stats': "{'cpu_utilization': 20, 'net_throughput' : 35, \ - 'net_throughput_peak': 100, 'io_throughput': 45, \ - 'io_throughput_peak': 100}", + 'stats': stats, 'uuid': self.uuid, 'memory': template_info['memory'], 'cpus': template_info['cpus'], diff --git a/src/kimchi/model/vms.py b/src/kimchi/model/vms.py index b482e80..9da6688 100644 --- a/src/kimchi/model/vms.py +++ b/src/kimchi/model/vms.py @@ -306,7 +306,7 @@ class VMModel(object): res['io_throughput_peak'] = vm_stats.get('max_disk_io', 100)
return {'state': state, - 'stats': str(res), + 'stats': res, 'uuid': dom.UUIDString(), 'memory': info[2] >> 10, 'cpus': info[3],
-- Thanks and best regards! Sheldon Feng(冯少合)<shaohef@linux.vnet.ibm.com> IBM Linux Technology Center

comments below On 02/20/2014 06:10 AM, Adam King wrote:
Current implementations of model and mock model do not produce valid JSON for /vms stats: field. Update both to produce valid JSON.
Signed-off-by: Adam King <rak@linux.vnet.ibm.com> --- src/kimchi/mockmodel.py | 8 +++++--- src/kimchi/model/vms.py | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-)
diff --git a/src/kimchi/mockmodel.py b/src/kimchi/mockmodel.py index 8ef4431..ece016e 100644 --- a/src/kimchi/mockmodel.py +++ b/src/kimchi/mockmodel.py @@ -915,10 +915,12 @@ class MockVM(object): ifaces = [MockVMIface(net) for net in self.networks] self.storagedevices = {} self.ifaces = dict([(iface.info['mac'], iface) for iface in ifaces]) + The whitespace is above line. + stats = {'cpu_utilization': 20, 'net_throughput' : 35, + 'net_throughput_peak': 100, 'io_throughput': 45, + 'io_throughput_peak': 100} self.info = {'state': 'shutoff', - 'stats': "{'cpu_utilization': 20, 'net_throughput' : 35, \ - 'net_throughput_peak': 100, 'io_throughput': 45, \ - 'io_throughput_peak': 100}", + 'stats': stats, 'uuid': self.uuid, 'memory': template_info['memory'], 'cpus': template_info['cpus'], diff --git a/src/kimchi/model/vms.py b/src/kimchi/model/vms.py index b482e80..9da6688 100644 --- a/src/kimchi/model/vms.py +++ b/src/kimchi/model/vms.py @@ -306,7 +306,7 @@ class VMModel(object): res['io_throughput_peak'] = vm_stats.get('max_disk_io', 100)
return {'state': state, - 'stats': str(res), + 'stats': res, 'uuid': dom.UUIDString(), 'memory': info[2] >> 10, 'cpus': info[3],
-- Thanks and best regards! Sheldon Feng(冯少合)<shaohef@linux.vnet.ibm.com> IBM Linux Technology Center

Updated model and mockmodel tests to account for JSON responses in place of the string responses that were returned before. Signed-off-by: Adam King <rak@linux.vnet.ibm.com> --- tests/test_mockmodel.py | 2 +- tests/test_model.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_mockmodel.py b/tests/test_mockmodel.py index 4ac08dd..ab5eb59 100644 --- a/tests/test_mockmodel.py +++ b/tests/test_mockmodel.py @@ -149,7 +149,7 @@ class MockModelTests(unittest.TestCase): self.assertEquals(1024, info['memory']) self.assertEquals(1, info['cpus']) self.assertEquals('images/icon-vm.png', info['icon']) - self.assertEquals(stats_keys, set(eval(info['stats']).keys())) + self.assertEquals(stats_keys, set(info['stats'].keys())) self.assertEquals('vnc', info['graphics']['type']) self.assertEquals('0.0.0.0', info['graphics']['listen']) diff --git a/tests/test_model.py b/tests/test_model.py index 298a39e..d3315a0 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -67,7 +67,7 @@ class ModelTests(unittest.TestCase): self.assertEquals(2048, info['memory']) self.assertEquals(2, info['cpus']) self.assertEquals(None, info['icon']) - self.assertEquals(stats_keys, set(eval(info['stats']).keys())) + self.assertEquals(stats_keys, set(info['stats'].keys())) self.assertRaises(NotFoundError, inst.vm_lookup, 'nosuchvm') @unittest.skipUnless(utils.running_as_root(), 'Must be run as root') -- 1.8.1.4

Reviewed-by: ShaoHe Feng <shaohef@linux.vnet.ibm.com> On 02/20/2014 06:10 AM, Adam King wrote:
Updated model and mockmodel tests to account for JSON responses in place of the string responses that were returned before.
Signed-off-by: Adam King <rak@linux.vnet.ibm.com> --- tests/test_mockmodel.py | 2 +- tests/test_model.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/tests/test_mockmodel.py b/tests/test_mockmodel.py index 4ac08dd..ab5eb59 100644 --- a/tests/test_mockmodel.py +++ b/tests/test_mockmodel.py @@ -149,7 +149,7 @@ class MockModelTests(unittest.TestCase): self.assertEquals(1024, info['memory']) self.assertEquals(1, info['cpus']) self.assertEquals('images/icon-vm.png', info['icon']) - self.assertEquals(stats_keys, set(eval(info['stats']).keys())) + self.assertEquals(stats_keys, set(info['stats'].keys())) self.assertEquals('vnc', info['graphics']['type']) self.assertEquals('0.0.0.0', info['graphics']['listen'])
diff --git a/tests/test_model.py b/tests/test_model.py index 298a39e..d3315a0 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -67,7 +67,7 @@ class ModelTests(unittest.TestCase): self.assertEquals(2048, info['memory']) self.assertEquals(2, info['cpus']) self.assertEquals(None, info['icon']) - self.assertEquals(stats_keys, set(eval(info['stats']).keys())) + self.assertEquals(stats_keys, set(info['stats'].keys())) self.assertRaises(NotFoundError, inst.vm_lookup, 'nosuchvm')
@unittest.skipUnless(utils.running_as_root(), 'Must be run as root')
-- Thanks and best regards! Sheldon Feng(冯少合)<shaohef@linux.vnet.ibm.com> IBM Linux Technology Center

Create a general, reusable circle gauge jQuery widget. Remove the old circle as its no longer used. Signed-off-by: Adam King <rak@linux.vnet.ibm.com> --- ui/css/theme-default/circle.css | 26 --------- ui/css/theme-default/circleGauge.css | 26 +++++++++ ui/js/src/kimchi.circle.js | 88 ----------------------------- ui/js/widgets/circleGauge.js | 104 +++++++++++++++++++++++++++++++++++ 4 files changed, 130 insertions(+), 114 deletions(-) delete mode 100644 ui/css/theme-default/circle.css create mode 100644 ui/css/theme-default/circleGauge.css delete mode 100644 ui/js/src/kimchi.circle.js create mode 100644 ui/js/widgets/circleGauge.js diff --git a/ui/css/theme-default/circle.css b/ui/css/theme-default/circle.css deleted file mode 100644 index 8008219..0000000 --- a/ui/css/theme-default/circle.css +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Project Kimchi - * - * Copyright IBM, Corp. 2013 - * - * Authors: - * Lise Noble <lwnoble@us.ibm.com> - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -.circle { - position: relative; - margin: 30px 10px 10px 10px; - width: 70px; - height: 70px; -} diff --git a/ui/css/theme-default/circleGauge.css b/ui/css/theme-default/circleGauge.css new file mode 100644 index 0000000..cf9970e --- /dev/null +++ b/ui/css/theme-default/circleGauge.css @@ -0,0 +1,26 @@ +/* + * Project Kimchi + * + * Copyright IBM, Corp. 2013 + * + * Authors: + * Lise Noble <lwnoble@us.ibm.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +.circleGauge { + position: relative; + margin: 30px 10px 10px 10px; + width: 70px; + height: 70px; +} diff --git a/ui/js/src/kimchi.circle.js b/ui/js/src/kimchi.circle.js deleted file mode 100644 index 7b9ccc7..0000000 --- a/ui/js/src/kimchi.circle.js +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Project Kimchi - * - * Copyright IBM, Corp. 2013 - * - * Authors: - * Hongliang Wang <hlwanghl@cn.ibm.com> - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -(function($) { - $.fn.circle = function(options) { - var settings = $.extend({ - color : '#87C004', - fillColor : '#87C004', - fontFamily : 'Geneva, sans-serif', - fontSize : 13, - radius : 35, - lineWidth : 20 - }, options); - - $(this).each(function() { - var that = $(this); - var json = eval("(" + that.data('value') + ")"); - var parentNode = that.parent() - var type = ''; - var display = 0; - var circle = 0; - - if (parentNode.hasClass('guest-cpu')) { - display = parseInt(json['cpu_utilization']); - circle = display; - type = '%'; - } else if (parentNode.hasClass('guest-network')) { - display = parseInt(json['net_throughput']); - circle = (display * 100) / parseInt(json['net_throughput_peak']); - } else if (parentNode.hasClass('guest-storage')) { - display = parseInt(json['io_throughput']); - circle = (display * 100) / parseInt(json['io_throughput_peak']); - } - - that.empty(); - var canvas = document.createElement('canvas'); - that.append($(canvas)); - var ctx = canvas.getContext('2d'); - var lineWidth = settings['lineWidth']; - var radius = settings['radius']; - var fontSize = settings['fontSize']; - var shadowSize = 2; - var width = height = radius * 2; - $(canvas).attr('height', height); - $(canvas).attr('width', width); - $(canvas).css({ - 'boxShadow' : shadowSize + 'px ' + shadowSize + 'px ' + shadowSize + 'px #fff, -' + shadowSize + 'px -' + shadowSize + 'px ' + shadowSize + 'px #eaeaea', - borderRadius : radius + 'px' - }); - ctx.clearRect(0, 0, width, height); - - ctx.fillStyle = settings['fillColor']; - ctx.font = 'bold ' + fontSize + 'px ' + settings['fontFamily']; - ctx.textAlign = 'center'; - var originPos = radius; - ctx.textBaseline = 'middle'; - ctx.fillText(display + type, originPos, originPos); - ctx.strokeStyle = settings['color']; - ctx.lineWidth = lineWidth; - ctx.beginPath(); - ctx.arc(originPos, originPos, radius, -.5 * Math.PI, (circle / 50 - .5) * Math.PI); - ctx.stroke(); - }); - - return this; - }; -}(jQuery)); - -kimchi.circle = function(selector) { - $(selector).circle(); -}; diff --git a/ui/js/widgets/circleGauge.js b/ui/js/widgets/circleGauge.js new file mode 100644 index 0000000..d0887fd --- /dev/null +++ b/ui/js/widgets/circleGauge.js @@ -0,0 +1,104 @@ +/* + * Project Kimchi + * + * Copyright IBM, Corp. 2013 + * + * Authors: + * Hongliang Wang <hlwanghl@cn.ibm.com> + * Adam King <rak@linux.vnet.ibm.com> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + (function($) { + $.widget('kimchi.circleGauge', { + + options : { + color : '#87C004', + fillColor : '#87C004', + lineWidth : 20, + shadowSize : '2px', + font : 'bold 13px Geneva, sans-serif', + textAlign : 'center', + radius : 35, + peakRate : 100, + display : 0, + circle : 0, + label : '' + }, + + _create : function() { + //valuesAttr="{" + this.element.data('value')+ "}"; + //console.info(valuesAttr); + //values=eval("(" + valuesAttr + ")"); + //$.extend(this.options, values); + this.options.display=this.element.data('display'); + this.options.percentage=this.element.data('percentage'); + this._fixupPeakRate(); + this._draw(); + }, + + setValues : function(values) { + $.extend(this.options, values); + this._fixupPeakRate(); + this._draw(); + }, + + _fixupPeakRate : function() { + if (this.options.circle>this.options.peakRate) { + this.options.peakRate=this.options.circle; + } + }, + + _draw : function() { + this.element.empty(); + var canvas = document.createElement('canvas'); + //this.element.append($(canvas)); //I don't quite understand this line so trying the one below... + this.element.append(canvas); + + var ctx = canvas.getContext('2d'); + var radius = this.options.radius; + + var shadowSize = 2; + var width = height = radius * 2; + $(canvas).attr('height', height); + $(canvas).attr('width', width); + + $(canvas).css({ + 'boxShadow' : shadowSize + 'px ' + shadowSize + 'px ' + shadowSize + 'px #fff, -' + shadowSize + 'px -' + shadowSize + 'px ' + shadowSize + 'px #eaeaea', + borderRadius : radius + 'px' + }); + + ctx.clearRect(0, 0, width, height); + ctx.fillStyle = this.options.fillColor; + ctx.font = this.options.font; + ctx.textAlign = 'center'; + var originPos = radius; + ctx.textBaseline = 'middle'; + ctx.fillText(this.options.display, originPos, originPos); + ctx.strokeStyle = this.options.color; + ctx.lineWidth = this.options.lineWidth; + ctx.beginPath(); + ctx.arc(originPos, originPos, radius, -.5 * Math.PI, (this.options.percentage / 50 - .5) * Math.PI); + ctx.stroke(); + }, + + destroy : function() { + this.element.empty(); + $.Widget.prototype.destroy.call(this); + } + }); +}(jQuery)); + +kimchi.circleGauge = function(selector) { + $(selector).circleGauge(); +}; -- 1.8.1.4

Updated the template to use the new circle gauge widget. Also retooled the template so that insertions can be handled by DOM manipulation rather than string manipulation. Signed-off-by: Adam King <rak@linux.vnet.ibm.com> --- ui/css/theme-default/list.css | 25 +++++++++++++++++++++++-- ui/pages/guest.html.tmpl | 39 ++++++++++++++++++++------------------- 2 files changed, 43 insertions(+), 21 deletions(-) diff --git a/ui/css/theme-default/list.css b/ui/css/theme-default/list.css index c435261..3b30a06 100644 --- a/ui/css/theme-default/list.css +++ b/ui/css/theme-default/list.css @@ -92,17 +92,36 @@ .list-vm .tile .imgload { display: none; - max-height: 110px; +} + +.list-vm .tile.shutdown .imgactive { + max-height: 110px; + max-width: 170px; + height: auto; + width: auto; + display:inline; +} + +.list-vm .tile.running .imgactive{ + max-height: 110px; max-width: 170px; height: auto; width: auto; + display:inline; + cursor: zoom-in; + cursor: -moz-zoom-in; + cursor: -webkit-zoom-in; } -.list-vm .tile .imgactive { +.list-vm .tile .overlay { max-height: 110px; max-width: 170px; height: auto; width: auto; + position:absolute; + bottom:0; + right:0; + display:none; } .guest-type { @@ -221,10 +240,12 @@ margin: 10px; } + .list-vm .tile:not(.shutoff) img { box-shadow: -1px -1px 2px rgb(0, 0, 0, .25), 3px 3px 3px #fff; } + .list-vm .shutoff { box-shadow: none !important; } diff --git a/ui/pages/guest.html.tmpl b/ui/pages/guest.html.tmpl index 6d83d57..341fbb2 100644 --- a/ui/pages/guest.html.tmpl +++ b/ui/pages/guest.html.tmpl @@ -5,6 +5,7 @@ * * Authors: * Hongliang Wang <hlwanghl@cn.ibm.com> + * Adam King <rak@us.ibm.com> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,43 +25,43 @@ #silent t = gettext.translation($lang.domain, $lang.localedir, languages=$lang.lang) #silent _ = t.gettext #silent _t = t.gettext - <li id="{name}"> + <li name="guest" class="guest"> <div class="sortable guest-type"> <div class="guest-general"> <h2 class="title" title="{name}">{name}</h2> </div> </div> - <div class="sortable guest-cpu"> - <div class="circle" data-value="{stats}"></div> + <div name="cpu_utilization" class="sortable"> + <div class="circleGauge"></div> </div> - <div class="sortable guest-network"> - <div class="circle" data-value="{stats}"></div> + <div name="io_throughput" class="sortable"> + <div class="circleGauge"></div> <div class="subtitle">KB/s</div> </div> - <div class="sortable guest-storage"> - <div class="circle" data-value="{stats}"></div> + <div name="net_throughput" class="sortable"> + <div class="circleGauge"></div> <div class="subtitle">KB/s</div> </div> - <div class="sortable guest-tile"> + <div name="guest-tile" class="sortable guest-tile"> <div class="tile {state}"> - <img class="imgactive" alt="" src="{tile-src}"> - <img class="imgload" alt="" src="{load-src}"> + <img class="imgactive" alt="" src=""> + <img class="imgload" alt="" src=""> + <img class="overlay shutoff-hidden" alt="$_("Start")" src="/images/theme-default/icon-power-down.png" > </div> </div> - <div class="sortable guest-actions"> + <div class="sortable guest-actions" name="guest-actions"> <div class="top"> - <a class="btn vm-reset" data-vmstate="{state}" data-vm="{name}" href="javascript:void(0);" title="$_("Reset")"><span class="icon reset"></span></a> - <a class="btn vm-start" data-vmstate="{state}" data-vm="{name}" href="javascript:void(0);" title="$_("Start")"><span class="icon power-down"></span></a> - <a class="btn vm-stop" data-vmstate="{state}" data-vm="{name}" href="javascript:void(0);" title="$_("Stop")"><span class="icon power-up"></span></a> + <a class="btn shutoff-disabled" name="vm-reset" href="javascript:void(0);" title="$_("Reset")"><span class="icon reset"></span></a> + <a class="btn running-hidden" name="vm-start" href="javascript:void(0);" title="$_("Start")"><span class="icon power-down"></span></a> + <a class="btn shutoff-hidden" name="vm-stop" href="javascript:void(0);" title="$_("Stop")"><span class="icon power-up"></span></a> </div> <div class="bottom"> - <div class="btn dropdown popable vm-action" data-vmstate="{state}" data-graphics="{graphics.type}" data-vm="{name}" style="width: 70px"> + <div name="actionmenu" class="btn dropdown popable vm-action" style="width: 70px"> <span class="text">$_("Actions")</span><span class="arrow"></span> <div class="popover actionsheet right-side" style="width: 250px"> - <button class="button-big vm-vnc" data-vm="{name}"><span class="text">VNC</span></button> - <button class="button-big vm-spice" data-vm="{name}"><span class="text">SPICE</span></button> - <button class="button-big vm-edit" data-vm="{name}"><span class="text">$_("Edit")</span></button> - <a class="button-big red vm-delete" data-vm="{name}">$_("Delete")</a> + <button class="button-big shutoff-disabled" name="vm-console" ><span class="text">$_("Console")</span></button> + <button class="button-big running-disabled" name="vm-edit"><span class="text">$_("Edit")</span></button> + <a class="button-big red " name="vm-delete">$_("Delete")</a> </div> </div> </div> -- 1.8.1.4

Update guests.html.template to manipulate the VM List my DOM manipulation rather than by string manipulation. Also update the tile to function as a start when the VM is stopped, or as a console connection when it is running. Signed-off-by: Adam King <rak@linux.vnet.ibm.com> --- ui/js/src/kimchi.guest_main.js | 366 +++++++++++++++++++++++------------------ ui/pages/tabs/guests.html.tmpl | 2 +- 2 files changed, 205 insertions(+), 163 deletions(-) diff --git a/ui/js/src/kimchi.guest_main.js b/ui/js/src/kimchi.guest_main.js index 99cb84a..58f648c 100644 --- a/ui/js/src/kimchi.guest_main.js +++ b/ui/js/src/kimchi.guest_main.js @@ -5,6 +5,7 @@ * * Authors: * Hongliang Wang <hlwanghl@cn.ibm.com> + * Adam King <rak@us.ibm.com> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,164 +19,111 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -kimchi.initVmButtonsAction = function() { - - var vmstart = function(event) { - if (!$(this).hasClass('loading')) { - $(this).addClass('loading'); - kimchi.startVM($(this).data('vm'), function(result) { - kimchi.listVmsAuto(); - }, function(err) { - kimchi.message.error(err.responseJSON.reason); - }); - } else { - event.preventDefault(); - event.stopPropagation(); - return; - } - }; - - var vmstop = function(event) { - if (!$(this).hasClass('loading')) { - $(this).addClass('loading'); - kimchi.stopVM($(this).data('vm'), function(result) { - kimchi.listVmsAuto(); - }, function(err) { - kimchi.message.error(err.responseJSON.reason); - }); - } else { - event.preventDefault(); - event.stopPropagation(); - } - }; - - $('.circle').circle(); - - $(".vm-start").each(function(index) { - if ('running' === $(this).data('vmstate')) { - $(this).hide(); - } else { - $(this).show(); - } - }); - - $(".vm-stop").each(function(index) { - if ('running' === $(this).data('vmstate')) { - $(this).show(); - } else { - $(this).hide(); - } - }); - - $(".vm-start").on({ - click : vmstart, - }); - - $(".vm-stop").on({ - click : vmstop, - }); - - $(".vm-reset").on("click", function(event) { - if ('running' === $(this).data('vmstate')) { - kimchi.resetVM($(this).data('vm'), function(result) { - kimchi.listVmsAuto(); - }, function(err) { - kimchi.message.error(err.responseJSON.reason); - }); - } else { - kimchi.startVM($(this).data('vm'), function(result) { - kimchi.listVmsAuto(); - }, function(err) { - kimchi.message.error(err.responseJSON.reason); - }); - } - }); - - $(".vm-delete").on("click", function(event) { - var vm = $(this); - var settings = { - title : i18n['KCHAPI6001M'], - content : i18n['KCHVM6001M'], - confirm : i18n['KCHAPI6002M'], - cancel : i18n['KCHAPI6003M'] - }; - kimchi.confirm(settings, function() { - kimchi.deleteVM(vm.data('vm'), function(result) { - kimchi.listVmsAuto(); - }, function(err) { - kimchi.message.error(err.responseJSON.reason); - }); - }, function() { - }); - }); - - $(".vm-edit").on("click", function(event) { - var vmName = $(this).data('vm'); - kimchi.selectedGuest = vmName; - kimchi.window.open("guest-edit.html"); - }); - $(".vm-vnc").on("click", function(event) { - kimchi.vncToVM($(this).data('vm')); - }); +kimchi.vmstart = function(event) { + var button=$(this); + if (!button.hasClass('loading')) { + button.addClass('loading'); + var vm=$(this).closest('li[name=guest]'); + var vm_id=vm.attr("id"); + kimchi.startVM(vm_id, function(result) { + button.removeClass('loading'); + kimchi.listVmsAuto(); + }, function() { + startButton.removeClass('loading'); + kimchi.message.error(i18n['msg.fail.start']); + } + ); + } else { + event.preventDefault(); + event.stopPropagation(); + return; + } +}; - $(".vm-spice").on("click", function(event) { - kimchi.spiceToVM($(this).data('vm')); - }); +kimchi.vmstop = function(event) { + var button=$(this); + if (!button.hasClass('loading')) { + button.addClass('loading'); + var vm=button.closest('li[name=guest]'); + var vm_id=vm.attr("id"); + kimchi.stopVM(vm_id, function(result) { + button.removeClass('loading'); + kimchi.listVmsAuto(); + }, function() { + kimchi.message.error(i18n['msg.fail.stop']); + }); + } else { + event.preventDefault(); + event.stopPropagation(); + } +}; - kimchi.init_button_stat(); +kimchi.vmreset = function(event){ + var vm=$(this).closest('li[name=guest]'); + var vm_id=vm.attr("id"); + kimchi.resetVM(vm_id, function(result) { + kimchi.listVmsAuto(); + }, function() { + kimchi.message.error(i18n['msg.fail.reset']); + } + ); +}; +kimchi.vmdelete = function(event) { + var vm = $(this).closest('li[name=guest]'); + var vm_id=vm.attr("id"); + var settings = { + title : i18n['msg.confirm.delete.title'], + content : i18n['msg.vm.confirm.delete'], + confirm : i18n['msg.confirm.delete.confirm'], + cancel : i18n['msg.confirm.delete.cancel'] + }; + kimchi.confirm(settings, function() { + kimchi.deleteVM(vm_id, function(result) { + kimchi.listVmsAuto(); + }, function() { + kimchi.message.error(i18n['msg.fail.delete']); + }); + }, function() { + }); }; -kimchi.init_button_stat = function() { - $('.vm-action').each(function() { - var vm_action = $(this); - var vm_vnc = vm_action.find('.vm-vnc'); - var vm_spice = vm_action.find('.vm-spice'); - var vm_graphics; - if (vm_action.data('graphics') === 'vnc') { - vm_spice.hide(); - vm_graphics = vm_vnc; - } else if (vm_action.data('graphics') === 'spice') { - vm_vnc.hide(); - vm_graphics = vm_spice; - } else { - vm_vnc.hide(); - vm_spice.hide(); - vm_graphics = null; - } +kimchi.vmedit = function(event) { + var vm = $(this).closest('li[name=guest]'); + var vm_id=vm.attr("id"); + kimchi.selectedGuest = vm_id; + kimchi.window.open("guest-edit.html"); +}; - if (vm_graphics !== null) { - if (vm_action.data('vmstate') === 'running') { - vm_graphics.removeAttr('disabled'); - } else { - vm_graphics.attr('disabled', 'disabled'); - } - } +kimchi.openVmConsole = function(event) { + var vm=$(this).closest('li[name=guest]'); + var vmObject=vm.data(); + if (vmObject.graphics['type'] == 'vnc') { + kimchi.vncToVM(vm.attr('id')); + } + else if (vmObject.graphics['type'] == 'spice') { + kimchi.spiceToVM(vm.attr('id')); + } - var editButton = vm_action.find('.vm-edit'); - editButton.prop('disabled', vm_action.data('vmstate') !== 'shutoff'); - }) }; -kimchi.getVmsOldImg = function() { +kimchi.getVmsCurrentConsoleImgs = function() { var res = new Object(); $('#guestList').children().each(function() { - res[$(this).attr('id')] = $(this).find('img').attr('src'); + res[$(this).attr('id')] = $(this).find('img.imgactive').attr('src'); }) return res; }; -kimchi.getVmsOldPopStats = function() { - var oldSettings = new Object(); - $('#guestList').children().each(function() { - if ($(this).find('.popable').hasClass('open')) { - oldSettings[$(this).attr('id')] = true; - } else { - oldSettings[$(this).attr('id')] = false; - } - }) - return oldSettings; +kimchi.getOpenMenuVmId = function() { + var result; + var openMenu = $('#guestList .open:first') + if(openMenu) { + var li_element=openMenu.closest('li'); + result=li_element.attr('id'); + } + return result; }; kimchi.listVmsAuto = function() { @@ -185,28 +133,18 @@ kimchi.listVmsAuto = function() { kimchi.listVMs(function(result, textStatus, jqXHR) { if (result && textStatus=="success") { if(result.length) { - $('#guestListField').show(); - $('#noGuests').hide(); var listHtml = ''; var guestTemplate = kimchi.guestTemplate; - var oldImages = kimchi.getVmsOldImg(); - var oldSettings = kimchi.getVmsOldPopStats(); - $.each(result, function(index, value) { - var oldImg = oldImages[value.name]; - curImg = value.state == 'running' ? value.screenshot : value.icon; - value['load-src'] = curImg || 'images/icon-vm.png'; - value['tile-src'] = oldImg || value['load-src']; - var statusTemplate = kimchi.editTemplate(guestTemplate, oldSettings[value.name]); - listHtml += kimchi.template(statusTemplate, value); - }); - $('#guestList').html(listHtml); - $('#guestList').find('.imgload').each(function() { - this.onload = function() { - $(this).prev('.imgactive').remove(); - $(this).show(); - } + var currentConsoleImages = kimchi.getVmsCurrentConsoleImgs(); + var openMenuGuest = kimchi.getOpenMenuVmId(); + $('#guestList').html(''); + $('#guestListField').show(); + $('#noGuests').hide(); + + $.each(result, function(index, vm) { + var guestLI = kimchi.createGuestLi(vm, currentConsoleImages[vm.name], vm.name==openMenuGuest); + $('#guestList').append(guestLI); }); - kimchi.initVmButtonsAction(); } else { $('#guestListField').hide(); $('#noGuests').show(); @@ -220,6 +158,109 @@ kimchi.listVmsAuto = function() { }); }; +kimchi.createGuestLi = function(vmObject, prevScreenImage, openMenu) { + var result=kimchi.guestElem.clone(); + + //Setup the VM list entry + var vmRunningBool=(vmObject.state=="running"); + result.attr('id',vmObject.name); + result.data(vmObject); + + //Add the Name + var guestTitle=result.find('.title').attr('title',vmObject.name); + guestTitle.html(vmObject.name); + + //Setup the VM console thumbnail display + var curImg = vmObject.state == 'running' ? vmObject.screenshot : vmObject.icon; + var load_src = curImg || 'images/icon-vm.png'; + var tile_src = prevScreenImage || vmObject['load-src']; + var liveTile=result.find('div[name=guest-tile] > .tile'); + liveTile.addClass(vmObject.state); + liveTile.find('.imgactive').attr('src',tile_src); + var imgLoad=liveTile.find('.imgload'); + imgLoad.on('load', function() { + var oldImg=$(this).parent().find('.imgactive'); + oldImg.removeClass("imgactive").addClass("imgload"); + oldImg.attr("src","#"); + $(this).addClass("imgactive").removeClass("imgload"); + $(this).off('load'); + }); + imgLoad.attr('src',load_src); + + //Link the stopped tile to the start action, the running tile to open the console + if (vmRunningBool) { + liveTile.off("click", kimchi.vmstart); + liveTile.on("click", kimchi.openVmConsole); + } + else { + liveTile.off("click", kimchi.openVmConsole); + liveTile.on("click", kimchi.vmstart); + liveTile.hover(function(event){$(this).find('.overlay').show()}, function(event){$(this).find('.overlay').hide()}); + } + + + //Setup the gauges + var stats=vmObject.stats; + var gaugeValue=0; + gaugeValue=parseInt(stats.net_throughput); + kimchi.circleGaugeInit(result, "net_throughput",gaugeValue,(gaugeValue*100/stats.net_throughput_peak)); + gaugeValue=parseInt(stats.io_throughput); + kimchi.circleGaugeInit(result, "io_throughput",gaugeValue,(gaugeValue*100/stats.io_throughput_peak)); + gaugeValue=parseInt(stats.cpu_utilization); + kimchi.circleGaugeInit(result, "cpu_utilization",gaugeValue+"%",gaugeValue); + + //Setup the VM Actions + var guestActions=result.find("div[name=guest-actions]"); + guestActions.find(".shutoff-disabled").prop('disabled', !vmRunningBool ); + guestActions.find(".running-disabled").prop('disabled', vmRunningBool ); + + if (vmRunningBool) { + guestActions.find(".running-hidden").hide(); + } + else { + guestActions.find(".shutoff-hidden").hide(); + } + + var consoleActions=guestActions.find("[name=vm-console]"); + + if ((vmObject.graphics['type'] == 'vnc') || (vmObject.graphics['type'] == 'spice')) { + consoleActions.on("click", kimchi.openVmConsole); + consoleActions.show(); + } else { //we don't recognize the VMs supported graphics, so hide the menu choice + consoleActions.hide(); + consoleActions.off("click",kimchi.openVmConsole); + } + + //Setup action event handlers + guestActions.find("[name=vm-start]").on({click : kimchi.vmstart}); + guestActions.find("[name=vm-stop]").on({click : kimchi.vmstop}); + if (vmRunningBool) { //If the guest is not running, do not enable reset + guestActions.find("[name=vm-reset]").on({click : kimchi.vmreset}); + } + guestActions.find("[name=vm-edit]").on({click : kimchi.vmedit}); + guestActions.find("[name=vm-delete]").on({click : kimchi.vmdelete}); + + //Maintain menu open state + var actionMenu=guestActions.find("div[name=actionmenu]"); + if (openMenu) { + actionMenu.addClass("open"); + } + + return result; +}; + +kimchi.circleGaugeInit = function(topElement, divName, display, percentage){ + var gauge=topElement.find('div[name="' + divName + '"] .circleGauge'); + if(gauge) { + var data=Object(); + data.percentage = percentage; + data.display = display; + gauge.data(data); + } + gauge.circleGauge(); + return(gauge); +}; + kimchi.guestSetRequestHeader = function(xhr) { xhr.setRequestHeader('Accept', 'text/html'); }; @@ -229,6 +270,7 @@ kimchi.guest_main = function() { kimchi.window.open('guest-add.html'); }); kimchi.guestTemplate = $('#guest-tmpl').html(); + kimchi.guestElem=$('<div/>').html(kimchi.guestTemplate).find('li'); $('#guests-root-container').on('remove', function() { kimchi.vmTimeout && clearTimeout(kimchi.vmTimeout); }); @@ -236,7 +278,7 @@ kimchi.guest_main = function() { }; kimchi.editTemplate = function(guestTemplate, oldPopStat) { - if (oldPopStat != null && oldPopStat) { + if (oldPopStat) { return guestTemplate.replace("vm-action", "vm-action open"); } return guestTemplate; diff --git a/ui/pages/tabs/guests.html.tmpl b/ui/pages/tabs/guests.html.tmpl index 8b530c7..e86718b 100644 --- a/ui/pages/tabs/guests.html.tmpl +++ b/ui/pages/tabs/guests.html.tmpl @@ -40,8 +40,8 @@ <ul class="list-title"> <li class="guest-type">$_("Name")</li> <li class="guest-cpu">$_("CPU")</li> - <li class="guest-network">$_("Network I/O")</li> <li class="guest-storage">$_("Disk I/O")</li> + <li class="guest-network">$_("Network I/O")</li> <li class="guest-tile">$_("Livetile")</li> <li class="guest-actions">$_("Actions")</li> </ul> -- 1.8.1.4
participants (2)
-
Adam King
-
Sheldon