diff options
Diffstat (limited to 'qpid/tools/src/java/qpid-qmf2-tools/bin/qpid-web/web/qmf-ui')
16 files changed, 4996 insertions, 0 deletions
diff --git a/qpid/tools/src/java/qpid-qmf2-tools/bin/qpid-web/web/qmf-ui/css/index.css b/qpid/tools/src/java/qpid-qmf2-tools/bin/qpid-web/web/qmf-ui/css/index.css new file mode 100644 index 0000000000..0032bedca1 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2-tools/bin/qpid-web/web/qmf-ui/css/index.css @@ -0,0 +1,64 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + * + */ + +html +{ + background: black; +} + +body +{ + position: absolute; + + top: 0; + left: 0; + width: 100%; + height: 100%; + margin: 0; + + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + border-radius: 5px; + + background: #bbb url(/qmf-ui/images/gradient.png) repeat-x; + background: -o-linear-gradient(top center, #fff 0%, #bbb 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0, #fff), color-stop(1, #bbb)); + background: -webkit-linear-gradient(top center, #fff 0%, #bbb 100%); + background: -ms-linear-gradient(top center, #fff 0%, #bbb 100%); + background: -moz-linear-gradient(top center, #fff 0%, #bbb 100%); +} + +div.logo +{ + position: absolute; + top: 5%; + left: 0; + right: 0; + height: 114px; + margin: 0 35% 0 35%; + + -ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=70)"; + filter: alpha(opacity=70); + opacity: 0.7; + + background: transparent url(/qmf-ui/images/qpid-logo.png) center no-repeat; +} + diff --git a/qpid/tools/src/java/qpid-qmf2-tools/bin/qpid-web/web/qmf-ui/css/qmf.css b/qpid/tools/src/java/qpid-qmf2-tools/bin/qpid-web/web/qmf-ui/css/qmf.css new file mode 100644 index 0000000000..11ca7fad12 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2-tools/bin/qpid-web/web/qmf-ui/css/qmf.css @@ -0,0 +1,144 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + * + */ + +a.settings +{ + background: url(/qmf-ui/images/settings.png) no-repeat left; +} + +a.brokers +{ + background: url(/qmf-ui/images/brokers.png) no-repeat left; +} + +a.connections +{ + background: url(/qmf-ui/images/connections.png) no-repeat left; +} + +a.exchanges +{ + background: url(/qmf-ui/images/exchanges.png) no-repeat left; +} + +a.queues +{ + background: url(/qmf-ui/images/queues.png) no-repeat left; +} + +a.links +{ + background: url(/qmf-ui/images/links.png) no-repeat left; +} + +a.route-topology +{ + background: url(/qmf-ui/images/route-topology.png) no-repeat left; +} + +a.events +{ + background: url(/qmf-ui/images/events.png) no-repeat left; +} + +/* This style allows a simple alert banner to be displayed */ +div.alert +{ + position: fixed; + opacity: 0.6; + + font-size: 30pt; + color: #d0d0d0; + background: red; + + top: 45.5%; + left: 10%; + width: 80%; + text-align: center; + + -o-transform: rotate(-45deg); + -ms-transform: rotate(-45deg); + -webkit-transform: rotate(-45deg); + -moz-transform: rotate(-45deg); + + box-shadow: 5px 5px 5px #888; + z-index: 5; + display: none; + + /* At it's only a few styles different just use <= IE8 css hack rather than a whole stylesheet */ + position: relative\9; + background: url(/itablet/images/ie/red6.png) repeat\9; + top: 200px\9; +} + + +#splash +{ + position: absolute; + + top: 0; + left: 0; + width: 100%; + height: 100%; + margin: 0; + + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + border-radius: 5px; + + background: #bbb url(/qmf-ui/images/gradient.png) repeat-x; + background: -o-linear-gradient(top center, #fff 0%, #bbb 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0, #fff), color-stop(1, #bbb)); + background: -webkit-linear-gradient(top center, #fff 0%, #bbb 100%); + background: -ms-linear-gradient(top center, #fff 0%, #bbb 100%); + background: -moz-linear-gradient(top center, #fff 0%, #bbb 100%); + + z-index: 100; +} + +#splash div.logo +{ + position: absolute; + top: 5%; + left: 0; + right: 0; + height: 114px; + margin: 0 35% 0 35%; + + -ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=70)"; + filter: alpha(opacity=70); + opacity: 0.7; + + background: transparent url(/qmf-ui/images/qpid-logo.png) center no-repeat; +} + +#splash div.loading +{ + position: absolute; + top: 70px; + left: 0; + right: 0; + height: 32px; + + background: transparent url(/qmf-ui/images/loading.gif) center no-repeat; +} + + diff --git a/qpid/tools/src/java/qpid-qmf2-tools/bin/qpid-web/web/qmf-ui/images/brokers.png b/qpid/tools/src/java/qpid-qmf2-tools/bin/qpid-web/web/qmf-ui/images/brokers.png Binary files differnew file mode 100644 index 0000000000..72efa7ab11 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2-tools/bin/qpid-web/web/qmf-ui/images/brokers.png diff --git a/qpid/tools/src/java/qpid-qmf2-tools/bin/qpid-web/web/qmf-ui/images/connections.png b/qpid/tools/src/java/qpid-qmf2-tools/bin/qpid-web/web/qmf-ui/images/connections.png Binary files differnew file mode 100644 index 0000000000..2e1d422d0a --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2-tools/bin/qpid-web/web/qmf-ui/images/connections.png diff --git a/qpid/tools/src/java/qpid-qmf2-tools/bin/qpid-web/web/qmf-ui/images/events.png b/qpid/tools/src/java/qpid-qmf2-tools/bin/qpid-web/web/qmf-ui/images/events.png Binary files differnew file mode 100644 index 0000000000..1e5a7ee5ca --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2-tools/bin/qpid-web/web/qmf-ui/images/events.png diff --git a/qpid/tools/src/java/qpid-qmf2-tools/bin/qpid-web/web/qmf-ui/images/exchanges.png b/qpid/tools/src/java/qpid-qmf2-tools/bin/qpid-web/web/qmf-ui/images/exchanges.png Binary files differnew file mode 100644 index 0000000000..5f15dbbfdc --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2-tools/bin/qpid-web/web/qmf-ui/images/exchanges.png diff --git a/qpid/tools/src/java/qpid-qmf2-tools/bin/qpid-web/web/qmf-ui/images/gradient.png b/qpid/tools/src/java/qpid-qmf2-tools/bin/qpid-web/web/qmf-ui/images/gradient.png Binary files differnew file mode 100644 index 0000000000..260a05701f --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2-tools/bin/qpid-web/web/qmf-ui/images/gradient.png diff --git a/qpid/tools/src/java/qpid-qmf2-tools/bin/qpid-web/web/qmf-ui/images/links.png b/qpid/tools/src/java/qpid-qmf2-tools/bin/qpid-web/web/qmf-ui/images/links.png Binary files differnew file mode 100644 index 0000000000..d8ad7af518 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2-tools/bin/qpid-web/web/qmf-ui/images/links.png diff --git a/qpid/tools/src/java/qpid-qmf2-tools/bin/qpid-web/web/qmf-ui/images/loading.gif b/qpid/tools/src/java/qpid-qmf2-tools/bin/qpid-web/web/qmf-ui/images/loading.gif Binary files differnew file mode 100644 index 0000000000..3288d1035d --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2-tools/bin/qpid-web/web/qmf-ui/images/loading.gif diff --git a/qpid/tools/src/java/qpid-qmf2-tools/bin/qpid-web/web/qmf-ui/images/qpid-logo.png b/qpid/tools/src/java/qpid-qmf2-tools/bin/qpid-web/web/qmf-ui/images/qpid-logo.png Binary files differnew file mode 100644 index 0000000000..d548b68245 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2-tools/bin/qpid-web/web/qmf-ui/images/qpid-logo.png diff --git a/qpid/tools/src/java/qpid-qmf2-tools/bin/qpid-web/web/qmf-ui/images/queues.png b/qpid/tools/src/java/qpid-qmf2-tools/bin/qpid-web/web/qmf-ui/images/queues.png Binary files differnew file mode 100644 index 0000000000..4f72abfe50 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2-tools/bin/qpid-web/web/qmf-ui/images/queues.png diff --git a/qpid/tools/src/java/qpid-qmf2-tools/bin/qpid-web/web/qmf-ui/images/route-topology.png b/qpid/tools/src/java/qpid-qmf2-tools/bin/qpid-web/web/qmf-ui/images/route-topology.png Binary files differnew file mode 100644 index 0000000000..c1b36d4077 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2-tools/bin/qpid-web/web/qmf-ui/images/route-topology.png diff --git a/qpid/tools/src/java/qpid-qmf2-tools/bin/qpid-web/web/qmf-ui/images/settings.png b/qpid/tools/src/java/qpid-qmf2-tools/bin/qpid-web/web/qmf-ui/images/settings.png Binary files differnew file mode 100644 index 0000000000..6994d4a0d4 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2-tools/bin/qpid-web/web/qmf-ui/images/settings.png diff --git a/qpid/tools/src/java/qpid-qmf2-tools/bin/qpid-web/web/qmf-ui/scripts/LICENCE b/qpid/tools/src/java/qpid-qmf2-tools/bin/qpid-web/web/qmf-ui/scripts/LICENCE new file mode 100644 index 0000000000..e90b016701 --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2-tools/bin/qpid-web/web/qmf-ui/scripts/LICENCE @@ -0,0 +1,39 @@ + +/* + * excanvas.js + * + * ExplorerCanvas - provides HTML5 canvas on IE + * + * note that this is using excanvas.js from http://explorercanvas.googlecode.com/svn/trunk/ as it happens that's + * more up to date than the version https://code.google.com/p/explorercanvas/downloads/list/excanvas_r3.zip which + * is the version one gets following the obvious path. Unfortunately excanvas_r3.zip has several bugs in drawImage() + * which affect the border radius rendering for IE7 canvas, the version from trunk fixes that.... + * + * Google Open Source: + * <http://code.google.com> + * <opensource@google.com> + * + * Developers: + * Emil A Eklund <emil@eae.net> + * Erik Arvidsson <erik@eae.net> + * Glen Murphy <glen@glenmurphy.com> + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + * + */ + diff --git a/qpid/tools/src/java/qpid-qmf2-tools/bin/qpid-web/web/qmf-ui/scripts/excanvas.js b/qpid/tools/src/java/qpid-qmf2-tools/bin/qpid-web/web/qmf-ui/scripts/excanvas.js new file mode 100644 index 0000000000..a6d9ddf5fa --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2-tools/bin/qpid-web/web/qmf-ui/scripts/excanvas.js @@ -0,0 +1,1416 @@ +// Copyright 2006 Google Inc. +// +// 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. + + +// Known Issues: +// +// * Patterns only support repeat. +// * Radial gradient are not implemented. The VML version of these look very +// different from the canvas one. +// * Clipping paths are not implemented. +// * Coordsize. The width and height attribute have higher priority than the +// width and height style values which isn't correct. +// * Painting mode isn't implemented. +// * Canvas width/height should is using content-box by default. IE in +// Quirks mode will draw the canvas using border-box. Either change your +// doctype to HTML5 +// (http://www.whatwg.org/specs/web-apps/current-work/#the-doctype) +// or use Box Sizing Behavior from WebFX +// (http://webfx.eae.net/dhtml/boxsizing/boxsizing.html) +// * Non uniform scaling does not correctly scale strokes. +// * Optimize. There is always room for speed improvements. + +// Only add this code if we do not already have a canvas implementation +if (!document.createElement('canvas').getContext) { + +(function() { + + // alias some functions to make (compiled) code shorter + var m = Math; + var mr = m.round; + var ms = m.sin; + var mc = m.cos; + var abs = m.abs; + var sqrt = m.sqrt; + + // this is used for sub pixel precision + var Z = 10; + var Z2 = Z / 2; + + var IE_VERSION = +navigator.userAgent.match(/MSIE ([\d.]+)?/)[1]; + + /** + * This funtion is assigned to the <canvas> elements as element.getContext(). + * @this {HTMLElement} + * @return {CanvasRenderingContext2D_} + */ + function getContext() { + return this.context_ || + (this.context_ = new CanvasRenderingContext2D_(this)); + } + + var slice = Array.prototype.slice; + + /** + * Binds a function to an object. The returned function will always use the + * passed in {@code obj} as {@code this}. + * + * Example: + * + * g = bind(f, obj, a, b) + * g(c, d) // will do f.call(obj, a, b, c, d) + * + * @param {Function} f The function to bind the object to + * @param {Object} obj The object that should act as this when the function + * is called + * @param {*} var_args Rest arguments that will be used as the initial + * arguments when the function is called + * @return {Function} A new function that has bound this + */ + function bind(f, obj, var_args) { + var a = slice.call(arguments, 2); + return function() { + return f.apply(obj, a.concat(slice.call(arguments))); + }; + } + + function encodeHtmlAttribute(s) { + return String(s).replace(/&/g, '&').replace(/"/g, '"'); + } + + function addNamespace(doc, prefix, urn) { + if (!doc.namespaces[prefix]) { + doc.namespaces.add(prefix, urn, '#default#VML'); + } + } + + function addNamespacesAndStylesheet(doc) { + addNamespace(doc, 'g_vml_', 'urn:schemas-microsoft-com:vml'); + addNamespace(doc, 'g_o_', 'urn:schemas-microsoft-com:office:office'); + + // Setup default CSS. Only add one style sheet per document + if (!doc.styleSheets['ex_canvas_']) { + var ss = doc.createStyleSheet(); + ss.owningElement.id = 'ex_canvas_'; + ss.cssText = 'canvas{display:inline-block;overflow:hidden;' + + // default size is 300x150 in Gecko and Opera + 'text-align:left;width:300px;height:150px}'; + } + } + + // Add namespaces and stylesheet at startup. + addNamespacesAndStylesheet(document); + + var G_vmlCanvasManager_ = { + init: function(opt_doc) { + var doc = opt_doc || document; + // Create a dummy element so that IE will allow canvas elements to be + // recognized. + doc.createElement('canvas'); + doc.attachEvent('onreadystatechange', bind(this.init_, this, doc)); + }, + + init_: function(doc) { + // find all canvas elements + var els = doc.getElementsByTagName('canvas'); + for (var i = 0; i < els.length; i++) { + this.initElement(els[i]); + } + }, + + /** + * Public initializes a canvas element so that it can be used as canvas + * element from now on. This is called automatically before the page is + * loaded but if you are creating elements using createElement you need to + * make sure this is called on the element. + * @param {HTMLElement} el The canvas element to initialize. + * @return {HTMLElement} the element that was created. + */ + initElement: function(el) { + if (!el.getContext) { + el.getContext = getContext; + + // Add namespaces and stylesheet to document of the element. + addNamespacesAndStylesheet(el.ownerDocument); + + // Remove fallback content. There is no way to hide text nodes so we + // just remove all childNodes. We could hide all elements and remove + // text nodes but who really cares about the fallback content. + el.innerHTML = ''; + + // do not use inline function because that will leak memory + el.attachEvent('onpropertychange', onPropertyChange); + el.attachEvent('onresize', onResize); + + var attrs = el.attributes; + if (attrs.width && attrs.width.specified) { + // TODO: use runtimeStyle and coordsize + // el.getContext().setWidth_(attrs.width.nodeValue); + el.style.width = attrs.width.nodeValue + 'px'; + } else { + el.width = el.clientWidth; + } + if (attrs.height && attrs.height.specified) { + // TODO: use runtimeStyle and coordsize + // el.getContext().setHeight_(attrs.height.nodeValue); + el.style.height = attrs.height.nodeValue + 'px'; + } else { + el.height = el.clientHeight; + } + //el.getContext().setCoordsize_() + } + return el; + } + }; + + function onPropertyChange(e) { + var el = e.srcElement; + + switch (e.propertyName) { + case 'width': + el.getContext().clearRect(); + el.style.width = el.attributes.width.nodeValue + 'px'; + // In IE8 this does not trigger onresize. + el.firstChild.style.width = el.clientWidth + 'px'; + break; + case 'height': + el.getContext().clearRect(); + el.style.height = el.attributes.height.nodeValue + 'px'; + el.firstChild.style.height = el.clientHeight + 'px'; + break; + } + } + + function onResize(e) { + var el = e.srcElement; + if (el.firstChild) { + el.firstChild.style.width = el.clientWidth + 'px'; + el.firstChild.style.height = el.clientHeight + 'px'; + } + } + + G_vmlCanvasManager_.init(); + + // precompute "00" to "FF" + var decToHex = []; + for (var i = 0; i < 16; i++) { + for (var j = 0; j < 16; j++) { + decToHex[i * 16 + j] = i.toString(16) + j.toString(16); + } + } + + function createMatrixIdentity() { + return [ + [1, 0, 0], + [0, 1, 0], + [0, 0, 1] + ]; + } + + function matrixMultiply(m1, m2) { + var result = createMatrixIdentity(); + + for (var x = 0; x < 3; x++) { + for (var y = 0; y < 3; y++) { + var sum = 0; + + for (var z = 0; z < 3; z++) { + sum += m1[x][z] * m2[z][y]; + } + + result[x][y] = sum; + } + } + return result; + } + + function copyState(o1, o2) { + o2.fillStyle = o1.fillStyle; + o2.lineCap = o1.lineCap; + o2.lineJoin = o1.lineJoin; + o2.lineWidth = o1.lineWidth; + o2.miterLimit = o1.miterLimit; + o2.shadowBlur = o1.shadowBlur; + o2.shadowColor = o1.shadowColor; + o2.shadowOffsetX = o1.shadowOffsetX; + o2.shadowOffsetY = o1.shadowOffsetY; + o2.strokeStyle = o1.strokeStyle; + o2.globalAlpha = o1.globalAlpha; + o2.font = o1.font; + o2.textAlign = o1.textAlign; + o2.textBaseline = o1.textBaseline; + o2.arcScaleX_ = o1.arcScaleX_; + o2.arcScaleY_ = o1.arcScaleY_; + o2.lineScale_ = o1.lineScale_; + } + + var colorData = { + aliceblue: '#F0F8FF', + antiquewhite: '#FAEBD7', + aquamarine: '#7FFFD4', + azure: '#F0FFFF', + beige: '#F5F5DC', + bisque: '#FFE4C4', + black: '#000000', + blanchedalmond: '#FFEBCD', + blueviolet: '#8A2BE2', + brown: '#A52A2A', + burlywood: '#DEB887', + cadetblue: '#5F9EA0', + chartreuse: '#7FFF00', + chocolate: '#D2691E', + coral: '#FF7F50', + cornflowerblue: '#6495ED', + cornsilk: '#FFF8DC', + crimson: '#DC143C', + cyan: '#00FFFF', + darkblue: '#00008B', + darkcyan: '#008B8B', + darkgoldenrod: '#B8860B', + darkgray: '#A9A9A9', + darkgreen: '#006400', + darkgrey: '#A9A9A9', + darkkhaki: '#BDB76B', + darkmagenta: '#8B008B', + darkolivegreen: '#556B2F', + darkorange: '#FF8C00', + darkorchid: '#9932CC', + darkred: '#8B0000', + darksalmon: '#E9967A', + darkseagreen: '#8FBC8F', + darkslateblue: '#483D8B', + darkslategray: '#2F4F4F', + darkslategrey: '#2F4F4F', + darkturquoise: '#00CED1', + darkviolet: '#9400D3', + deeppink: '#FF1493', + deepskyblue: '#00BFFF', + dimgray: '#696969', + dimgrey: '#696969', + dodgerblue: '#1E90FF', + firebrick: '#B22222', + floralwhite: '#FFFAF0', + forestgreen: '#228B22', + gainsboro: '#DCDCDC', + ghostwhite: '#F8F8FF', + gold: '#FFD700', + goldenrod: '#DAA520', + grey: '#808080', + greenyellow: '#ADFF2F', + honeydew: '#F0FFF0', + hotpink: '#FF69B4', + indianred: '#CD5C5C', + indigo: '#4B0082', + ivory: '#FFFFF0', + khaki: '#F0E68C', + lavender: '#E6E6FA', + lavenderblush: '#FFF0F5', + lawngreen: '#7CFC00', + lemonchiffon: '#FFFACD', + lightblue: '#ADD8E6', + lightcoral: '#F08080', + lightcyan: '#E0FFFF', + lightgoldenrodyellow: '#FAFAD2', + lightgreen: '#90EE90', + lightgrey: '#D3D3D3', + lightpink: '#FFB6C1', + lightsalmon: '#FFA07A', + lightseagreen: '#20B2AA', + lightskyblue: '#87CEFA', + lightslategray: '#778899', + lightslategrey: '#778899', + lightsteelblue: '#B0C4DE', + lightyellow: '#FFFFE0', + limegreen: '#32CD32', + linen: '#FAF0E6', + magenta: '#FF00FF', + mediumaquamarine: '#66CDAA', + mediumblue: '#0000CD', + mediumorchid: '#BA55D3', + mediumpurple: '#9370DB', + mediumseagreen: '#3CB371', + mediumslateblue: '#7B68EE', + mediumspringgreen: '#00FA9A', + mediumturquoise: '#48D1CC', + mediumvioletred: '#C71585', + midnightblue: '#191970', + mintcream: '#F5FFFA', + mistyrose: '#FFE4E1', + moccasin: '#FFE4B5', + navajowhite: '#FFDEAD', + oldlace: '#FDF5E6', + olivedrab: '#6B8E23', + orange: '#FFA500', + orangered: '#FF4500', + orchid: '#DA70D6', + palegoldenrod: '#EEE8AA', + palegreen: '#98FB98', + paleturquoise: '#AFEEEE', + palevioletred: '#DB7093', + papayawhip: '#FFEFD5', + peachpuff: '#FFDAB9', + peru: '#CD853F', + pink: '#FFC0CB', + plum: '#DDA0DD', + powderblue: '#B0E0E6', + rosybrown: '#BC8F8F', + royalblue: '#4169E1', + saddlebrown: '#8B4513', + salmon: '#FA8072', + sandybrown: '#F4A460', + seagreen: '#2E8B57', + seashell: '#FFF5EE', + sienna: '#A0522D', + skyblue: '#87CEEB', + slateblue: '#6A5ACD', + slategray: '#708090', + slategrey: '#708090', + snow: '#FFFAFA', + springgreen: '#00FF7F', + steelblue: '#4682B4', + tan: '#D2B48C', + thistle: '#D8BFD8', + tomato: '#FF6347', + turquoise: '#40E0D0', + violet: '#EE82EE', + wheat: '#F5DEB3', + whitesmoke: '#F5F5F5', + yellowgreen: '#9ACD32' + }; + + + function getRgbHslContent(styleString) { + var start = styleString.indexOf('(', 3); + var end = styleString.indexOf(')', start + 1); + var parts = styleString.substring(start + 1, end).split(','); + // add alpha if needed + if (parts.length != 4 || styleString.charAt(3) != 'a') { + parts[3] = 1; + } + return parts; + } + + function percent(s) { + return parseFloat(s) / 100; + } + + function clamp(v, min, max) { + return Math.min(max, Math.max(min, v)); + } + + function hslToRgb(parts){ + var r, g, b, h, s, l; + h = parseFloat(parts[0]) / 360 % 360; + if (h < 0) + h++; + s = clamp(percent(parts[1]), 0, 1); + l = clamp(percent(parts[2]), 0, 1); + if (s == 0) { + r = g = b = l; // achromatic + } else { + var q = l < 0.5 ? l * (1 + s) : l + s - l * s; + var p = 2 * l - q; + r = hueToRgb(p, q, h + 1 / 3); + g = hueToRgb(p, q, h); + b = hueToRgb(p, q, h - 1 / 3); + } + + return '#' + decToHex[Math.floor(r * 255)] + + decToHex[Math.floor(g * 255)] + + decToHex[Math.floor(b * 255)]; + } + + function hueToRgb(m1, m2, h) { + if (h < 0) + h++; + if (h > 1) + h--; + + if (6 * h < 1) + return m1 + (m2 - m1) * 6 * h; + else if (2 * h < 1) + return m2; + else if (3 * h < 2) + return m1 + (m2 - m1) * (2 / 3 - h) * 6; + else + return m1; + } + + var processStyleCache = {}; + + function processStyle(styleString) { + if (styleString in processStyleCache) { + return processStyleCache[styleString]; + } + + var str, alpha = 1; + + styleString = String(styleString); + if (styleString.charAt(0) == '#') { + str = styleString; + } else if (/^rgb/.test(styleString)) { + var parts = getRgbHslContent(styleString); + var str = '#', n; + for (var i = 0; i < 3; i++) { + if (parts[i].indexOf('%') != -1) { + n = Math.floor(percent(parts[i]) * 255); + } else { + n = +parts[i]; + } + str += decToHex[clamp(n, 0, 255)]; + } + alpha = +parts[3]; + } else if (/^hsl/.test(styleString)) { + var parts = getRgbHslContent(styleString); + str = hslToRgb(parts); + alpha = parts[3]; + } else { + str = colorData[styleString] || styleString; + } + return processStyleCache[styleString] = {color: str, alpha: alpha}; + } + + var DEFAULT_STYLE = { + style: 'normal', + variant: 'normal', + weight: 'normal', + size: 10, + family: 'sans-serif' + }; + + // Internal text style cache + var fontStyleCache = {}; + + function processFontStyle(styleString) { + if (fontStyleCache[styleString]) { + return fontStyleCache[styleString]; + } + + var el = document.createElement('div'); + var style = el.style; + try { + style.font = styleString; + } catch (ex) { + // Ignore failures to set to invalid font. + } + + return fontStyleCache[styleString] = { + style: style.fontStyle || DEFAULT_STYLE.style, + variant: style.fontVariant || DEFAULT_STYLE.variant, + weight: style.fontWeight || DEFAULT_STYLE.weight, + size: style.fontSize || DEFAULT_STYLE.size, + family: style.fontFamily || DEFAULT_STYLE.family + }; + } + + function getComputedStyle(style, element) { + var computedStyle = {}; + + for (var p in style) { + computedStyle[p] = style[p]; + } + + // Compute the size + var canvasFontSize = parseFloat(element.currentStyle.fontSize), + fontSize = parseFloat(style.size); + + if (typeof style.size == 'number') { + computedStyle.size = style.size; + } else if (style.size.indexOf('px') != -1) { + computedStyle.size = fontSize; + } else if (style.size.indexOf('em') != -1) { + computedStyle.size = canvasFontSize * fontSize; + } else if(style.size.indexOf('%') != -1) { + computedStyle.size = (canvasFontSize / 100) * fontSize; + } else if (style.size.indexOf('pt') != -1) { + computedStyle.size = fontSize / .75; + } else { + computedStyle.size = canvasFontSize; + } + + // Different scaling between normal text and VML text. This was found using + // trial and error to get the same size as non VML text. + computedStyle.size *= 0.981; + + return computedStyle; + } + + function buildStyle(style) { + return style.style + ' ' + style.variant + ' ' + style.weight + ' ' + + style.size + 'px ' + style.family; + } + + var lineCapMap = { + 'butt': 'flat', + 'round': 'round' + }; + + function processLineCap(lineCap) { + return lineCapMap[lineCap] || 'square'; + } + + /** + * This class implements CanvasRenderingContext2D interface as described by + * the WHATWG. + * @param {HTMLElement} canvasElement The element that the 2D context should + * be associated with + */ + function CanvasRenderingContext2D_(canvasElement) { + this.m_ = createMatrixIdentity(); + + this.mStack_ = []; + this.aStack_ = []; + this.currentPath_ = []; + + // Canvas context properties + this.strokeStyle = '#000'; + this.fillStyle = '#000'; + + this.lineWidth = 1; + this.lineJoin = 'miter'; + this.lineCap = 'butt'; + this.miterLimit = Z * 1; + this.globalAlpha = 1; + this.font = '10px sans-serif'; + this.textAlign = 'left'; + this.textBaseline = 'alphabetic'; + this.canvas = canvasElement; + + var cssText = 'width:' + canvasElement.clientWidth + 'px;height:' + + canvasElement.clientHeight + 'px;overflow:hidden;position:absolute'; + var el = canvasElement.ownerDocument.createElement('div'); + el.style.cssText = cssText; + canvasElement.appendChild(el); + + var overlayEl = el.cloneNode(false); + // Use a non transparent background. + overlayEl.style.backgroundColor = 'red'; + overlayEl.style.filter = 'alpha(opacity=0)'; + canvasElement.appendChild(overlayEl); + + this.element_ = el; + this.arcScaleX_ = 1; + this.arcScaleY_ = 1; + this.lineScale_ = 1; + } + + var contextPrototype = CanvasRenderingContext2D_.prototype; + contextPrototype.clearRect = function() { + if (this.textMeasureEl_) { + this.textMeasureEl_.removeNode(true); + this.textMeasureEl_ = null; + } + this.element_.innerHTML = ''; + }; + + contextPrototype.beginPath = function() { + // TODO: Branch current matrix so that save/restore has no effect + // as per safari docs. + this.currentPath_ = []; + }; + + contextPrototype.moveTo = function(aX, aY) { + var p = getCoords(this, aX, aY); + this.currentPath_.push({type: 'moveTo', x: p.x, y: p.y}); + this.currentX_ = p.x; + this.currentY_ = p.y; + }; + + contextPrototype.lineTo = function(aX, aY) { + var p = getCoords(this, aX, aY); + this.currentPath_.push({type: 'lineTo', x: p.x, y: p.y}); + + this.currentX_ = p.x; + this.currentY_ = p.y; + }; + + contextPrototype.bezierCurveTo = function(aCP1x, aCP1y, + aCP2x, aCP2y, + aX, aY) { + var p = getCoords(this, aX, aY); + var cp1 = getCoords(this, aCP1x, aCP1y); + var cp2 = getCoords(this, aCP2x, aCP2y); + bezierCurveTo(this, cp1, cp2, p); + }; + + // Helper function that takes the already fixed cordinates. + function bezierCurveTo(self, cp1, cp2, p) { + self.currentPath_.push({ + type: 'bezierCurveTo', + cp1x: cp1.x, + cp1y: cp1.y, + cp2x: cp2.x, + cp2y: cp2.y, + x: p.x, + y: p.y + }); + self.currentX_ = p.x; + self.currentY_ = p.y; + } + + contextPrototype.quadraticCurveTo = function(aCPx, aCPy, aX, aY) { + // the following is lifted almost directly from + // http://developer.mozilla.org/en/docs/Canvas_tutorial:Drawing_shapes + + var cp = getCoords(this, aCPx, aCPy); + var p = getCoords(this, aX, aY); + + var cp1 = { + x: this.currentX_ + 2.0 / 3.0 * (cp.x - this.currentX_), + y: this.currentY_ + 2.0 / 3.0 * (cp.y - this.currentY_) + }; + var cp2 = { + x: cp1.x + (p.x - this.currentX_) / 3.0, + y: cp1.y + (p.y - this.currentY_) / 3.0 + }; + + bezierCurveTo(this, cp1, cp2, p); + }; + + contextPrototype.arc = function(aX, aY, aRadius, + aStartAngle, aEndAngle, aClockwise) { + aRadius *= Z; + var arcType = aClockwise ? 'at' : 'wa'; + + var xStart = aX + mc(aStartAngle) * aRadius - Z2; + var yStart = aY + ms(aStartAngle) * aRadius - Z2; + + var xEnd = aX + mc(aEndAngle) * aRadius - Z2; + var yEnd = aY + ms(aEndAngle) * aRadius - Z2; + + // IE won't render arches drawn counter clockwise if xStart == xEnd. + if (xStart == xEnd && !aClockwise) { + xStart += 0.125; // Offset xStart by 1/80 of a pixel. Use something + // that can be represented in binary + } + + var p = getCoords(this, aX, aY); + var pStart = getCoords(this, xStart, yStart); + var pEnd = getCoords(this, xEnd, yEnd); + + this.currentPath_.push({type: arcType, + x: p.x, + y: p.y, + radius: aRadius, + xStart: pStart.x, + yStart: pStart.y, + xEnd: pEnd.x, + yEnd: pEnd.y}); + + }; + + contextPrototype.rect = function(aX, aY, aWidth, aHeight) { + this.moveTo(aX, aY); + this.lineTo(aX + aWidth, aY); + this.lineTo(aX + aWidth, aY + aHeight); + this.lineTo(aX, aY + aHeight); + this.closePath(); + }; + + contextPrototype.strokeRect = function(aX, aY, aWidth, aHeight) { + var oldPath = this.currentPath_; + this.beginPath(); + + this.moveTo(aX, aY); + this.lineTo(aX + aWidth, aY); + this.lineTo(aX + aWidth, aY + aHeight); + this.lineTo(aX, aY + aHeight); + this.closePath(); + this.stroke(); + + this.currentPath_ = oldPath; + }; + + contextPrototype.fillRect = function(aX, aY, aWidth, aHeight) { + var oldPath = this.currentPath_; + this.beginPath(); + + this.moveTo(aX, aY); + this.lineTo(aX + aWidth, aY); + this.lineTo(aX + aWidth, aY + aHeight); + this.lineTo(aX, aY + aHeight); + this.closePath(); + this.fill(); + + this.currentPath_ = oldPath; + }; + + contextPrototype.createLinearGradient = function(aX0, aY0, aX1, aY1) { + var gradient = new CanvasGradient_('gradient'); + gradient.x0_ = aX0; + gradient.y0_ = aY0; + gradient.x1_ = aX1; + gradient.y1_ = aY1; + return gradient; + }; + + contextPrototype.createRadialGradient = function(aX0, aY0, aR0, + aX1, aY1, aR1) { + var gradient = new CanvasGradient_('gradientradial'); + gradient.x0_ = aX0; + gradient.y0_ = aY0; + gradient.r0_ = aR0; + gradient.x1_ = aX1; + gradient.y1_ = aY1; + gradient.r1_ = aR1; + return gradient; + }; + + contextPrototype.drawImage = function(image, var_args) { + var dx, dy, dw, dh, sx, sy, sw, sh; + + // to find the original width we overide the width and height + var oldRuntimeWidth = image.runtimeStyle.width; + var oldRuntimeHeight = image.runtimeStyle.height; + image.runtimeStyle.width = 'auto'; + image.runtimeStyle.height = 'auto'; + + // get the original size + var w = image.width; + var h = image.height; + + // and remove overides + image.runtimeStyle.width = oldRuntimeWidth; + image.runtimeStyle.height = oldRuntimeHeight; + + if (arguments.length == 3) { + dx = arguments[1]; + dy = arguments[2]; + sx = sy = 0; + sw = dw = w; + sh = dh = h; + } else if (arguments.length == 5) { + dx = arguments[1]; + dy = arguments[2]; + dw = arguments[3]; + dh = arguments[4]; + sx = sy = 0; + sw = w; + sh = h; + } else if (arguments.length == 9) { + sx = arguments[1]; + sy = arguments[2]; + sw = arguments[3]; + sh = arguments[4]; + dx = arguments[5]; + dy = arguments[6]; + dw = arguments[7]; + dh = arguments[8]; + } else { + throw Error('Invalid number of arguments'); + } + + var d = getCoords(this, dx, dy); + + var w2 = sw / 2; + var h2 = sh / 2; + + var vmlStr = []; + + var W = 10; + var H = 10; + + // For some reason that I've now forgotten, using divs didn't work + vmlStr.push(' <g_vml_:group', + ' coordsize="', Z * W, ',', Z * H, '"', + ' coordorigin="0,0"' , + ' style="width:', W, 'px;height:', H, 'px;position:absolute;'); + + // If filters are necessary (rotation exists), create them + // filters are bog-slow, so only create them if abbsolutely necessary + // The following check doesn't account for skews (which don't exist + // in the canvas spec (yet) anyway. + + if (this.m_[0][0] != 1 || this.m_[0][1] || + this.m_[1][1] != 1 || this.m_[1][0]) { + var filter = []; + + // Note the 12/21 reversal + filter.push('M11=', this.m_[0][0], ',', + 'M12=', this.m_[1][0], ',', + 'M21=', this.m_[0][1], ',', + 'M22=', this.m_[1][1], ',', + 'Dx=', mr(d.x / Z), ',', + 'Dy=', mr(d.y / Z), ''); + + // Bounding box calculation (need to minimize displayed area so that + // filters don't waste time on unused pixels. + var max = d; + var c2 = getCoords(this, dx + dw, dy); + var c3 = getCoords(this, dx, dy + dh); + var c4 = getCoords(this, dx + dw, dy + dh); + + max.x = m.max(max.x, c2.x, c3.x, c4.x); + max.y = m.max(max.y, c2.y, c3.y, c4.y); + + vmlStr.push('padding:0 ', mr(max.x / Z), 'px ', mr(max.y / Z), + 'px 0;filter:progid:DXImageTransform.Microsoft.Matrix(', + filter.join(''), ", sizingmethod='clip');"); + + } else { + vmlStr.push('top:', mr(d.y / Z), 'px;left:', mr(d.x / Z), 'px;'); + } + + vmlStr.push(' ">' , + '<g_vml_:image src="', image.src, '"', + ' style="width:', Z * dw, 'px;', + ' height:', Z * dh, 'px"', + ' cropleft="', sx / w, '"', + ' croptop="', sy / h, '"', + ' cropright="', (w - sx - sw) / w, '"', + ' cropbottom="', (h - sy - sh) / h, '"', + ' />', + '</g_vml_:group>'); + + this.element_.insertAdjacentHTML('BeforeEnd', vmlStr.join('')); + }; + + contextPrototype.stroke = function(aFill) { + var lineStr = []; + var lineOpen = false; + + var W = 10; + var H = 10; + + lineStr.push('<g_vml_:shape', + ' filled="', !!aFill, '"', + ' style="position:absolute;width:', W, 'px;height:', H, 'px;"', + ' coordorigin="0,0"', + ' coordsize="', Z * W, ',', Z * H, '"', + ' stroked="', !aFill, '"', + ' path="'); + + var newSeq = false; + var min = {x: null, y: null}; + var max = {x: null, y: null}; + + for (var i = 0; i < this.currentPath_.length; i++) { + var p = this.currentPath_[i]; + var c; + + switch (p.type) { + case 'moveTo': + c = p; + lineStr.push(' m ', mr(p.x), ',', mr(p.y)); + break; + case 'lineTo': + lineStr.push(' l ', mr(p.x), ',', mr(p.y)); + break; + case 'close': + lineStr.push(' x '); + p = null; + break; + case 'bezierCurveTo': + lineStr.push(' c ', + mr(p.cp1x), ',', mr(p.cp1y), ',', + mr(p.cp2x), ',', mr(p.cp2y), ',', + mr(p.x), ',', mr(p.y)); + break; + case 'at': + case 'wa': + lineStr.push(' ', p.type, ' ', + mr(p.x - this.arcScaleX_ * p.radius), ',', + mr(p.y - this.arcScaleY_ * p.radius), ' ', + mr(p.x + this.arcScaleX_ * p.radius), ',', + mr(p.y + this.arcScaleY_ * p.radius), ' ', + mr(p.xStart), ',', mr(p.yStart), ' ', + mr(p.xEnd), ',', mr(p.yEnd)); + break; + } + + + // TODO: Following is broken for curves due to + // move to proper paths. + + // Figure out dimensions so we can do gradient fills + // properly + if (p) { + if (min.x == null || p.x < min.x) { + min.x = p.x; + } + if (max.x == null || p.x > max.x) { + max.x = p.x; + } + if (min.y == null || p.y < min.y) { + min.y = p.y; + } + if (max.y == null || p.y > max.y) { + max.y = p.y; + } + } + } + lineStr.push(' ">'); + + if (!aFill) { + appendStroke(this, lineStr); + } else { + appendFill(this, lineStr, min, max); + } + + lineStr.push('</g_vml_:shape>'); + + this.element_.insertAdjacentHTML('beforeEnd', lineStr.join('')); + }; + + function appendStroke(ctx, lineStr) { + var a = processStyle(ctx.strokeStyle); + var color = a.color; + var opacity = a.alpha * ctx.globalAlpha; + var lineWidth = ctx.lineScale_ * ctx.lineWidth; + + // VML cannot correctly render a line if the width is less than 1px. + // In that case, we dilute the color to make the line look thinner. + if (lineWidth < 1) { + opacity *= lineWidth; + } + + lineStr.push( + '<g_vml_:stroke', + ' opacity="', opacity, '"', + ' joinstyle="', ctx.lineJoin, '"', + ' miterlimit="', ctx.miterLimit, '"', + ' endcap="', processLineCap(ctx.lineCap), '"', + ' weight="', lineWidth, 'px"', + ' color="', color, '" />' + ); + } + + function appendFill(ctx, lineStr, min, max) { + var fillStyle = ctx.fillStyle; + var arcScaleX = ctx.arcScaleX_; + var arcScaleY = ctx.arcScaleY_; + var width = max.x - min.x; + var height = max.y - min.y; + if (fillStyle instanceof CanvasGradient_) { + // TODO: Gradients transformed with the transformation matrix. + var angle = 0; + var focus = {x: 0, y: 0}; + + // additional offset + var shift = 0; + // scale factor for offset + var expansion = 1; + + if (fillStyle.type_ == 'gradient') { + var x0 = fillStyle.x0_ / arcScaleX; + var y0 = fillStyle.y0_ / arcScaleY; + var x1 = fillStyle.x1_ / arcScaleX; + var y1 = fillStyle.y1_ / arcScaleY; + var p0 = getCoords(ctx, x0, y0); + var p1 = getCoords(ctx, x1, y1); + var dx = p1.x - p0.x; + var dy = p1.y - p0.y; + angle = Math.atan2(dx, dy) * 180 / Math.PI; + + // The angle should be a non-negative number. + if (angle < 0) { + angle += 360; + } + + // Very small angles produce an unexpected result because they are + // converted to a scientific notation string. + if (angle < 1e-6) { + angle = 0; + } + } else { + var p0 = getCoords(ctx, fillStyle.x0_, fillStyle.y0_); + focus = { + x: (p0.x - min.x) / width, + y: (p0.y - min.y) / height + }; + + width /= arcScaleX * Z; + height /= arcScaleY * Z; + var dimension = m.max(width, height); + shift = 2 * fillStyle.r0_ / dimension; + expansion = 2 * fillStyle.r1_ / dimension - shift; + } + + // We need to sort the color stops in ascending order by offset, + // otherwise IE won't interpret it correctly. + var stops = fillStyle.colors_; + stops.sort(function(cs1, cs2) { + return cs1.offset - cs2.offset; + }); + + var length = stops.length; + var color1 = stops[0].color; + var color2 = stops[length - 1].color; + var opacity1 = stops[0].alpha * ctx.globalAlpha; + var opacity2 = stops[length - 1].alpha * ctx.globalAlpha; + + var colors = []; + for (var i = 0; i < length; i++) { + var stop = stops[i]; + colors.push(stop.offset * expansion + shift + ' ' + stop.color); + } + + // When colors attribute is used, the meanings of opacity and o:opacity2 + // are reversed. + lineStr.push('<g_vml_:fill type="', fillStyle.type_, '"', + ' method="none" focus="100%"', + ' color="', color1, '"', + ' color2="', color2, '"', + ' colors="', colors.join(','), '"', + ' opacity="', opacity2, '"', + ' g_o_:opacity2="', opacity1, '"', + ' angle="', angle, '"', + ' focusposition="', focus.x, ',', focus.y, '" />'); + } else if (fillStyle instanceof CanvasPattern_) { + if (width && height) { + var deltaLeft = -min.x; + var deltaTop = -min.y; + lineStr.push('<g_vml_:fill', + ' position="', + deltaLeft / width * arcScaleX * arcScaleX, ',', + deltaTop / height * arcScaleY * arcScaleY, '"', + ' type="tile"', + // TODO: Figure out the correct size to fit the scale. + //' size="', w, 'px ', h, 'px"', + ' src="', fillStyle.src_, '" />'); + } + } else { + var a = processStyle(ctx.fillStyle); + var color = a.color; + var opacity = a.alpha * ctx.globalAlpha; + lineStr.push('<g_vml_:fill color="', color, '" opacity="', opacity, + '" />'); + } + } + + contextPrototype.fill = function() { + this.stroke(true); + }; + + contextPrototype.closePath = function() { + this.currentPath_.push({type: 'close'}); + }; + + function getCoords(ctx, aX, aY) { + var m = ctx.m_; + return { + x: Z * (aX * m[0][0] + aY * m[1][0] + m[2][0]) - Z2, + y: Z * (aX * m[0][1] + aY * m[1][1] + m[2][1]) - Z2 + }; + }; + + contextPrototype.save = function() { + var o = {}; + copyState(this, o); + this.aStack_.push(o); + this.mStack_.push(this.m_); + this.m_ = matrixMultiply(createMatrixIdentity(), this.m_); + }; + + contextPrototype.restore = function() { + if (this.aStack_.length) { + copyState(this.aStack_.pop(), this); + this.m_ = this.mStack_.pop(); + } + }; + + function matrixIsFinite(m) { + return isFinite(m[0][0]) && isFinite(m[0][1]) && + isFinite(m[1][0]) && isFinite(m[1][1]) && + isFinite(m[2][0]) && isFinite(m[2][1]); + } + + function setM(ctx, m, updateLineScale) { + if (!matrixIsFinite(m)) { + return; + } + ctx.m_ = m; + + if (updateLineScale) { + // Get the line scale. + // Determinant of this.m_ means how much the area is enlarged by the + // transformation. So its square root can be used as a scale factor + // for width. + var det = m[0][0] * m[1][1] - m[0][1] * m[1][0]; + ctx.lineScale_ = sqrt(abs(det)); + } + } + + contextPrototype.translate = function(aX, aY) { + var m1 = [ + [1, 0, 0], + [0, 1, 0], + [aX, aY, 1] + ]; + + setM(this, matrixMultiply(m1, this.m_), false); + }; + + contextPrototype.rotate = function(aRot) { + var c = mc(aRot); + var s = ms(aRot); + + var m1 = [ + [c, s, 0], + [-s, c, 0], + [0, 0, 1] + ]; + + setM(this, matrixMultiply(m1, this.m_), false); + }; + + contextPrototype.scale = function(aX, aY) { + this.arcScaleX_ *= aX; + this.arcScaleY_ *= aY; + var m1 = [ + [aX, 0, 0], + [0, aY, 0], + [0, 0, 1] + ]; + + setM(this, matrixMultiply(m1, this.m_), true); + }; + + contextPrototype.transform = function(m11, m12, m21, m22, dx, dy) { + var m1 = [ + [m11, m12, 0], + [m21, m22, 0], + [dx, dy, 1] + ]; + + setM(this, matrixMultiply(m1, this.m_), true); + }; + + contextPrototype.setTransform = function(m11, m12, m21, m22, dx, dy) { + var m = [ + [m11, m12, 0], + [m21, m22, 0], + [dx, dy, 1] + ]; + + setM(this, m, true); + }; + + /** + * The text drawing function. + * The maxWidth argument isn't taken in account, since no browser supports + * it yet. + */ + contextPrototype.drawText_ = function(text, x, y, maxWidth, stroke) { + var m = this.m_, + delta = 1000, + left = 0, + right = delta, + offset = {x: 0, y: 0}, + lineStr = []; + + var fontStyle = getComputedStyle(processFontStyle(this.font), + this.element_); + + var fontStyleString = buildStyle(fontStyle); + + var elementStyle = this.element_.currentStyle; + var textAlign = this.textAlign.toLowerCase(); + switch (textAlign) { + case 'left': + case 'center': + case 'right': + break; + case 'end': + textAlign = elementStyle.direction == 'ltr' ? 'right' : 'left'; + break; + case 'start': + textAlign = elementStyle.direction == 'rtl' ? 'right' : 'left'; + break; + default: + textAlign = 'left'; + } + + // 1.75 is an arbitrary number, as there is no info about the text baseline + switch (this.textBaseline) { + case 'hanging': + case 'top': + offset.y = fontStyle.size / 1.75; + break; + case 'middle': + break; + default: + case null: + case 'alphabetic': + case 'ideographic': + case 'bottom': + offset.y = -fontStyle.size / 2.25; + break; + } + + switch(textAlign) { + case 'right': + left = delta; + right = 0.05; + break; + case 'center': + left = right = delta / 2; + break; + } + + var d = getCoords(this, x + offset.x, y + offset.y); + + lineStr.push('<g_vml_:line from="', -left ,' 0" to="', right ,' 0.05" ', + ' coordsize="100 100" coordorigin="0 0"', + ' filled="', !stroke, '" stroked="', !!stroke, + '" style="position:absolute;width:1px;height:1px;">'); + + if (stroke) { + appendStroke(this, lineStr); + } else { + // TODO: Fix the min and max params. + appendFill(this, lineStr, {x: -left, y: 0}, + {x: right, y: fontStyle.size}); + } + + var skewM = m[0][0].toFixed(3) + ',' + m[1][0].toFixed(3) + ',' + + m[0][1].toFixed(3) + ',' + m[1][1].toFixed(3) + ',0,0'; + + var skewOffset = mr(d.x / Z) + ',' + mr(d.y / Z); + + lineStr.push('<g_vml_:skew on="t" matrix="', skewM ,'" ', + ' offset="', skewOffset, '" origin="', left ,' 0" />', + '<g_vml_:path textpathok="true" />', + '<g_vml_:textpath on="true" string="', + encodeHtmlAttribute(text), + '" style="v-text-align:', textAlign, + ';font:', encodeHtmlAttribute(fontStyleString), + '" /></g_vml_:line>'); + + this.element_.insertAdjacentHTML('beforeEnd', lineStr.join('')); + }; + + contextPrototype.fillText = function(text, x, y, maxWidth) { + this.drawText_(text, x, y, maxWidth, false); + }; + + contextPrototype.strokeText = function(text, x, y, maxWidth) { + this.drawText_(text, x, y, maxWidth, true); + }; + + contextPrototype.measureText = function(text) { + if (!this.textMeasureEl_) { + var s = '<span style="position:absolute;' + + 'top:-20000px;left:0;padding:0;margin:0;border:none;' + + 'white-space:pre;"></span>'; + this.element_.insertAdjacentHTML('beforeEnd', s); + this.textMeasureEl_ = this.element_.lastChild; + } + var doc = this.element_.ownerDocument; + this.textMeasureEl_.innerHTML = ''; + this.textMeasureEl_.style.font = this.font; + // Don't use innerHTML or innerText because they allow markup/whitespace. + this.textMeasureEl_.appendChild(doc.createTextNode(text)); + return {width: this.textMeasureEl_.offsetWidth}; + }; + + /******** STUBS ********/ + contextPrototype.clip = function() { + // TODO: Implement + }; + + contextPrototype.arcTo = function() { + // TODO: Implement + }; + + contextPrototype.createPattern = function(image, repetition) { + return new CanvasPattern_(image, repetition); + }; + + // Gradient / Pattern Stubs + function CanvasGradient_(aType) { + this.type_ = aType; + this.x0_ = 0; + this.y0_ = 0; + this.r0_ = 0; + this.x1_ = 0; + this.y1_ = 0; + this.r1_ = 0; + this.colors_ = []; + } + + CanvasGradient_.prototype.addColorStop = function(aOffset, aColor) { + aColor = processStyle(aColor); + this.colors_.push({offset: aOffset, + color: aColor.color, + alpha: aColor.alpha}); + }; + + function CanvasPattern_(image, repetition) { + assertImageIsValid(image); + switch (repetition) { + case 'repeat': + case null: + case '': + this.repetition_ = 'repeat'; + break + case 'repeat-x': + case 'repeat-y': + case 'no-repeat': + this.repetition_ = repetition; + break; + default: + throwException('SYNTAX_ERR'); + } + + this.src_ = image.src; + this.width_ = image.width; + this.height_ = image.height; + } + + function throwException(s) { + throw new DOMException_(s); + } + + function assertImageIsValid(img) { + if (!img || img.nodeType != 1 || img.tagName != 'IMG') { + throwException('TYPE_MISMATCH_ERR'); + } + if (img.readyState != 'complete') { + throwException('INVALID_STATE_ERR'); + } + } + + function DOMException_(s) { + this.code = this[s]; + this.message = s +': DOM Exception ' + this.code; + } + var p = DOMException_.prototype = new Error; + p.INDEX_SIZE_ERR = 1; + p.DOMSTRING_SIZE_ERR = 2; + p.HIERARCHY_REQUEST_ERR = 3; + p.WRONG_DOCUMENT_ERR = 4; + p.INVALID_CHARACTER_ERR = 5; + p.NO_DATA_ALLOWED_ERR = 6; + p.NO_MODIFICATION_ALLOWED_ERR = 7; + p.NOT_FOUND_ERR = 8; + p.NOT_SUPPORTED_ERR = 9; + p.INUSE_ATTRIBUTE_ERR = 10; + p.INVALID_STATE_ERR = 11; + p.SYNTAX_ERR = 12; + p.INVALID_MODIFICATION_ERR = 13; + p.NAMESPACE_ERR = 14; + p.INVALID_ACCESS_ERR = 15; + p.VALIDATION_ERR = 16; + p.TYPE_MISMATCH_ERR = 17; + + // set up externs + G_vmlCanvasManager = G_vmlCanvasManager_; + CanvasRenderingContext2D = CanvasRenderingContext2D_; + CanvasGradient = CanvasGradient_; + CanvasPattern = CanvasPattern_; + DOMException = DOMException_; +})(); + +} // if diff --git a/qpid/tools/src/java/qpid-qmf2-tools/bin/qpid-web/web/qmf-ui/scripts/qmf-ui.js b/qpid/tools/src/java/qpid-qmf2-tools/bin/qpid-web/web/qmf-ui/scripts/qmf-ui.js new file mode 100644 index 0000000000..4c24b44dbb --- /dev/null +++ b/qpid/tools/src/java/qpid-qmf2-tools/bin/qpid-web/web/qmf-ui/scripts/qmf-ui.js @@ -0,0 +1,3333 @@ +/* + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + * + */ + +/** + * + * This program implements the QMF Console User Interface logic for qmf.html + * + * It has dependencies on the following: + * qmf.css + * itablet.css + * iscroll.js + * jquery.js (jquery-1.7.1.min.js) + * itablet.js + * excanvas.js (for IE < 9 only) + * qpid.js + * + * author Fraser Adams + */ + +//------------------------------------------------------------------------------------------------------------------- + +// Create a new namespace for the qmfui "package". +var qmfui = {}; +qmfui.TOUCH_ENABLED = 'ontouchstart' in window && !((/hp-tablet/gi).test(navigator.appVersion)); +qmfui.END_EV = (qmfui.TOUCH_ENABLED) ? "touchend" : "mouseup"; + +//------------------------------------------------------------------------------------------------------------------- + +/** + * This class holds the history of various key statistics that may be held for some QMF + * Management Objects so we may see how the state has changes over a particular time range. + */ +qmfui.Statistics = function(description) { + this.description = description; // Array describing the contents of each stored statistic + this.short = new util.RingBuffer(60); // Statistics for a 10 minute period (10*60/REFRESH_PERIOD) + this.medium = new util.RingBuffer(60); // Statistics for a 1 hour period (Entries are updated every minute) + this.long = new util.RingBuffer(144); // Statistics for a 1 day period (Entries are updated every 10 minutes) + + /** + * Add an item to the end of each statistic buffer. + * @param item an array containing the current statistics for each property that we want to hold for a + * Management Object, the last item in the array is the Management Object's update timestamp. + * As an example for the connection Management Object we would do: + * stats.put([connection.msgsFromClient, connection.msgsToClient, connection._update_ts]); + * This approach is a little ugly and not terribly OO, but it's pretty memory efficient, which is + * important as there could be lots of Management Objects on a heavily utilised broker. + */ + this.put = function(item) { + var TIMESTAMP = item.length - 1; // The timestamp is stored as the last item of each sample. + var timestamp = item[TIMESTAMP]; + + var lastItem = this.short.getLast(); + if (lastItem == null) { + this.short.put(item); // Update the 10 minute period statistics. + } else { + var lastTimestamp = lastItem[TIMESTAMP]; + // 9000000000 is 9 seconds in nanoseconds. If the time delta is less than 9 seconds we hold off adding + // the sample otherwise the ring buffer will end up holding less than the full 10 minutes worth. + if ((timestamp - lastTimestamp) >= 9000000000) { + this.short.put(item); // Update the 10 minute period statistics. + } + } + + var lastItem = this.medium.getLast(); + if (lastItem == null) { + this.medium.put(item); // Update the 1 hour period statistics. + } else { + var lastTimestamp = lastItem[TIMESTAMP]; + // 59000000000 is 59 seconds in nanoseconds. We use 59 seconds rather than 60 seconds because the + // update period has a modest +/i variance around 10 seconds. + if ((timestamp - lastTimestamp) >= 59000000000) { + this.medium.put(item); // Update the 1 hour period statistics. + } + } + + lastItem = this.long.getLast(); + if (lastItem == null) { + this.long.put(item); // Update the 1 day period statistics. + } else { + var lastTimestamp = lastItem[TIMESTAMP]; + // 599000000000 is 599 seconds in nanoseconds (just short of 10 minutes). We use 599 seconds rather + // than 600 seconds because the update period has a modest +/ variance around 10 seconds. + if ((timestamp - lastTimestamp) >= 599000000000) { + this.long.put(item); // Update the 1 day period statistics. + } + } + }; + + /** + * This method computes the most recent instantaneous rate for the property specified by the index. + * For example for the Connection Management Object an index of 1 would represent msgsToClient as + * described in the comments for put(). Note that the rate that is returned is the most recent + * instantaneous rate, which means that it uses the samples held in the ring buffer used to hold + * the ten minute window. + * @param the index of the property that we wish to obtain the rate for. + * @return the most recent intantaneous rate in items/s + */ + this.getRate = function(index) { + var size = this.short.size(); + if (size < 2) { + return 0; + } + + var s1 = this.short.get(size - 2); + var t1 = s1[s1.length - 1]; + + var s2 = this.short.get(size - 1); + var t2 = s2[s2.length - 1]; + + var delta = (t2 == t1) ? 0.0000001 : t2 - t1; // Shouldn't happen, but this tries to avoid divide by zero. + var rate = ((s2[index] - s1[index]) * 1000000000)/delta + return rate; + }; +}; + +//------------------------------------------------------------------------------------------------------------------- +// Helper Methods to provide a consistent way to render the UI lists. +//------------------------------------------------------------------------------------------------------------------- + +/** + * This helper method renders the specified properties of the specifed JavaScript object to the specified html list. + * @param list jQuery object representing the html list (ul) we wish to populate. + * @param object the object whose properties we wish to render. + * @param props an array of properties that we wish to render. + * @param href optional string specifying URL fragment to e.g. "#graphs?connectionId=" + connectionId. + */ +qmfui.renderObject = function(list, object, props, href, useIndex) { + iTablet.renderList(list, function(i) { + var key = props[i]; + var value = object[key]; + if (value == null) { // Only show properties that are actually available. + return false; + } else { + if (href) { + var anchor = href + "&property=" + i; + return "<li class='arrow'><a href='" + anchor + "'>" + key + "<p>" + value + "</p></a></li>"; + } else { + return "<li><a href='#'>" + key + "<p>" + value + "</p></a></li>"; + } + } + }, props.length); +}; + +/** + * This helper method renders the specified list of html list items (li) to the specified html list. + * @param list jQuery object representing the html list (ul) we wish to populate. + * @param array the array of html list items (li) that we wish to render. + */ +qmfui.renderArray = function(list, array) { + iTablet.renderList(list, function(i) { + return array[i]; + }, array.length); +}; + +//------------------------------------------------------------------------------------------------------------------- +// Main Console Class +//------------------------------------------------------------------------------------------------------------------- + +/** + * Create a Singleton instance of the main Console class. + * This class contains the QMF Console and and caches the core QMF management objects. Caching the getObjects() + * results is obviously sensible and helps avoid the temptation to call getObjects() elsewhere which is rather + * inefficient as it is invoked using AMQP request/response but morever in the case of this UI is is called via + * a REST proxy. Caching getObjects() calls also helps to abstract the asynchronous nature of JavaScript as the + * cache methods can be called synchronously which "feels" a more natural way to get the data. + * + * This class is also responsible for initialising the rest of the pages when jQuery.ready() fires and updating + * them when QMF object updates occur, in other words it might be considered "Main". + * + * This class handles the QMF Console Connection lifecycle management. It's worth pointing out that it's fairly + * subtle and complex particularly due to the asynchronous nature of JavaScript. It's made even more complex + * by the fact that there are two distinct ways used to decide when to get the QMF Management Objects. The default + * way is where QMF Event delivery is enabled, in this case the onEvent() method is triggered periodically by + * the underlying QMF Console's Event dispatcher and in this case the Event dispatcher takes care of reconnection + * attempts. However if disableEvents is selected then the QMF Management Objects are retrieved via a timed + * poll. In this case this method must correctly start the pollForData() but must also let it expire if the user + * selects a new Connection that has QMF Event deliver enabled. + */ +qmfui.Console = new function() { + /** + * This is an array of QMF Console Connections that the user is interested in. It gets initiated with the + * default connection (that is to say the connection that the REST API has been configured to use if no + * explicit URL has been supplied). Using a Connection URL of "" makes the REST API use its default. + * This property has been exposed as a "public" property of qmfui.Console because then it becomes possible + * to "configure" the initial set of consoleConnections via a trivial config.js file containing: + * qmfui.Console.consoleConnections = [{name: "default", url: ""},{name: "wildcard", url: "0.0.0.0:5672", + * connectionOptions: {sasl_mechs:"ANONYMOUS"}, disableEvents: true}]; + * i.e. a JSON array of Console Connection settings. + */ + this.consoleConnections = [{name: "default", url: ""}]; + + var _objects = {}; // A map used to cache the QMF Management Object lists returned by getObjects(). + var _disableEvents = false; // Set for Connections that have Events disabled and thus refresh via timed polling. + var _polling = false; // Set when the timed polling is active so we can avoid starting it multiple times. + var _receivedData = false; // This flag is used to tell the difference between a failure to connect and a disconnect. + var _console = null; // The QMF Console used to retrieve information from the broker. + var _connection = null; + var _activeConsoleConnection = 0; // The index of the currently active QMF Console Connection. + + /** + * Resets the messages that get rendered if the REST API Server or the Qpid broker fail. + */ + var resetErrorMessages = function() { + $("#restapi-disconnected").hide(); + $("#broker-disconnected").hide(); + $("#failed-to-connect").hide(); + }; + + /** + * Show Rest API Disconnected Message, hide the others. + */ + var showRestAPIDisconnected = function() { + resetErrorMessages(); + $("#restapi-disconnected").show(); + }; + + /** + * Show Broker Disconnected Message, hide the others. + */ + var showBrokerDisconnected = function() { + resetErrorMessages(); + $("#broker-disconnected").show(); + }; + + /** + * Show Failed to Connect Message, hide the others. + */ + var showFailedToConnect = function() { + resetErrorMessages(); + $("#failed-to-connect").show(); + }; + + /** + * QMF2 EventHandler that we register with the QMF2 Console. + * @param workItem a QMF2 API WorkItem. + */ + var onEvent = function(workItem) { + if (workItem._type == "AGENT_DELETED") { + var agent = workItem._params.agent; + + if (agent._product == "qpidd" && _connection != null) { + if (_receivedData) { + showBrokerDisconnected(); + } else { + showFailedToConnect(); + } + } else if (agent._product == "qpid.restapi") { + showRestAPIDisconnected(); + } + } else { + _receivedData = true; + resetErrorMessages(); + if (workItem._type == "EVENT_RECEIVED") { + qmfui.Events.update(workItem); + } + } + + // onEvent() will be called periodically by the broker heartbeat events, so we use that fact to trigger + // a call to getAllObjects() which will itself trigger a call to updateState() when its results return. + getAllObjects(); + }; + + /** + * This method is called if startConsole() has been called with disableEvents. In this state + * the onEvent() method won't be triggered by the QMF2 callback so we need to explicitly poll via a timer. + */ + var pollForData = function() { + if (_connection != null && _disableEvents) { + _polling = true; + getAllObjects(); + setTimeout(function() { + pollForData(); + }, qmf.REFRESH_PERIOD); + } else { + _polling = false; + } + }; + + /** + * This method is called by the failure handler of getAllObjects(). If it is triggered it means there has + * been some form of Server side disconnection. If this occurs set an error banner then attempt to re-open + * the Qpid Connection. If the Connection reopens successfully updateState() will start getting called again. + * This method returns immediately if _disableEvents is false because the underlying QMF Console has its own + * reconnection logic in its Event dispatcher, which we only need to replicate if that is disabled. + * @param xhr the jQuery XHR object. + */ + var timeout = function(xhr) { + if (_disableEvents) { + if (xhr.status == 0) { + showRestAPIDisconnected(); + } else { + if (_receivedData) { + showBrokerDisconnected(); + } else { + showFailedToConnect(); + } + if (xhr.status == 404 && _connection != null && _connection.open) { + _connection.open(); + } + } + } + }; + + /** + * This method is called when getAllObjects() completes, that is to say when all of the asynchronous responses + * for the Deferred XHR objects returned by getObjects() have all returned successfully. This method triggers + * the update() method on each of the user interface pages. + */ + var updateState = function() { + _receivedData = true; + resetErrorMessages(); + + qmfui.Broker.update(); + qmfui.Connections.update(); + qmfui.Exchanges.update(); + qmfui.Queues.update(); + //qmfui.Links.update(); // TODO + //qmfui.RouteTopology.update(); // TODO + + // Update sub-pages after main pages as these may require state (such as statistics) set in the main pages. + qmfui.SelectedConnection.update(); + qmfui.SelectedQueue.update(); + qmfui.QueueSubscriptions.update(); + qmfui.ConnectionSubscriptions.update(); + qmfui.SelectedExchange.update(); + qmfui.Bindings.update(); + qmfui.Graphs.update(); + }; + + /** + * This method retrieves the QmfConsoleData Objects from the real QMF Console via AJAX calls to the REST API. + * Because the AJAX calls are all asynchronous we use jQuery.when(), which provides a way to execute callback + * functions based on one or more objects, usually Deferred objects that represent asynchronous events. + * See http://api.jquery.com/jQuery.when/ and http://api.jquery.com/deferred.then/ + */ + var getAllObjects = function() { + $.when( + _console.getObjects("broker", function(data) {_objects.broker = data;}), + _console.getObjects("queue", function(data) {_objects.queue = data;}), + _console.getObjects("exchange", function(data) {_objects.exchange = data;}), + _console.getObjects("binding", function(data) {_objects.binding = data;}), + _console.getObjects("subscription", function(data) {_objects.subscription = data;}), + _console.getObjects("connection", function(data) {_objects.connection = data;}), +// _console.getObjects("link", function(data) {_objects.link = data;}), +// _console.getObjects("bridge", function(data) {_objects.bridge = data;}), + _console.getObjects("session", function(data) {_objects.session = data;}) + ).then(updateState, timeout); + }; + + /** + * Handle the load event, triggered when the document has completely loaded. + */ + var loadHandler = function() { + qmfui.Console.startConsole(0); // Start the default QMF Console Connection. + }; + + /** + * Handle the unload event, triggered when the document unloads or we navigate off the page. It's not 100% + * reliable, which is a shame as it's the best way to clear up Server state. If it fails the Server will + * eventually clear up unused Connections after a timeout period. TODO Opera seems to be especially bad at + * firing the unloadHandler, not sure why this is, something to look into. + * @param event the event that triggered the unloadHandler. + */ + var unloadHandler = function(event) { + // For mobile Safari (at least) we get pagehide events when navigating off the page, but also when closing via + // home or locking the device. Fortunately this case has the persisted flag set as we don't want to close then. + var persisted = (event.type == "pagehide") ? event.originalEvent.persisted : false; + if (!persisted) { + qmfui.Console.stopConsole(); + } + }; + + /** + * Callback handler triggered when _console.addConnection() fails. This should only occur if an actual + * exception occurs on the REST API Server due to invalid connectionOptions. + */ + var handleConnectionFailure = function() { + qmfui.Console.stopConsole(); + showFailedToConnect(); + } + + // ******************************************* Accessor Methods ******************************************* + + /** + * Retrieve the broker Management Object, optionally as a QmfConsoleData Object (with invokeMethod attached). + * @param makeConsoleData if true attach the invokeMethod method to the returned Management Object. + * @return the QMF broker Management Object optionally as a QmfConsoleData. + */ + this.getBroker = function(makeConsoleData) { + var brokers = _objects.broker; + + if (brokers == null || brokers.length == 0) { + // Return a fake QmfConsoleData Object with an invokeMethod that calls the callback handler with a + // response object containing error_text. It is actually pretty uncommon for the brokers array to + // be empty so this approach allows us to call invokeMethod without lots of checking for broker == null. + return {invokeMethod: function(name, inArgs, handler) { + handler({"error_text" : "Could not retrieve broker Management Object"}); + }}; + } else { + var broker = brokers[0]; + if (makeConsoleData) { + _console.makeConsoleData(broker); + } + return broker; + } + }; + + /** + * These methods are basic accessors returning the cached lists of QmfData objects returned by getObjects() + */ + this.getQueues = function() { + return _objects.queue; + }; + + this.getExchanges = function() { + return _objects.exchange; + }; + + this.getBindings = function() { + return _objects.binding; + }; + + this.getSubscriptions = function() { + return _objects.subscription; + }; + + this.getConnections = function() { + return _objects.connection; + }; + + this.getSessions = function() { + return _objects.session; + }; + +/* TODO + this.getLinks = function() { + return _objects.link; + }; + + this.getBridges = function() { + return _objects.bridge; + }; +*/ + + /** + * Calls the underlying QMF Console's makeConsoleData(). This call turns a QmfData object into a QmfConsoleData + * object, which adds methods such as invokeMethod() to the object. + * @param the object that we want to turn into a QmfConsoleData. + */ + this.makeConsoleData = function(object) { + _console.makeConsoleData(object); + }; + + /** + * @return the list of QMF Console Connections that the user is interested in. The returned list is a list of + * objects containing url, name and connectionOptions properties. + */ + this.getConsoleConnectionList = function() { + return this.consoleConnections; + }; + + /** + * Note that in the following it's important to note that there is separation between adding/removing + * Console Connections and actually starting/stopping Console Connections, this is because a user may wish + * to add several QMF Console Connections to point to a number of different brokers before actually + * chosing to connect to a particular broker. Similarly a user may wish to delete a QMF Console Connection + * from the list (s)he is interested in independently from selecting a new connection. + */ + + /** + * Append a new Console Connection with the specified url, name and connectionOptions to the end of the + * list of QMF Console Connections the the user many be interested in. Note that this method *does not* + * actually start a connection to the new console for that we must call the startConsole() method. + * The name supplied in this method is simply a user friendly name and is not related to the name that + * may be applied to the Connection when it is stored on the REST API, that name is really best considered + * as an opaque "handle" and is intended to be unique for each connection. + * + * @param name a user friendly name for the Console Connection. + * @param url the broker Connection URL in one of the formats supported by the Java ConnectionHelper class + * namely an AMQP 0.10 URL, an extended AMQP 0-10 URL, a Broker URL or a Java Connection URL. + * @param connectionOptions a JSON string containing the Connection Options in the same form as used + * in the qpid::messaging API. + */ + this.addConsoleConnection = function(name, url, connectionOptions, disableEvents) { + if (disableEvents) { + this.consoleConnections.push({name: name, url: url, connectionOptions: connectionOptions, + disableEvents: true}); + } else { + this.consoleConnections.push({name: name, url: url, connectionOptions: connectionOptions}); + } + }; + + /** + * Remove the Console Connection specified by the index. Note that this method *does not* actually stop + * a connection to the console for that we must call the stopConsole() method. + * @param index the index of the Console Connection that we want to remove. + */ + this.removeConsoleConnection = function(index) { + if (_activeConsoleConnection > index) { + _activeConsoleConnection--; + } + this.consoleConnections.splice(index, 1); // remove a single array item at the specified index. + }; + + /** + * Actually start the Qpid Connection and QMF Console for the Console Connection stored at the specified index. + * When the QMF Console successfully starts it will start sending QMF Events and updating the Management + * Objects automatically, this will in turn cause the User Interface pages to refresh. + * Alternatively if QMF Event delivery is disabled this method initiates the pollForData(). + * @param index the index of the Console Connection that we wish to start. Index zero is the default Console. + */ + this.startConsole = function(index) { + var connection = this.consoleConnections[index]; + + // Using a Connection URL of "" makes the REST API use its default configured broker connection. + var factory = new qpid.ConnectionFactory(connection.url, connection.connectionOptions); + _connection = factory.createConnection(); + + // Initialise QMF Console + _console = new qmf.Console(onEvent); + if (connection.disableEvents != null) { + _disableEvents = true; + _console.disableEvents(); + } else { + _disableEvents = false; + } + + _receivedData = false; + _console.addConnection(_connection, handleConnectionFailure); + _activeConsoleConnection = index; + + // If disableEvents is set we have to use a timed poll to get the Management Objects. We check if the polling + // loop is already running, because if we try and start it multiple times we may get spurious refreshes. + if (_disableEvents && !_polling) { + pollForData(); + } + }; + + /** + * Stops the currently running Console Connection and closes the Qpid Connection. This will result in the + * underlying Connection object on the REST API Server getting properly cleaned up. It's not essential + * to call stopConsole() before a call to startConsole() as the server Connection objects will eventually + * time out, but it's good practice to do it if at all possible. + */ + this.stopConsole = function() { + if (_console) { + _console.destroy(); + } + + if (_connection) { + _connection.close(); + _connection = null; + } + }; + + /** + * @return the index of the currently active (connected) QMF Console Connection. + */ + this.getActiveConsoleConnection = function() { + return _activeConsoleConnection; + }; + + // *********************** Initialise class when the DOM loads using jQuery.ready() *********************** + $(function() { + // Create a fake logging console for browsers that don't have a real console.log - only for debugging. + if (!window.console) { + /* // A slightly hacky console.log() to help with debugging on old versions of IE. + console = window.open("", "console", "toolbar, menubar, status, width=500, height=500, scrollbars=yes"); + console.document.open("text/plain"); + console.log = function(text) {console.document.writeln(text);};*/ + + console = {log: function(text) {}}; // Dummy to avoid bad references in case logging accidentally added. + } + + // Add a default show handler. Pages that bind update() to show should remove this by doing unbind("show"). + $(".main").bind("show", function() {$("#resource-deleted").hide();}); + + // pagehide and unload each work better than the other in certain circumstances so we trigger on both. + $(window).bind("unload pagehide", unloadHandler); + + // Iterate through each page calling its initialise method if one is present. + for (var i in qmfui) { + if (qmfui[i].initialise) { + qmfui[i].initialise(); + } + } + + // Send a synthesised click event to the settings-tab element to select the settings page on startup. + // We check if the left property of the main class is zero, if it is then the sidebar has been expanded + // to become the main menu (e.g. for mobile devices) otherwise we show the settings page. + if (parseInt($(".main").css('left'), 10) != 0) { + $("#settings-tab").click(); + } + + // Hide the splash page. + $("#splash").hide(); + }); + + $(window).load(loadHandler); +}; // End of qmfui.Console definition + + +//------------------------------------------------------------------------------------------------------------------- +// Configure Settings +//------------------------------------------------------------------------------------------------------------------- + +/** + * Create a Singleton instance of the Settings class managing the id="settings" page. + */ +qmfui.Settings = new function() { + /** + * Show the Settings page, rendering any dynamic content if necessary. + */ + var show = function() { + // Retrieve the currently configured QMF Console Connections. + var qmfConsoleConnections = qmfui.Console.getConsoleConnectionList(); + + iTablet.renderList($("#qmf-console-selector"), function(i) { + var qmfConsoleConnection = qmfConsoleConnections[i]; + var name = qmfConsoleConnection.name; + var url = qmfConsoleConnection.url; + url = (url == null || url == "") ? "" : " (" + url + ")"; + var label = (name == null || name == "") ? url : name + url; + var checked = (i == qmfui.Console.getActiveConsoleConnection()) ? "checked" : ""; + + return "<li class='arrow'><label for='qmf-console" + i + "'>" + label + "</label><input type='radio' id='qmf-console" + i + "' name='qmf-console-selector' value='" + i + "' " + checked + "/><a href='#selected-qmf-console-connection?index=" + i + "'></a></li>"; + }, qmfConsoleConnections.length); + + $("#qmf-console-selector input").change(changeConsole); + }; + + /** + * If the settings-hide-qmf-objects checkbox gets changed refresh the Queues and Exchanges pages to reflect this. + */ + var changeHideQmf = function() { + qmfui.Queues.update(); + qmfui.Exchanges.update(); + }; + + /** + * Handles changes to the Console selection Radio buttons. If a change occurs the Console Connection is stopped + * and the newly selected Console Connection is started (we chose based on the index into the list) + */ + var changeConsole = function() { + qmfui.Console.stopConsole(); + qmfui.Console.startConsole($(this).val()); + }; + + this.initialise = function() { + $("#settings").bind("show", show); + $("#settings-hide-qmf-objects").change(changeHideQmf); + }; +}; // End of qmfui.Settings definition + + +/** + * Create a Singleton instance of the AddConsoleConnection class managing the id="add-console-connection" page. + */ +qmfui.AddConsoleConnection = new function() { + var submit = function() { + var consoleURL = $("#console-url"); + + // Check that a URL value has been supplied. TODO Probably worth doing some validation that the supplied + // Connection URL is at least syntactically valid too. + var url = consoleURL.val(); + if (url == "") { + consoleURL.addClass("error"); + return; + } else { + consoleURL.removeClass("error"); + } + + var name = $("#console-name").val(); + try { + var connectionOptions = $.parseJSON($("#add-connection-options textarea").val()); + // TODO worth checking that the connection options are valid and in a usable format. Connection Options + // is still a bit of a work in progressed. though it seems to work fine if sensible options are used. + qmfui.Console.addConsoleConnection(name, url, connectionOptions, $("#console-disable-events")[0].checked); + iTablet.location.back(); + } catch(e) { + setTimeout(function() { + alert("Connection Options must be entered as a well-formed JSON string."); + return; + }, 0); + } + }; + + this.initialise = function() { + // Using END_EV avoids the 300ms delay responding to anchor click events that occurs on mobile browsers. + $("#add-console-connection .right.button").bind(qmfui.END_EV, submit); + }; +}; // End of qmfui.AddConsoleConnection definition + + +/** + * Create a Singleton instance of the SelectedQMFConsoleConnection class managing the + * id="selected-qmf-console-connection" page. + */ +qmfui.SelectedQMFConsoleConnection = new function() { + var _index = null; + var _name = ""; + var _url = ""; + + /** + * This method deletes the selected QMFConsoleConnection. + */ + var deleteHandler = function() { + // Flag to check if the Console we're trying to delete is the currently selected/connected one. + var currentlySelected = ($("#qmf-console-selector input[checked]").val() == _index); + + // Text for the confirm dialogue with additional wording if it's currently connected. + var confirmText = 'Delete QMF Connection "' + _name + '" to ' + _url + '?'; + confirmText += currentlySelected ? "\nNote that this is the current active Connection, so deleting it will cause a reconnection to the default Connection." : "" + + // Wrap in a timeout call because confirm doesn't play nicely with touchend and causes it to trigger twice. + // Calling confirm within a timeout ensures things are placed correctly onto the event queue. + setTimeout(function() { + if (confirm(confirmText) == false) { + return; + } else { + qmfui.Console.removeConsoleConnection(_index); + // If the QMF Console Connection being deleted is the currently connected one we stop the Console + // and establish a connection to the default QMF Console Connection. + if (currentlySelected) { + qmfui.Console.stopConsole(); + qmfui.Console.startConsole(0); // Start the default QMF Console Connection. + } + + iTablet.location.back(); // Navigate to the previous page. + } + }, 0); + }; + + var show = function() { + var location = iTablet.location; + var data = location.data; + if (data == null || location.hash != "#selected-qmf-console-connection") { + return; + } + + // Get the selected QMFConsoleConnection + _index = parseInt(data.index); // The parseInt is important! Without it the index lookup gives odd results.. + var qmfConsoleConnections = qmfui.Console.getConsoleConnectionList(); + var qmfConsoleConnection = qmfConsoleConnections[_index]; + _name = qmfConsoleConnection.name; + _url = qmfConsoleConnection.url; + var connectionOptions = qmfConsoleConnection.connectionOptions; + + var eventsDisabled = false; + if (qmfConsoleConnection.disableEvents != null) { + eventsDisabled = qmfConsoleConnection.disableEvents; + } + + // If the selected Console is the default one hide the delete button, otherwise show it. + if (_index == 0) { + $("#selected-qmf-console-connection .header a.delete").hide(); + } else { + $("#selected-qmf-console-connection .header a.delete").show(); + } + + // Populate the page header with the Console Connection name/url. + var urlText = (_url == null || _url == "") ? "" : " (" + _url + ")"; + var label = (_name == null || _name == "") ? urlText : _name + urlText; + $("#selected-qmf-console-connection .header h1").text(label); + + $("#selected-qmf-console-connection-url p").text(((_url == "") ? 'default' : _url)); + $("#selected-qmf-console-connection-name p").text(((_name == "") ? '""' : _name)); + $("#selected-qmf-console-connection-events-disabled p").text(eventsDisabled); + + if (_url == "") { + $("#selected-qmf-console-connection-default-info").show(); + } else { + $("#selected-qmf-console-connection-default-info").hide(); + } + + if (connectionOptions == "") { + $("#selected-qmf-console-connection-connection-options").hide(); + } else { + $("#selected-qmf-console-connection-connection-options textarea").val(util.stringify(connectionOptions)); + $("#selected-qmf-console-connection-connection-options").show(); + } + }; + + this.initialise = function() { + $("#selected-qmf-console-connection").unbind("show").bind("show", show); + + // Using END_EV avoids the 300ms delay responding to anchor click events that occurs on mobile browsers. + $("#selected-qmf-console-connection .header a.delete").bind(qmfui.END_EV, deleteHandler); + }; +}; // End of qmfui.SelectedQMFConsoleConnection definition + + +//------------------------------------------------------------------------------------------------------------------- +// Broker Information +//------------------------------------------------------------------------------------------------------------------- + +/** + * Create a Singleton instance of the Broker class managing the id="broker" page. + */ +qmfui.Broker = new function() { + /** + * Convert nanoseconds into hours, minutes and seconds. + */ + var convertTime = function(ns) { + var milliSecs = ns/1000000; + var msSecs = (1000); + var msMins = (msSecs * 60); + var msHours = (msMins * 60); + var numHours = Math.floor(milliSecs/msHours); + var numMins = Math.floor((milliSecs - (numHours * msHours)) / msMins); + var numSecs = Math.floor((milliSecs - (numHours * msHours) - (numMins * msMins))/ msSecs); + numSecs = numSecs < 10 ? numSecs = "0" + numSecs : numSecs; + numMins = numMins < 10 ? numMins = "0" + numMins : numMins; + + return (numHours + ":" + numMins + ":" + numSecs); + }; + + this.update = function() { + var broker = qmfui.Console.getBroker(); + broker.uptime = convertTime(broker.uptime); // Convert uptime to a more human readable value. + + qmfui.renderObject($("#broker-list"), broker, ["name", "version", "uptime", "port", "maxConns", "connBacklog", + "dataDir", "mgmtPublish", "mgmtPubInterval", "workerThreads", + /* 0.20 publishes many more stats so include them if available */ + "queueCount", "acquires", "releases", "abandoned", "abandonedViaAlt"]); + + // Render a number of 0.20 statistics in their own subsections to improve readability. + + // Render the Message Input Output Statistics. + if (broker.msgDepth == null) { + $("#broker-msgio-container").hide(); + } else { + $("#broker-msgio-container").show(); + qmfui.renderObject($("#broker-msgio"), broker, ["msgDepth", "msgTotalEnqueues", "msgTotalDequeues"]); + } + + // Render the Byte Input Output Statistics. + if (broker.byteDepth == null) { + $("#broker-byteio-container").hide(); + } else { + $("#broker-byteio-container").show(); + qmfui.renderObject($("#broker-byteio"), broker, ["byteDepth", "byteTotalEnqueues", "byteTotalDequeues"]); + } + + var hideDetails = $("#settings-hide-details")[0].checked; + + // Render the Flow-to-disk Statistics. + if (broker.msgFtdDepth == null || hideDetails) { + $("#broker-flow-to-disk-container").hide(); + } else { + $("#broker-flow-to-disk-container").show(); + qmfui.renderObject($("#broker-flow-to-disk"), broker, ["msgFtdDepth", "msgFtdEnqueues", "msgFtdDequeues", + "byteFtdDepth", "byteFtdEnqueues", "byteFtdDequeues"]); + } + + // Render the Dequeue Details. + if (broker.discardsTtl == null || hideDetails) { + $("#broker-dequeue-container").hide(); + } else { + $("#broker-dequeue-container").show(); + qmfui.renderObject($("#broker-dequeue"), broker, ["discardsTtl", "discardsRing", "discardsLvq", + "discardsOverflow", "discardsSubscriber", "discardsPurge", "reroutes"]); + } + }; + + /** + * This click handler is triggered by a click on the broker-log-level radio button. It invokes the QMF + * setLogLevel method to set the log level of the connected broker to debug or normal. + */ + var setLogLevel = function(e) { + var level = $(e.target).val(); + + // Set to the QMF2 levels for broker debug and normal. + level = (level == "debug") ? "debug+:Broker" : "notice+"; + + var broker = qmfui.Console.getBroker(true); // Retrieve broker QmfConsoleData object. + broker.invokeMethod("setLogLevel", {"level": level}, function(data) { + if (data.error_text) { + alert(data.error_text); + } + }); + }; + + this.initialise = function() { + // Explicitly using a click handler rather than change as it's possible that another instance has changed + // the log level so we may want to be able to reset it by clicking the currently selected level. + $("#broker-log-level li input").click(setLogLevel); + }; +}; // End of qmfui.Broker definition + + +//------------------------------------------------------------------------------------------------------------------- +// Connection Information +//------------------------------------------------------------------------------------------------------------------- + +/** + * Create a Singleton instance of the Connections class managing the id="connections" page. + */ +qmfui.Connections = new function() { + var _connectionMap = {}; // Connections indexed by ObjectId. + var _sessionMap = {}; // Sessions indexed by ObjectId. + var _subscriptionMap = {}; // Subscriptions indexed by ObjectId. + var _queueToSubscriptionAssociations = {}; // 0..* association between Queue and Subscription keyed by Queue ID. + + /** + * Return the Connection object indexed by QMF ObjectId. + */ + this.getConnection = function(oid) { + return _connectionMap[oid]; + }; + + /** + * Return the Session object indexed by QMF ObjectId. + */ + this.getSession = function(oid) { + return _sessionMap[oid]; + }; + + /** + * Return the Subscription object indexed by QMF ObjectId. + */ + this.getSubscription = function(oid) { + return _subscriptionMap[oid]; + }; + + /** + * Return the Subscription association List indexed by a queue's QMF ObjectId. + */ + this.getQueueSubscriptions = function(oid) { + var subs = _queueToSubscriptionAssociations[oid]; + return (subs == null) ? [] : subs; // If it's null set it to an empty array. + }; + + /** + * The Connections update method includes a number of subtle complexities due to the way QMF Management + * Objects are associated with each other. For example a Subscription is associated with a single + * Session however a Session may have zero or more Subscriptions, similarly a Session is associated with + * a single Connection but a Connection may have zero or more Sessions. + * + * The QMF Management Objects maintain the single unidirectional associations, which generally makes sense + * but an unfortunate side-effect of this is that if one wishes to obtain information about Sessions and + * Subscriptions related to a given Connection it's somewhat of a pain as one has to do a multi-pass dereference. + * + * This method does the dereferencing and creates a 0..* association to Session in each Connection object and + * a 0..* association to Subscription in each Session object so other pages can avoid their own dereferencing. + * N.B. these added associations are references to actual Session or Subscription objects and NOT via ObjectIds + * This is because these associations are not *really* QmfData object properties and only exist within the local + * memory space of this application, so using actual memory references avoids an additional dereference. + */ + this.update = function() { + _subscriptionMap = {}; // Clear _subscriptionMap. + _sessionMap = {}; // Clear _sessionMap. + _queueToSubscriptionAssociations = {}; // Clear _queueToSubscriptionAssociations. + + var subscriptions = qmfui.Console.getSubscriptions(); + for (var i in subscriptions) { + var subscription = subscriptions[i]; + var subscriptionId = subscription._object_id; + var sessionRef = subscription.sessionRef; + var queueRef = subscription.queueRef; + + // Create the Session->Subscriptions association array and store it keyed by the sessionRef, when we go + // to store the actual Session we can retrieve the association and store it as a property of the Session. + if (_sessionMap[sessionRef] == null) { + _sessionMap[sessionRef] = [subscription]; + } else { + _sessionMap[sessionRef].push(subscription); + } + + // Create the Queue->Subscriptions association array and store it keyed by the queueRef, when we go + // to store the actual Session we can retrieve the association and store it as a property of the Session. + if (_queueToSubscriptionAssociations[queueRef] == null) { + _queueToSubscriptionAssociations[queueRef] = [subscription]; + } else { + _queueToSubscriptionAssociations[queueRef].push(subscription); + } + + _subscriptionMap[subscriptionId] = subscription; // Index subscriptions by ObjectId. + } + + var connectionToSessionAssociations = {}; + var sessions = qmfui.Console.getSessions(); + for (var i in sessions) { + var session = sessions[i]; + var sessionId = session._object_id; + var connectionRef = session.connectionRef; + + var subs = _sessionMap[sessionId]; // Retrieve the association array and store it as a property. + subs = (subs == null) ? [] : subs; // If it's null set it to an empty array. + session._subscriptions = subs; + + // Create the Connection->Sessions association array and store it keyed by the connectionRef, when we go to + // store the actual Connection we can retrieve the association and store it as a property of the Connection. + if (connectionToSessionAssociations[connectionRef] == null) { + connectionToSessionAssociations[connectionRef] = [session]; + } else { + connectionToSessionAssociations[connectionRef].push(session); + } + _sessionMap[sessionId] = session; // Index sessions by ObjectId. + } + + // Temporary connections map, we move active connections to this so deleted connections wither and die. + var temp = {}; + var connections = qmfui.Console.getConnections(); + iTablet.renderList($("#connections-list"), function(i) { + var connection = connections[i]; + var connectionId = connection._object_id; + + // Look up the previous value for the indexed connection using its objectId. + var prev = _connectionMap[connectionId]; + var stats = (prev == null) ? new qmfui.Statistics(["msgsFromClient", "msgsToClient"]) : prev._statistics; + stats.put([connection.msgsFromClient, connection.msgsToClient, connection._update_ts]); + connection._statistics = stats; + + // Retrieve the association array and store it as a property. + var sessions = connectionToSessionAssociations[connectionId]; + sessions = (sessions == null) ? [] : sessions; // If it's null set it to an empty array. + connection._sessions = sessions; + temp[connectionId] = connection; + + // Calculate the total number of subscriptions for this connection, this is useful because a count of + // zero indicates that a connection is probably a producer only connection. + var connectionSubscriptions = 0; + for (var i in connection._sessions) { + var session = connection._sessions[i]; + connectionSubscriptions += session._subscriptions.length; + } + + var address = connection.address + " (" + connection.remoteProcessName + ")"; + if (connectionSubscriptions == 0) { + return "<li class='arrow'><a href='#selected-connection?id=" + connectionId + "'>" + address + + "<p>No Subscriptions</p></a></li>"; + } else { + return "<li class='arrow'><a href='#selected-connection?id=" + connectionId + "'>" + address + + "</a></li>"; + } + }, connections.length); + + // Replace the saved statistics with the newly populated temp instance, which only has active objects + // moved to it, this means that any deleted objects are no longer present. + _connectionMap = temp; + }; + +}; // End of qmfui.Connections definition + + +/** + * Create a Singleton instance of the SelectedConnection class managing the id="selected-connection" page. + */ +qmfui.SelectedConnection = new function() { + var _sessions = []; // Populate this with the ID of matching sessions enabling navigation to sessions. + + this.update = function() { + var location = iTablet.location; + var data = location.data; + if (data == null || location.hash != "#selected-connection") { + return; + } + + // Get the latest statistics update of the selected connection object. + var connectionId = data.id; + var connection = qmfui.Connections.getConnection(connectionId); + if (connection == null) { + $("#resource-deleted").show(); + } else { + $("#resource-deleted").hide(); + + var name = connection.address + " (" + connection.remoteProcessName + ")"; + $("#selected-connection .header h1").text(name); + + // Populate the back button with "Subscription" or "Connections" depending on context + var backText = data.fromSubscription ? "Subscrip..." : "Connect..."; + + // Using $("#selected-connection .header a").text(backText) wipes all child elements so use the following. + $("#selected-connection .header a")[0].firstChild.nodeValue = backText; + + // Render the connection message statistics to #selected-connection-msgio + qmfui.renderObject($("#selected-connection-msgio"), connection, ["msgsFromClient", "msgsToClient"], + "#graphs?connectionId=" + connectionId); + + // Render the connection byte statistics to #selected-connection-byteio + qmfui.renderObject($("#selected-connection-byteio"), connection, ["bytesFromClient", "bytesToClient"]); + + // Render the connection frame statistics to #selected-connection-frameio + qmfui.renderObject($("#selected-connection-frameio"), connection, ["framesFromClient", "framesToClient"]); + + // Render selected general connection properties to #selected-connection-general. + qmfui.renderObject($("#selected-connection-general"), connection, ["federationLink", "SystemConnection", + "incoming", "authIdentity", "userProxyAuth", "saslMechanism", "saslSsf", "remotePid", + "shadow", "closing", "protocol"]); + + // Render links to the sessions associated with this connection. + _sessions = connection._sessions; + var subscribedSessions = $("#selected-connection-subscribed-sessions"); + var unsubscribedSessions = $("#selected-connection-unsubscribed-sessions"); + if (_sessions.length == 0) { // Show a message if there are no sessions at all + subscribedSessions.hide(); + subscribedSessions.prev().hide(); + unsubscribedSessions.show(); + unsubscribedSessions.prev().show(); + iTablet.renderList(unsubscribedSessions, function(i) { + return "<li class='grey'><a href='#'>There are currently no sessions attached to " + + name + "</a></li>"; + }); + } else { + var subscribed = []; + var unsubscribed = []; + for (var i in _sessions) { + var session = _sessions[i]; + var id = session._object_id; + var subscriptionCount = session._subscriptions.length; + if (subscriptionCount == 0) { + unsubscribed.push("<li><a href='#'>" + session.name + "</a></li>"); + } else { + var plural = subscriptionCount > 1 ? " Subscriptions" : " Subscription"; + subscribed.push("<li class='multiline arrow'><a href='#connection-subscriptions?id=" + id + "'><div>" + + session.name + "<p class='sub'>" + subscriptionCount + plural + "</p></div></a></li>"); + } + } + + if (subscribed.length > 0) { + subscribedSessions.show(); + subscribedSessions.prev().show(); + qmfui.renderArray(subscribedSessions, subscribed); + } else { + subscribedSessions.hide(); + subscribedSessions.prev().hide(); + } + + if (unsubscribed.length > 0) { + unsubscribedSessions.show(); + unsubscribedSessions.prev().show(); + qmfui.renderArray(unsubscribedSessions, unsubscribed); + } else { + unsubscribedSessions.hide(); + unsubscribedSessions.prev().hide(); + } + } + } + }; + + this.initialise = function() { + $("#selected-connection").unbind("show").bind("show", qmfui.SelectedConnection.update); + }; +}; // End of qmfui.SelectedConnection definition + + +/** + * Create a Singleton instance of the ConnectionSubscriptions class managing the id="connection-subscriptions" page. + * This page is slightly different than most of the others as there can be multiple subscriptions and thus multiple + * arbitrary lists. Most pages have tried to reuse HTML list items for efficiency but in this page we clear and + * regenerate the contents of the page div each update as it's simpler than attempting to reuse elements. + */ +qmfui.ConnectionSubscriptions = new function() { + this.update = function() { + var location = iTablet.location; + var data = location.data; + if (data == null || location.hash != "#connection-subscriptions") { + return; + } + + var sessionId = data.id; + var session = qmfui.Connections.getSession(sessionId); + if (session == null) { + $("#resource-deleted").show(); + } else { + $("#resource-deleted").hide(); + + var hideQmfObjects = $("#settings-hide-qmf-objects")[0].checked; + + // Populate the page header with the session name. + $("#connection-subscriptions .header h1").text(session.name); + + var subscriptions = session._subscriptions; + var length = subscriptions.length; + + var page = $("#connection-subscriptions .page"); + page.children().remove(); // Clear the contents of the page div. + + for (var i = 0; i < length; i++) { + var subscription = subscriptions[i]; + var id = subscription.queueRef; + var queue = qmfui.Queues.getQueue(id); + + if (i == 0) { + page.append("<h1 class='first'>Subscription 1</h1>"); + } else { + page.append("<h1>Subscription " + (i + 1) + "</h1>"); + } + + var name = $("<ul id='connection-subscription-name" + i + "' class='list'></ul>"); + page.append(name); + + // Render the associated queue name to #connection-subscription-name. + var isQmfQueue = queue._isQmfQueue; + iTablet.renderList(name, function(i) { + // If the associated Queue is a QMF Queue and hideQmfObjects has been selected render the + // Queue name grey and make it non-navigable otherwise render normally. + if (isQmfQueue && hideQmfObjects) { + return "<li class='grey'><a href='#selected-queue?id=" + id + "&fromSubscriptions=true'>" + + queue.name + "</a></li>"; + } else { + return "<li class='arrow'><a href='#selected-queue?id=" + id + "&fromSubscriptions=true'>" + + queue.name + "</a></li>"; + } + }); + + page.append("<p/>"); + + var list = $("<ul id='connection-subscription" + i + "' class='list'></ul>"); + page.append(list); + + // Render the useful subscription properties to #connection-subscriptions-list. + qmfui.renderObject(list, subscription, + ["delivered", "browsing", "acknowledged", "exclusive", "creditMode"]); + } + + $("#connection-subscriptions").trigger("refresh"); // Make sure touch scroller is up-to-date. + } + }; + + this.initialise = function() { + $("#connection-subscriptions").unbind("show").bind("show", qmfui.ConnectionSubscriptions.update); + }; +}; // End of qmfui.ConnectionSubscriptions definition + + +//------------------------------------------------------------------------------------------------------------------- +// Exchange Information +//------------------------------------------------------------------------------------------------------------------- + +/** + * Create a Singleton instance of the Exchanges class managing the id="exchanges" page. + */ +qmfui.Exchanges = new function() { + var QMF_EXCHANGES = {"qmf.default.direct": true, "qmf.default.topic": true, "qpid.management": true}; + var _exchangeNameMap = {}; // Exchanges indexed by name. + var _exchangeMap = {}; // Exchanges indexed by ObjectId. + + /** + * Return the Exchange object for the given QMF ObjectId. The Exchange object contains the latest update + * of the given object and a RingBuffer containing previous values of key statistics over a 24 hour period. + */ + this.getExchange = function(oid) { + return _exchangeMap[oid]; + }; + + /** + * Return the Exchange object with the given name. The Exchange object contains the latest update + * of the given object and a RingBuffer containing previous values of key statistics over a 24 hour period. + */ + this.getExchangeByName = function(name) { + return _exchangeNameMap[name]; + }; + + this.update = function() { + var hideQmfObjects = $("#settings-hide-qmf-objects")[0].checked; + + // We move active exchanges to temp so deleted exchanges wither and die. We can't just do _exchangeMap = {} + // as we need to retrieve and update statistics from any active (non-deleted) exchange. + var temp = {}; + _exchangeNameMap = {}; // We can simply clear the map of exchanges indexed by name though. + var exchanges = qmfui.Console.getExchanges(); + iTablet.renderList($("#exchanges-list"), function(i) { + var exchange = exchanges[i]; + + // Look up the previous value for the indexed exchange using its objectId. + var prev = _exchangeMap[exchange._object_id]; + var stats = (prev == null) ? new qmfui.Statistics(["msgReceives", "msgRoutes", "msgDrops"]) : + prev._statistics; + + stats.put([exchange.msgReceives, exchange.msgRoutes, exchange.msgDrops, exchange._update_ts]); + exchange._statistics = stats; + + var id = exchange._object_id; + temp[id] = exchange; + + var name = exchange.name; + name = (name == "") ? "'' (default direct)" : name; + + _exchangeNameMap[name] = exchange; + + if (QMF_EXCHANGES[name] && hideQmfObjects) { + return false; // Filter out any QMF related exchanges if the settings filter is checked. + } else { + return "<li class='arrow'><a href='#selected-exchange?id=" + id + "'>" + name + + "<p>" + exchange.type + "</p></a></li>"; + } + }, exchanges.length); + + // Replace the saved statistics with the newly populated temp instance, which only has active objects + // moved to it, this means that any deleted objects are no longer present. + _exchangeMap = temp; + }; + +}; // End of qmfui.Exchanges definition + + +/** + * Create a Singleton instance of the AddExchange class managing the id="add-exchange" page. + */ +qmfui.AddExchange = new function() { + var submit = function() { + var properties = {}; + + if ($("#exchange-durable")[0].checked) { + properties["durable"] = true; + } else { + properties["durable"] = false; + } + + if ($("#sequence")[0].checked) { + properties["qpid.msg_sequence"] = 1; + } + + if ($("#ive")[0].checked) { + properties["qpid.ive"] = 1; + } + + properties["exchange-type"] = $("#exchange-type input[checked]").val(); + + var alternateExchangeName = $("#add-exchange-additional-alternate-exchange-name p").text(); + if (alternateExchangeName != "None (default)") { + alternateExchangeName = alternateExchangeName.split(" (")[0]; // Remove the exchange type from the text. + properties["alternate-exchange"] = alternateExchangeName; + } + + var exchangeName = $("#exchange-name"); + var name = exchangeName.val(); + if (name == "") { + exchangeName.addClass("error"); + } else { + exchangeName.removeClass("error"); + + var arguments = {"type": "exchange", "name": name, "properties": properties}; + var broker = qmfui.Console.getBroker(true); // Retrieve broker QmfConsoleData object. + broker.invokeMethod("create", arguments, function(data) { + if (data.error_text) { + alert(data.error_text); + } else { + iTablet.location.back(); + } + }); + } + }; + + var changeType = function(e) { + var jthis = $(e.target); + if (jthis.attr("checked")) { + $("#add-exchange-exchange-type p").text(jthis.siblings("label").text()); + } + }; + + this.initialise = function() { + // Using END_EV avoids the 300ms delay responding to anchor click events that occurs on mobile browsers. + $("#add-exchange .right.button").bind(qmfui.END_EV, submit); + $("#exchange-type input").change(changeType); + + // Always initialise to default value irrespective of browser caching. + $("#direct").click(); + }; +}; // End of qmfui.AddExchange definition + + +/** + * Create a Singleton instance of the ExchangeSelector class managing the id="exchange-selector" page. + */ +qmfui.ExchangeSelector = new function() { + // Protected Exchanges are exchanges that we don't permit binding to - default direct and the QMF exchanges. + var PROTECTED_EXCHANGES = {"": true, "''": true, "qmf.default.direct": true, + "qmf.default.topic": true, "qpid.management": true}; + var _id; + + /** + * This method renders dynamic content in the ExchangeSelector page. This is necessary because the set + * of exchanges to be rendered may change as exchanges are added or deleted. + */ + var show = function() { + var location = iTablet.location; + var data = location.data; + if (data == null || location.hash != "#exchange-selector") { + return; + } + + // We pass in the ID of the list ltem that contains the anchor with an href to exchange-selector. + _id = data.id; + + if (_id == "#add-binding-exchange-name") { + $("#exchange-selector .header a").text("Add Bin..."); + $("#exchange-selector .header h1").text("Select Exchange"); + } else if (_id == "#reroute-messages-exchange-name") { + $("#exchange-selector .header a").text("Reroute..."); + $("#exchange-selector .header h1").text("Select Exchange"); + } else { + $("#exchange-selector .header a").text("Additio..."); + $("#exchange-selector .header h1").text("Alternate Exchange"); + } + + var exchanges = qmfui.Console.getExchanges(); + var filteredExchanges = []; + var currentlySelected = $(_id + " p").text(); + + // Check the status of any Exchange that the user may have previously selected prior to hitting "Done". + if (currentlySelected != "None (default)") { + // Remove the exchange type from the text before testing. + currentlySelected = currentlySelected.split(" (")[0]; + if (qmfui.Exchanges.getExchangeByName(currentlySelected) == null) { // Check if it has been deleted. + alert('The currently selected Exchange "' + currentlySelected + '" appears to have been deleted.'); + currentlySelected = "None (default)"; + } + } + + var checked = (currentlySelected == "None (default)") ? "checked" : ""; + filteredExchanges.push("<li><label for='exchange-selector-exchangeNone'>None (default)</label><input type='radio' id='exchange-selector-exchangeNone' name='exchange-selector' value='None (default)' " + checked + "/></li>"); + + var length = exchanges.length; + for (var i = 0; i < length; i++) { + var name = exchanges[i].name; + var type = exchanges[i].type; + checked = (currentlySelected == name) ? "checked" : ""; + + // Filter out default direct and QMF exchanges as we don't want to allow binding to those. + if (!PROTECTED_EXCHANGES[name]) { + filteredExchanges.push("<li><label for='exchange-selector-exchange" + i + "'>" + name + " (" + type + ")</label><input type='radio' id='exchange-selector-exchange" + i + "' name='exchange-selector' value='" + name + "' " + checked + "/></li>"); + } + } + + qmfui.renderArray($("#exchange-selector-list"), filteredExchanges); + $("#exchange-selector-list input").change(changeExchange); + }; + + /** + * Event handler for the change event on "#exchange-selector-list input". Note that this is bound "dynamically" + * in the show handler because the exchange list is created dynamically each time the ExchangeSelector is shown. + */ + var changeExchange = function(e) { + var jthis = $(e.target); + if (jthis.attr("checked")) { + $(_id + " p").text(jthis.siblings("label").text()); + } + }; + + this.initialise = function() { + $("#exchange-selector").unbind("show").bind("show", show); + }; +}; // End of qmfui.ExchangeSelector definition + + +/** + * Create a Singleton instance of the SelectedExchange class managing the id="selected-exchange" page. + */ +qmfui.SelectedExchange = new function() { + // System Exchanges are exchanges that should not be deleted so we hide the delete button for those Exchanges. + var SYSTEM_EXCHANGES = {"''": true, "amq.direct": true, "amq.fanout": true, "amq.match": true, "amq.topic": true, + "qmf.default.direct": true, "qmf.default.topic": true, "qpid.management": true}; + + // Protected Exchanges are exchanges that we don't binding to - default direct and the QMF exchanges. + var PROTECTED_EXCHANGES = {"": true, "''": true, "qmf.default.direct": true, + "qmf.default.topic": true, "qpid.management": true}; + var _name = ""; + + /** + * This method deletes the selected exchange by invoking the QMF delete method. + */ + var deleteHandler = function() { + // Wrap in a timeout call because confirm doesn't play nicely with touchend and causes it to trigger twice. + // Calling confirm within a timeout ensures things are placed correctly onto the event queue. + setTimeout(function() { + if (confirm('Delete Exchange "' + _name + '"?') == false) { + return; + } else { + var arguments = {"type": "exchange", "name": _name}; + var broker = qmfui.Console.getBroker(true); // Retrieve broker QmfConsoleData object. + broker.invokeMethod("delete", arguments, function(data) { + if (data.error_text) { + alert(data.error_text); + } else { + iTablet.location.back(); + } + }); + } + }, 0); + }; + + this.update = function() { + var location = iTablet.location; + var data = location.data; + if (data == null || location.hash != "#selected-exchange") { + return; + } + + // Get the latest update of the selected exchange object. + var exchangeId = data.id; + var exchange = qmfui.Exchanges.getExchange(exchangeId); + if (exchange == null) { + $("#resource-deleted").show(); + } else { + $("#resource-deleted").hide(); + + // Populate the page header with the exchange name and type + _name = exchange.name; + _name = (_name == "") ? "''" : _name; + $("#selected-exchange .header h1").text(_name + " (" + exchange.type + ")"); + + // If the selected Exchange is a system Exchange hide the delete button, otherwise show it. + if (SYSTEM_EXCHANGES[_name]) { + $("#selected-exchange .header a.delete").hide(); + } else { + $("#selected-exchange .header a.delete").show(); + } + + // Populate the back button with "Exchanges" or "Bindings" depending on context + var backText = "Exchan..."; + if (data.bindingKey) { + backText = "Bindings"; + // Ensure that the binding that linked to this page gets correctly highlighted if we navigate back. + // If default direct add an extra "&" otherwise all ObjectIds will match in a simple string search. + qmfui.Bindings.setHighlightedBinding(exchangeId + (_name == "''" ? "&" : ""), + data.bindingKey); + } + + // Using $("#selected-exchange .header a").text(backText) wipes all child elements so use the following. + $("#selected-exchange .header a")[0].firstChild.nodeValue = backText; + + // Render the bindingCount to #selected-exchange-bindings + if (exchange.bindingCount == 0) { + // We don't allow bindings to be added to default direct or QMF Exchanges. + if (PROTECTED_EXCHANGES[_name]) { + iTablet.renderList($("#selected-exchange-bindings"), function(i) { + return "<li class='grey'><a href='#'>There are currently no bindings to " + _name + "</a></li>"; + }); + } else { + iTablet.renderList($("#selected-exchange-bindings"), function(i) { + return "<li class='pop'><a href='#add-binding?exchangeId=" + exchangeId + "'>Add Binding</a></li>"; + }); + } + } else { + iTablet.renderList($("#selected-exchange-bindings"), function(i) { + return "<li class='arrow'><a href='#bindings?exchangeId=" + exchangeId + "'>bindingCount<p>" + + exchange.bindingCount + "</p></a></li>"; + }); + } + + // Render the exchange statistics to #selected-exchange-msgio + qmfui.renderObject($("#selected-exchange-msgio"), exchange, ["msgReceives", "msgRoutes", "msgDrops"], + "#graphs?exchangeId=" + exchangeId); + + // Render the exchange statistics to #selected-exchange-byteio + qmfui.renderObject($("#selected-exchange-byteio"), exchange, ["byteReceives", "byteRoutes", "byteDrops"]); + + // Render selected general exchange properties and exchange.declare arguments to #selected-exchange-general. + keys = ["durable", "autoDelete", "producerCount"]; + var general = []; + + // Render any alternate exchange that may be attached to this exchange. Note that exchange.altExchange + // is a reference property so we need to dereference it before extracting the exchange name. + var altExchange = qmfui.Exchanges.getExchange(exchange.altExchange); + if (altExchange) { + general.push("<li><a href='#'>altExchange<p>" + altExchange.name + "</p></a></li>"); + } + + for (var i in keys) { // Populate with selected properties. + var key = keys[i]; + general.push("<li><a href='#'>" + key + "<p>" + exchange[key] + "</p></a></li>"); + } + for (var i in exchange.arguments) { // Populate with arguments. + general.push("<li><a href='#'>" + i + "<p>" + exchange.arguments[i] + "</p></a></li>"); + } + qmfui.renderArray($("#selected-exchange-general"), general); + } + }; + + this.initialise = function() { + $("#selected-exchange").unbind("show").bind("show", qmfui.SelectedExchange.update); + + // Using END_EV avoids the 300ms delay responding to anchor click events that occurs on mobile browsers. + $("#selected-exchange .header a.delete").bind(qmfui.END_EV, deleteHandler); + }; +}; // End of qmfui.SelectedExchange definition + + +//------------------------------------------------------------------------------------------------------------------- +// Queue Information +//------------------------------------------------------------------------------------------------------------------- + +/** + * Create a Singleton instance of the Queues class managing the id="queues" page. + */ +qmfui.Queues = new function() { + var QMF_EXCHANGES = { "qmf.default.direct": true, "qmf.default.topic": true, "qpid.management": true}; + var _queueNameMap = {}; // Queues indexed by name. + var _queueMap = {}; // Queues indexed by ObjectId. + + /** + * Return the Queue object for the given QMF ObjectId. The Queue object contains the latest update + * of the given object and a RingBuffer containing previous values of key statistics over a 24 hour period. + */ + this.getQueue = function(oid) { + return _queueMap[oid]; + }; + + /** + * Return the Queue object with the given name. The Queue object contains the latest update + * of the given object and a RingBuffer containing previous values of key statistics over a 24 hour period. + */ + this.getQueueByName = function(name) { + return _queueNameMap[name]; + }; + + this.update = function() { + var hideQmfObjects = $("#settings-hide-qmf-objects")[0].checked; + + // We move active queues to temp so deleted queues wither and die. We can't just do _queueMap = {} as we + // need to retrieve and update statistics from any active (non-deleted) queue. + var temp = {}; + _queueNameMap = {}; // We can simply clear the map of queues indexed by name though. + var queues = qmfui.Console.getQueues(); + iTablet.renderList($("#queues-list"), function(i) { + var queue = queues[i]; + var objectId = queue._object_id; + + // Look up the previous value for the indexed queue using its objectId. + var prev = _queueMap[objectId]; + var stats = (prev == null) ? new qmfui.Statistics(["msgDepth", "msgTotalEnqueues", "msgTotalDequeues"]) : + prev._statistics; + + stats.put([queue.msgDepth, queue.msgTotalEnqueues, queue.msgTotalDequeues, queue._update_ts]); + + // Add statistics as an additional property of the queue object. + queue._statistics = stats; + + /* + * Check if the queue is associated with a QMF exchange. Because we need to iterate through the + * bindings in an inner loop we only want to do this once for each queue, however we can't only + * do it when the queue is missing from the _queueMap, because there's a race condition with QMF + * properties getting asynchronously returned, so it's possible for a queue to exist without a + * binding object referencing it existing for a short period, so we don't add the _isQmfQueue + * property until a binding referencing the queue actually exists. + */ + if (prev == null || prev._isQmfQueue == null) { + var bindings = qmfui.Console.getBindings(); // Get the cached list of binding objects. + for (var i in bindings) { + var b = bindings[i]; + if (b.queueRef == objectId) { + var exchange = qmfui.Exchanges.getExchange(b.exchangeRef); // Dereference the exchangeRef. + if (exchange != null) { + // If a binding referencing the queue and an exchange exists add isQmfQueue state as an + // additional property of the queue object. + if (QMF_EXCHANGES[exchange.name]) { + queue._isQmfQueue = true; + break; + } else { + queue._isQmfQueue = false; + } + } + } + } + } else { + // Add previous status of _isQmfQueue as an additional property of the queue object. + queue._isQmfQueue = prev._isQmfQueue; + } + + temp[objectId] = queue; + _queueNameMap[queue.name] = queue; + + if (queue._isQmfQueue && hideQmfObjects) { + return false; // Filter out any QMF related queues if the settings filter is checked. + } else { + return "<li class='arrow'><a href='#selected-queue?id=" + objectId + "'>" + queue.name + "</a></li>"; + } + }, queues.length); + + // Replace the saved statistics with the newly populated temp instance, which only has active objects + // moved to it, this means that any deleted objects are no longer present. + _queueMap = temp; + }; + +}; // End of qmfui.Queues definition + + +/** + * Create a Singleton instance of the AddQueue class managing the id="add-queue" page. + */ +qmfui.AddQueue = new function() { + var _properties = {}; + + var parseIntegerProperty = function(selector, name) { + var value = $(selector).removeClass("error").val(); + if (value == "") { + return true; // "" doesn't populate the property, but it's still valid so return true; + } else { + if (value.search(/[kKmMgG]/) == (value.length - 1)) { // Does it end in K/M/G + _properties[name] = value; + return true; + } else { + var integer = parseInt(value); + if (isNaN(integer)) { + $(selector).addClass("error"); + return false; + } else { + _properties[name] = integer; + return true; + } + } + } + }; + + var submit = function() { + _properties = {}; + + if (!parseIntegerProperty("#max-queue-size", "qpid.max_size")) { + return; + } else if (!parseIntegerProperty("#max-queue-count", "qpid.max_count")) { + return; + } else if (!parseIntegerProperty("#flow-stop-size", "qpid.flow_stop_size")) { + return; + } else if (!parseIntegerProperty("#flow-stop-count", "qpid.flow_stop_count")) { + return; + } else if (!parseIntegerProperty("#flow-resume-size", "qpid.flow_resume_size")) { + return; + } else if (!parseIntegerProperty("#flow-resume-count", "qpid.flow_resume_count")) { + return; + } + + if ($("#queue-durable")[0].checked) { + _properties["durable"] = true; + if (!parseIntegerProperty("#file-size", "qpid.file_size")) { + return; + } else if (!parseIntegerProperty("#file-count", "qpid.file_count")) { + return; + } + } else { + _properties["durable"] = false; + } + + var limitPolicy = $("#limit-policy input[checked]").val(); + if (limitPolicy != "none") { + _properties["qpid.policy_type"] = limitPolicy; + } + + var orderingPolicy = $("#ordering-policy input[checked]").val(); + if (orderingPolicy == "lvq") { + _properties["qpid.last_value_queue"] = 1; + } else if (orderingPolicy == "lvq-no-browse") { + _properties["qpid.last_value_queue_no_browse"] = 1; + } + + if (!parseIntegerProperty("#generate-queue-events input[checked]", "qpid.queue_event_generation")) { + return; + } + + var alternateExchangeName = $("#add-queue-additional-alternate-exchange-name p").text(); + if (alternateExchangeName != "None (default)") { + alternateExchangeName = alternateExchangeName.split(" (")[0]; // Remove the exchange type from the text. + _properties["alternate-exchange"] = alternateExchangeName; + } + + var queueName = $("#queue-name"); + var name = queueName.val(); + if (name == "") { + queueName.addClass("error"); + } else { + queueName.removeClass("error"); + + var arguments = {"type": "queue", "name": name, "properties": _properties}; + var broker = qmfui.Console.getBroker(true); // Retrieve broker QmfConsoleData object. + broker.invokeMethod("create", arguments, function(data) { + if (data.error_text) { + alert(data.error_text); + } else { + iTablet.location.back(); + } + }); + } + }; + + var changeLimitPolicy = function(e) { + var jthis = $(e.target); + if (jthis.attr("checked")) { + $("#add-queue-limit-policy p").text(jthis.siblings("label").text()); + } + }; + + var changeOrderingPolicy = function(e) { + var jthis = $(e.target); + if (jthis.attr("checked")) { + var value = jthis.attr("value"); + if (value == "fifo") { + $("#add-queue-ordering-policy p").text("Fifo (default)"); + } else if (value == "lvq") { + $("#add-queue-ordering-policy p").text("LVQ"); + } else if (value == "lvq-no-browse") { + $("#add-queue-ordering-policy p").text("LVQ No Browse"); + } + } + }; + + var changeQueueEventGeneration = function(e) { + var jthis = $(e.target); + if (jthis.attr("checked")) { + $("#add-queue-generate-queue-events p").text(jthis.siblings("label").text()); + } + }; + + var changeDurable = function(e) { + var durable = $("#queue-durable")[0].checked; + var durableList = $("#add-queue-additional-durable-list"); + var hiddenList = $("#add-queue-additional-hidden-list").hide(); + + if (durable) { + setTimeout(function() { + iTablet.renderList(durableList); + setTimeout(function() { + $("#file-size").parent().appendTo(durableList); + iTablet.renderList(durableList); + setTimeout(function() { + $("#file-count").parent().appendTo(durableList); + iTablet.renderList(durableList); + $("#add-queue-additional").trigger("refresh"); // Refresh touch scroller. + }, 30); + }, 30); + }, 30); + + $("#add-queue-additional-journal-note").show(); + } else { + setTimeout(function() { + $("#file-count").parent().appendTo(hiddenList); + setTimeout(function() { + $("#file-size").parent().appendTo(hiddenList); + setTimeout(function() { + iTablet.renderList(durableList); + $("#add-queue-additional").trigger("refresh"); // Refresh touch scroller. + }, 30); + }, 30); + }, 30); + + $("#add-queue-additional-journal-note").hide(); + } + }; + + this.initialise = function() { + // Using END_EV avoids the 300ms delay responding to anchor click events that occurs on mobile browsers. + $("#add-queue .right.button").bind(qmfui.END_EV, submit); + $("#limit-policy input").change(changeLimitPolicy); + $("#ordering-policy input").change(changeOrderingPolicy); + $("#generate-queue-events input").change(changeQueueEventGeneration); + + // Always initialise to default value irrespective of browser caching. + $("#none").click(); + $("#fifo").click(); + $("#generate-no-events").click(); + + changeDurable(); + $("#queue-durable").change(changeDurable); + }; +}; // End of qmfui.AddQueue definition + + +/** + * Create a Singleton instance of the SelectedQueue class managing the id="selected-queue" page. + */ +qmfui.SelectedQueue = new function() { + var _name = ""; + + // The Queue depth and number of consumers are reported during the Queue delete confirmation process. + var _depth = 0; + var _consumers = 0; + + /** + * This method deletes the selected queue by invoking the QMF delete method. + */ + var deleteHandler = function() { + var plural = (_consumers == 1) ? " consumer?" : " consumers?" + + // Wrap in a timeout call because confirm doesn't play nicely with touchend and causes it to trigger twice. + // Calling confirm within a timeout ensures things are placed correctly onto the event queue. + setTimeout(function() { + if (confirm('Delete Queue "' + _name + + '"\nwhich contains ' + _depth + " messages and has " + _consumers + plural) == false) { + return; + } else { + var arguments = {"type": "queue", "name": _name}; + var broker = qmfui.Console.getBroker(true); // Retrieve broker QmfConsoleData object. + broker.invokeMethod("delete", arguments, function(data) { + if (data.error_text) { + alert(data.error_text); + } else { + iTablet.location.back(); + } + }); + } + }, 0); + }; + + this.update = function() { + var location = iTablet.location; + var data = location.data; + if (data == null || location.hash != "#selected-queue") { + return; + } + + // Get the latest update of the selected queue object. + var queueId = data.id; + var queue = qmfui.Queues.getQueue(queueId); + if (queue == null) { + $("#resource-deleted").show(); + } else { + $("#resource-deleted").hide(); + + _name = queue.name; + _depth = queue.msgDepth; // Reported to user if Queue delete is selected. + _consumers = queue.consumerCount; // Reported to user if Queue delete is selected. + + // Populate the page header with the queue name. + $("#selected-queue .header h1").text(_name); + + // If the selected Queue is a QMF Queue hide the delete button, otherwise show it. + // Deleting QMF Queues is a bad thing as it will stop any QMF Consoles behaving as the should. + if (queue._isQmfQueue) { + $("#selected-queue .header a.delete").hide(); + $("#selected-queue-admin-wrapper").hide(); + } else { + $("#selected-queue .header a.delete").show(); + $("#selected-queue-admin-wrapper").show(); + } + + // Populate the back button with "Queues", "Bindings" or "Subscriptions" depending on context + var backText = "Queues"; + if (data.bindingKey) { + backText = "Bindings"; + // Ensure that the binding that linked to this page gets correctly highlighted if we navigate back. + qmfui.Bindings.setHighlightedBinding(queueId, data.bindingKey); + } else if (data.fromSubscriptions) { + backText = "Subscrip..."; + } + + // Using $("#selected-queue .header a").text(backText) wipes all child elements so use the following. + $("#selected-queue .header a")[0].firstChild.nodeValue = backText; + + // Render the bindingCount to #selected-queue-bindings + // There should always be at least one binding to a queue as the default direct exchange is always bound. + iTablet.renderList($("#selected-queue-bindings"), function(i) { + return "<li class='arrow'><a href='#bindings?queueId=" + queueId + "'>bindingCount<p>" + + queue.bindingCount + "</p></a></li>"; + }); + + // Render the queue statistics to #selected-queue-msgio + qmfui.renderObject($("#selected-queue-msgio"), queue, ["msgDepth", "msgTotalEnqueues", "msgTotalDequeues"], + "#graphs?queueId=" + queueId); + + // Render the queue statistics to #selected-queue-byteio + qmfui.renderObject($("#selected-queue-byteio"), queue, ["byteDepth", "byteTotalEnqueues", + "byteTotalDequeues"]); + + // Render selected general queue properties and queue.declare arguments to #selected-queue-general. + keys = ["durable", "autoDelete", "exclusive", "unackedMessages", "acquires", "releases", + "messageLatency", "messageLatencyAvg", "consumerCount", "flowStopped", "flowStoppedCount"]; + var general = []; + + // Render any alternate exchange that may be attached to this queue. Note that queue.altExchange + // is a reference property so we need to dereference it before extracting the exchange name. + var altExchange = qmfui.Exchanges.getExchange(queue.altExchange); + if (altExchange) { + general.push("<li><a href='#'>altExchange<p>" + altExchange.name + "</p></a></li>"); + } + + for (var i in keys) { // Populate with selected properties. + var key = keys[i]; + var value = queue[key]; + if (value != null) { + general.push("<li><a href='#'>" + key + "<p>" + value + "</p></a></li>"); + } + } + for (var i in queue.arguments) { // Populate with arguments. + general.push("<li><a href='#'>" + i + "<p>" + queue.arguments[i] + "</p></a></li>"); + } + qmfui.renderArray($("#selected-queue-general"), general); + + var hideDetails = $("#settings-hide-details")[0].checked; + + // Render the Flow-to-disk Statistics. + if (queue.msgFtdDepth == null || hideDetails) { + $("#selected-queue-flow-to-disk-container").hide(); + } else { + $("#selected-queue-flow-to-disk-container").show(); + qmfui.renderObject($("#selected-queue-flow-to-disk"), queue, ["msgFtdDepth", "msgFtdEnqueues", + "msgFtdDequeues", "byteFtdDepth", "byteFtdEnqueues", "byteFtdDequeues"]); + } + + // Render the Dequeue Details. + if (queue.discardsTtl == null || hideDetails) { + $("#selected-queue-dequeue-container").hide(); + } else { + $("#selected-queue-dequeue-container").show(); + qmfui.renderObject($("#selected-queue-dequeue"), queue, ["discardsTtl", "discardsRing", "discardsLvq", + "discardsOverflow", "discardsSubscriber", "discardsPurge", "reroutes"]); + } + + // Render links to the subscriptions associated with this queue to #selected-queue-subscriptions. + // Unfortunately the subscription name isn't especially useful so find the associated connection + // and display the connection address instead. + var subscriptions = qmfui.Connections.getQueueSubscriptions(queueId); + if (subscriptions.length == 0) { + iTablet.renderList($("#selected-queue-subscriptions"), function(i) { + return "<li class='grey'><a href='#'>There are currently no subscriptions to " + _name + "</a></li>"; + }); + } else { + iTablet.renderList($("#selected-queue-subscriptions"), function(i) { + var subscription = subscriptions[i]; + var id = subscription._object_id; + + // The subscription.sessionRef really should be present, but the Java Broker does not yet correctly + // populate the association between Subscription and Session so we need this defensive block. + if (subscription.sessionRef != null) { + var session = qmfui.Connections.getSession(subscription.sessionRef); + var connection = qmfui.Connections.getConnection(session.connectionRef); + var address = connection.address + " (" + connection.remoteProcessName + ")"; + return "<li class='arrow'><a href='#queue-subscriptions?id=" + id + "'>" + address + "</a></li>"; + } else { + return "<li class='arrow'><a href='#queue-subscriptions?id=" + id + "'>" + subscription.name + "</a></li>"; + } + }, subscriptions.length); + } + + // We have to dynamically render the admin list so that we can attach the queueId to the URL. + var admin = []; + admin.push("<li class='arrow pop'><a href='#purge-queue?queueId=" + queueId + "'>Purge</a></li>"); + admin.push("<li class='arrow pop'><a href='#reroute-messages?queueId=" + queueId + "'>Reroute Messages</a></li>"); + admin.push("<li class='arrow pop'><a href='#move-messages?queueId=" + queueId + "'>Move Messages</a></li>"); + qmfui.renderArray($("#selected-queue-admin"), admin); + } + }; + + this.initialise = function() { + $("#selected-queue").unbind("show").bind("show", qmfui.SelectedQueue.update); + + // Using END_EV avoids the 300ms delay responding to anchor click events that occurs on mobile browsers. + $("#selected-queue .header a.delete").bind(qmfui.END_EV, deleteHandler); + }; +}; // End of qmfui.SelectedQueue definition + + +/** + * Create a Singleton instance of the QueueSubscriptions class managing the id="queue-subscriptions" page. + */ +qmfui.QueueSubscriptions = new function() { + this.update = function() { + var location = iTablet.location; + var data = location.data; + if (data == null || location.hash != "#queue-subscriptions") { + return; + } + + var subscriptionId = data.id; + var subscription = qmfui.Connections.getSubscription(subscriptionId); + if (subscription == null) { + $("#resource-deleted").show(); + } else { + $("#resource-deleted").hide(); + + var session = qmfui.Connections.getSession(subscription.sessionRef); + session = session ? session : {}; + var connection = qmfui.Connections.getConnection(session.connectionRef); + connection = connection ? connection : {}; + + // The connection.address should be present but for the 0.20 Java Broker it is not yet populated + // so we need to do some defensive code to check if it's set and if not render subscription.name. + var name = connection.address ? connection.address + " (" + connection.remoteProcessName + ")" : + subscription.name; + + var connectionId = connection._object_id; + + // Populate the page header with the address of the connection associated with the subscription. + $("#queue-subscriptions .header h1").text(name); + + iTablet.renderList($("#queue-subscriptions-connection"), function(i) { + if (connectionId) { + return "<li class='arrow'><a href='#selected-connection?id=" + connectionId + + "&fromSubscription=true'>" + name + "</a></li>"; + } else { + return "<li><a href='#'>Connection is Unknown</a></li>"; + } + }); + + // subscription.sessionRef should be present but for the 0.20 Java Broker it is not yet populated + // so we need to do some defensive code to check if it's set and if not render "Session is Unknown". + if (subscription.sessionRef) { + // Render the useful session properties to #queue-subscriptions-session + qmfui.renderObject($("#queue-subscriptions-session"), session, ["name", "framesOutstanding", + "unackedMessages", "channelId", "maxClientRate", "clientCredit"]); + } else { + iTablet.renderList($("#queue-subscriptions-session"), function(i) { + return "<li><a href='#'>Session is Unknown</a></li>"; + }); + } + + // Render the useful subscription properties to #queue-subscriptions-subscription + qmfui.renderObject($("#queue-subscriptions-subscription"), subscription, ["name", "delivered", "browsing", + "acknowledged", "exclusive", "creditMode"]); + } + }; + + this.initialise = function() { + $("#queue-subscriptions").unbind("show").bind("show", qmfui.QueueSubscriptions.update); + }; +}; // End of qmfui.QueueSubscriptions definition + + +//------------------------------------------------------------------------------------------------------------------- +// Queue Admin +//------------------------------------------------------------------------------------------------------------------- + +/** + * Create a Singleton instance of the PurgeQueue class managing the id="purge-queue" page. + */ +qmfui.PurgeQueue = new function() { + /** + * Actually purge the messages using the QMF purge method on the Queue Management Object. + */ + var submit = function() { + var location = iTablet.location; + var data = location.data; + if (data == null || location.hash != "#purge-queue") { + return; + } + + var selector = $("#purge-queue-request-number"); + var value = selector.val(); + var messageCount = 0; + if (value != "") { + messageCount = parseInt(value); + if (isNaN(messageCount)) { + selector.addClass("error"); + return false; + } + } + + selector.removeClass("error"); + + var queueId = data.queueId; + var queue = qmfui.Queues.getQueue(queueId); + qmfui.Console.makeConsoleData(queue); // Make queue a QmfConsoleData object with an invokeMethod method. + + // Wrap in a timeout call because confirm doesn't play nicely with touchend and causes it to trigger twice. + // Calling confirm within a timeout ensures things are placed correctly onto the event queue. + var countText = (messageCount == 0) ? "all" : messageCount; + setTimeout(function() { + if (confirm('Purge ' + countText + ' messages from "' + queue.name + '"?') == false) { + return; + } else { + var arguments = {"request": messageCount}; + + queue.invokeMethod("purge", arguments, function(data) { + if (data.error_text) { + alert(data.error_text); + } else { + iTablet.location.back(); + } + }); + } + }, 0); + }; + + this.initialise = function() { + // Using END_EV avoids the 300ms delay responding to anchor click events that occurs on mobile browsers. + $("#purge-queue .right.button").bind(qmfui.END_EV, submit); + }; +}; // End of qmfui.PurgeQueue definition + + +/** + * Create a Singleton instance of the RerouteMessages class managing the id="reroute-messages" page. + */ +qmfui.RerouteMessages = new function() { + /** + * Actually reroute the messages using the QMF reroute method on the Queue Management Object. + */ + var submit = function() { + var location = iTablet.location; + var data = location.data; + if (data == null || location.hash != "#reroute-messages") { + return; + } + + var selector = $("#reroute-messages-request-number"); + var value = selector.val(); + var messageCount = 0; + if (value != "") { + messageCount = parseInt(value); + if (isNaN(messageCount)) { + selector.addClass("error"); + return false; + } + } + + selector.removeClass("error"); + + var queueId = data.queueId; + var queue = qmfui.Queues.getQueue(queueId); + qmfui.Console.makeConsoleData(queue); // Make queue a QmfConsoleData object with an invokeMethod method. + + var useAltExchange = $("#reroute-messages-use-alternate-exchange")[0].checked; + var countText = (messageCount == 0) ? "all" : messageCount; + + var exchangeName = $("#reroute-messages-exchange-name p").text(); + if (exchangeName != "None (default)") { + exchangeName = exchangeName.split(" (")[0]; // Remove the exchange type from the text. + } + + var exchangeText = useAltExchange ? "Alternate Exchange?" : exchangeName + "?"; + // Wrap in a timeout call because confirm doesn't play nicely with touchend and causes it to trigger twice. + // Calling confirm within a timeout ensures things are placed correctly onto the event queue. + setTimeout(function() { + if (confirm('Reroute ' + countText + ' messages from "' + queue.name + '" to ' + exchangeText) == false) { + return; + } else { + var arguments = {"request": messageCount, "useAltExchange": useAltExchange}; + if (!useAltExchange) { + arguments["exchange"] = exchangeName; + } + + queue.invokeMethod("reroute", arguments, function(data) { + if (data.error_text) { + alert(data.error_text); + } else { + iTablet.location.back(); + } + }); + } + }, 0); + }; + + /** + * This method is the change handler for the use alternate exchange switch, it is used to show or hide the + * exchange selector widget. This handler is also bound to "show" because the state of the alternate exchange + * switch is cached in some browsers so triggering on change alone wouldn't handle that. + */ + var changeUseAlternateExchange = function() { + if ($("#reroute-messages-use-alternate-exchange")[0].checked) { + $("#reroute-messages-use-selected-exchange").hide(); + } else { + $("#reroute-messages-use-selected-exchange").show(); + } + }; + + this.initialise = function() { + // Using END_EV avoids the 300ms delay responding to anchor click events that occurs on mobile browsers. + $("#reroute-messages .right.button").bind(qmfui.END_EV, submit); + $("#reroute-messages-use-alternate-exchange").change(changeUseAlternateExchange); + $("#reroute-messages").unbind("show").bind("show", changeUseAlternateExchange); + }; +}; // End of qmfui.RerouteMessages definition + + +/** + * Create a Singleton instance of the RerouteMessages class managing the id="move-messages" page. + */ +qmfui.MoveMessages = new function() { + var _sourceQueueName = ""; + var _destinationQueueName = ""; + var _sourceQueue = {}; + + /** + * Actually reroute the messages using the QMF reroute method on the Queue Management Object. + */ + var submit = function() { + // The queueMoveMessages method returns an InvalidParameter Exception if called when srcQueue has msgDepth == 0 + // This is pretty confusing as the parameters *are* actually OK. https://issues.apache.org/jira/browse/QPID-4543 + // has been raised on this but the following is some defensive logic to provide a more helpful warning. + if (_sourceQueue.msgDepth == 0) { + setTimeout(function() { + alert("Can't call Move Messages on a queue with a msgDepth of zero"); + return false; + }, 0); + } else { + var selector = $("#move-messages-request-number"); + var value = selector.val(); + var messageCount = 0; + if (value != "") { + messageCount = parseInt(value); + if (isNaN(messageCount)) { + selector.addClass("error"); + return false; + } + } + + selector.removeClass("error"); + + // Wrap in a timeout call because confirm doesn't play nicely with touchend and causes it to trigger twice. + // Calling confirm within a timeout ensures things are placed correctly onto the event queue. + var countText = (messageCount == 0) ? "all" : messageCount; + setTimeout(function() { + if (confirm('Move ' + countText + ' messages from "' + _sourceQueueName + '" to "' + _destinationQueueName + '"?') == false) { + return; + } else { + var arguments = {"srcQueue": _sourceQueueName, "destQueue": _destinationQueueName, "qty": messageCount}; + var broker = qmfui.Console.getBroker(true); // Retrieve broker QmfConsoleData object. + broker.invokeMethod("queueMoveMessages", arguments, function(data) { + if (data.error_text) { + alert(data.error_text); + } else { + iTablet.location.back(); + } + }); + } + }, 0); + } + }; + + /** + * This method renders the main move-messages page when it is made visible by the show event being triggered. + */ + var show = function() { + var location = iTablet.location; + var data = location.data; + if (data == null || location.hash != "#move-messages") { + return; + } + + var queueId = data.queueId; + _sourceQueue = qmfui.Queues.getQueue(queueId); + _sourceQueueName = (_sourceQueue == null) ? "" : _sourceQueue.name; + _destinationQueueName = $("#move-messages-queue-name p").text(); + + if (_sourceQueueName == _destinationQueueName) { + $("#move-messages-queue-name p").text("None (default)"); + } else { + $("#move-messages-queue-name p").text(_destinationQueueName); + } + }; + + this.getSourceQueueName = function() { + return _sourceQueueName; + }; + + this.initialise = function() { + $("#move-messages").unbind("show").bind("show", show); + + // Using END_EV avoids the 300ms delay responding to anchor click events that occurs on mobile browsers. + $("#move-messages .right.button").bind(qmfui.END_EV, submit); + }; +}; // End of qmfui.MoveMessages definition + + +//------------------------------------------------------------------------------------------------------------------- +// Generic Bindings Rendering Page +//------------------------------------------------------------------------------------------------------------------- + +/** + * Create a Singleton instance of the Bindings class managing the id="bindings" page. + */ +qmfui.Bindings = new function() { + // Protected Exchanges are exchanges that we don't permit unbinding from - default direct and the QMF exchanges. + var PROTECTED_EXCHANGES = {"": true, "''": true, "qmf.default.direct": true, + "qmf.default.topic": true, "qpid.management": true}; + var _highlightedObject = null; + var _highlightedObjectKey = null; + + /** + * This method is used to render the binding information for headers and XML exchanges which need a little + * bit more effort than just rendering the binding key. We use <p class="title"> and <p class="sub"> + * to give fairly neat formatting. + * + * @param exchange the exchange that the binding is bound to. + * @param binding the binding that we wish to render. + */ + var render = function(exchange, binding) { + if (exchange.type == "headers") { + // Arguments *should* be returned, but set to empty object if not to protect subsequent code. + var arguments = binding.arguments ? binding.arguments : {"x-match": "any"}; + var headers = "<p class='title'>x-match: " + arguments["x-match"] + "</p>"; + for (var key in arguments) { + if (key != "x-match") { + headers = headers + "<p class='sub'>" + key + ": " + arguments[key] + "</p>"; + } + } + return headers; + } else if (exchange.type == "xml") { + var arguments = binding.arguments; + var xquery = "<p class='title'>xquery:</p>"; + xquery = xquery + "<p class='sub'>" + arguments["xquery"] + "</p>"; + return xquery; + } else { + return " "; + } + }; + + /** + * This method confirms the unbind request and invokes the QMF delete binding method on the broker. + * + * @param exchangeName the exchange that we wish to unbind from. + * @param queueName the queue that we wish to unbind from. + * @param bindingKey the binding key that we wish to unbind from. + */ + var unbind = function(exchangeName, queueName, bindingKey) { + var bindingIdentifier = exchangeName + "/" + queueName; + if (bindingKey != "") { + bindingIdentifier = bindingIdentifier + "/" + bindingKey; + } + + if (confirm('Delete Binding "' + bindingIdentifier + '"?') == false) { + return; + } else { + var arguments = {"type": "binding", "name": bindingIdentifier}; + var broker = qmfui.Console.getBroker(true); // Retrieve broker QmfConsoleData object. + broker.invokeMethod("delete", arguments, function(data) { + if (data.error_text) { + alert(data.error_text); + } + }); + } + }; + + /** + * This handler is triggered by clicking a <li class="clickable-icon">. Unfortunately there's bit more work + * to do because we only want to respond if the actual icon has been clicked so we need to work out the + * mouse or tap position within the li and check it's less than the icon width. + * Once we're happy that we've clicked the icon we need to work out which binding the <li> relates to. The + * approach to this is a little messy and involves scraping some of the html associated with the <li> + */ + var clickHandler = function(e) { + var target = e.target; + var jthis = $(target).closest("ul li.clickable-icon"); + + if (jthis.length != 0) { + var ICON_WIDTH = 45; // The width of the icon image plus some padding. + var offset = Math.ceil(jthis.offset().left); + var x = (e.pageX != null) ? e.pageX - offset : // Mouse position. + (e.originalEvent != null) ? e.originalEvent.targetTouches[0].pageX - offset : 0; // Touch pos. + + if (x < ICON_WIDTH) { + var bindingKey = jthis.text().split("]")[0]; + bindingKey = bindingKey.split("[")[1]; + var href = jthis.children("a:first").attr("href"); + href = href.replace(window.location, ""); + + if (href.indexOf("#selected-exchange") == 0) { + var queue = $("#bindings .header h1").text().split(" bindings")[0]; + var exchange = jthis.find("p:last").text(); + unbind(exchange, queue, bindingKey); + + } else { + var queue = jthis.find("p:last").text(); + var exchange = $("#bindings .header h1").text().split(" bindings")[0]; + unbind(exchange, queue, bindingKey); + } + } + } + }; + + this.update = function() { + var location = iTablet.location; + var data = location.data; + if (data == null || location.hash != "#bindings") { + return; + } + + // Get the latest update of the selected object. + var queueId = data.queueId; + var exchangeId = data.exchangeId; + var object = queueId ? qmfui.Queues.getQueue(queueId) : qmfui.Exchanges.getExchange(exchangeId); + + if (object == null) { + $("#resource-deleted").show(); + } else { + $("#resource-deleted").hide(); + + var name = object.name; + name = (name == "") ? "''" : name; + + // Populate the page header with the object name. + $("#bindings .header h1").text(name + " bindings"); + + // Populate the back button with "Queues" or "Bindings" depending on context + var backText = "Exchange"; + + if (queueId) { + backText = "Queue"; + $("#bindings").addClass("queue"); + } else { + $("#bindings").removeClass("queue"); + } + + // Using $("#bindings .header a").text(backText) wipes all child elements so use the following. + $("#bindings .header a")[0].firstChild.nodeValue = backText; + + // Ensure the correct item on the previous page is set active so it gets highlighted when we slide back. + if (queueId) { + $("#selected-queue-bindings").children("li").addClass("active"); + $("#selected-exchange-bindings").children("li").removeClass("active"); + + // Add queueId to add-binding URL so add-binding page can automatically populate the queue name. + iTablet.renderList($("#bindings-add-binding"), function(i) { + return "<li class='pop'><a href='#add-binding?queueId=" + queueId + "'>Add Binding</a></li>"; + }); + } else { + $("#selected-queue-bindings").children("li").removeClass("active"); + $("#selected-exchange-bindings").children("li").addClass("active"); + + // Add exchangeId to add-binding URL so add-binding page can automatically populate the exchange name. + iTablet.renderList($("#bindings-add-binding"), function(i) { + return "<li class='pop'><a href='#add-binding?exchangeId=" + exchangeId + "'>Add Binding</a></li>"; + }); + } + + // Note that we don't allow bindings to be added to the default direct exchange, QMF exchanges or + // queues that are bound to QMF exchanges as doing so may have undesired consequenses. + if (PROTECTED_EXCHANGES[name] || object._isQmfQueue) { + $("#bindings-add-binding").hide(); + $("#bindings .page h1").addClass("first"); + } else { + $("#bindings-add-binding").show(); + $("#bindings .page h1").removeClass("first"); + } + + // Render selected queue's bindings to #queue-details-bindings. + var bindings = qmfui.Console.getBindings(); + var binding = []; + for (var i in bindings) { + var b = bindings[i]; + if (b.queueRef == queueId) { + var exchange = qmfui.Exchanges.getExchange(b.exchangeRef); // Dereference exchangeRef + if (exchange != null) { + var ename = exchange.name; + if (ename == "") { + // Note that we don't allow bindings to be deleted from default direct exchange. + binding.push("<li class='arrow'><a href='#selected-exchange?id=" + + b.exchangeRef + "&bindingKey=" + b.bindingKey + "'>" + + "bind [" + b.bindingKey + "] => <p>''</p></a></li>"); + } else { + var text = render(exchange, b); + + // Note that we don't allow bindings to be deleted from QMF exchanges. + if (PROTECTED_EXCHANGES[ename]) { + binding.push("<li class='multiline arrow'><a href='#selected-exchange?id=" + + b.exchangeRef + "&bindingKey=" + b.bindingKey + "'>" + + "<div>bind [" + b.bindingKey + "] =></div>" + + "<div>" + text + "<p>" + ename + "</p></div></a></li>"); + } else { + binding.push("<li class='multiline arrow clickable-icon'><a class='delete' href='#selected-exchange?id=" + + b.exchangeRef + "&bindingKey=" + b.bindingKey + "'>" + + "<div>bind [" + b.bindingKey + "] =></div>" + + "<div>" + text + "<p>" + ename + "</p></div></a></li>"); + } + } + } + } else if (b.exchangeRef == exchangeId) { + var queue = qmfui.Queues.getQueue(b.queueRef); // Dereference queueRef + if (queue != null) { + var qname = queue.name; + var ename = object.name; // object is actually exchange in this case. + var text = render(object, b); + + // Note that we don't allow bindings to be deleted from default direct or QMF exchanges. + if (PROTECTED_EXCHANGES[ename]) { + binding.push("<li class='multiline arrow'><a href='#selected-queue?id=" + + b.queueRef + "&bindingKey=" + b.bindingKey + "'>" + + "<div>bind [" + b.bindingKey + "] =></div>" + + "<div>" + text + "<p class='fullwidth'>" + qname + "</p></div></a></li>"); + + } else { + binding.push("<li class='multiline arrow clickable-icon'><a class='delete' href='#selected-queue?id=" + + b.queueRef + "&bindingKey=" + b.bindingKey + "'>" + + "<div>bind [" + b.bindingKey + "] =></div>" + + "<div>" + text + "<p class='fullwidth'>" + qname + "</p></div></a></li>"); + } + } + } + } + + if (binding.length == 0) { + iTablet.renderList($("#bindings-list"), function(i) { + return "<li class='grey'><a href='#'>There are currently no bindings to " + name + "</a></li>"; + }); + } else { + qmfui.renderArray($("#bindings-list"), binding); + + // If _highlightedObject has been set by qmfui.SelectedQueue or qmfui.SelectedExchange search for + // the list item that would have caused navigation to that page and set it active, which causes + // it to be given a highlight that fades as we navigate back to qmfui.Bindings. + if (_highlightedObject) { + $("#bindings-list li").each(function() { + var li = $(this); + // Find the HTML that contains the highlightedObject string and highlightedObjectKey string. + var html = li.html(); + if (html.search(_highlightedObject) != -1 && html.search(_highlightedObjectKey) != -1) { + li.addClass("active"); + } + }); + + _highlightedObject = null; + _highlightedObjectKey = null; + } + } + } + }; + + /** + * This method is called by qmfui.SelectedQueue or qmfui.SelectedExchange to allow qmfui.Bindings to set the + * correct list item highlighting to allow it to fade out as we navigate back to qmfui.Bindings. This is necessary + * because we maintain a singleton qmfui.Bindings page and as such its state will get modified as we navigate + * through multiple queue->binding->exchange->binding etc. Setting the name of the most recent page navigated + * to in this method allows the fading to be handled correctly. + */ + this.setHighlightedBinding = function(name, bindingKey) { + _highlightedObject = name; + _highlightedObjectKey = "bindingKey=" + bindingKey; // Binding keys are always rendered within square braces. + }; + + this.initialise = function() { + $("#bindings").unbind("show").bind("show", qmfui.Bindings.update); + + // Using a click handler here doesn't seem to have the 300ms delay that occurs when using click handlers + // attached to anchors (hence why those use END_EV). It's *probably* because this is a delegating handler. + // JavaScript alert() can interfere with touchend so it's generally better to use click() if it's possible + // to do so without it causing that irritating delay!! + $("#bindings-list").click(clickHandler); + }; +}; // End of qmfui.Bindings definition + + +/** + * Create a Singleton instance of the AddBinding class managing the id="add-binding" page. + */ +qmfui.AddBinding = new function() { + var _queueName = ""; + var _exchangeName = ""; + var _exchangeType = ""; + var _properties = {}; + + /** + * Actually add the new binding using the QMF create method. + */ + var submit = function() { + if (_exchangeType == "headers") { + _properties["x-match"] = $("#x-match input[checked]").val(); + } else if (_exchangeType == "xml") { + _properties = {}; + var textarea = $("#add-xml-binding textarea"); + var xquery = textarea.val(); + if (xquery == "") { + textarea.addClass("error"); + return; + } else { + textarea.removeClass("error"); + _properties["xquery"] = xquery; + } + } else { + _properties = {}; + } + + var bindingKey = $("#add-binding-key-name").val(); + var bindingIdentifier = _exchangeName + "/" + _queueName; + if (bindingKey != "") { + bindingIdentifier = bindingIdentifier + "/" + bindingKey; + } + + // Before we actually add the new binding check to see if a binding with specified bindingIdentifier currently + // exists. It's a slight faff, but the Qpid broker doesn't currently detect this condition. + var duplicateKey = false; + var bindings = qmfui.Console.getBindings(); + for (var i in bindings) { + var b = bindings[i]; + // If a binding with the chosen key is found check if the queue and exchange names match. + if (b.bindingKey == bindingKey) { + var exchange = qmfui.Exchanges.getExchange(b.exchangeRef); // Dereference the exchangeRef. + if (exchange != null && exchange.name == _exchangeName) { + var queue = qmfui.Queues.getQueue(b.queueRef); // Dereference the queueRef. + if (queue != null && queue.name == _queueName) { + duplicateKey = true; + break; + } + } + } + } + + if (duplicateKey) { + alert('A binding with the identifier "' + bindingIdentifier + '" aleady exists.'); + } else { + var arguments = {"type": "binding", "name": bindingIdentifier, "properties": _properties}; + var broker = qmfui.Console.getBroker(true); // Retrieve broker QmfConsoleData object. + broker.invokeMethod("create", arguments, function(data) { + if (data.error_text) { + alert(data.error_text); + } else { + iTablet.location.back(); + } + }); + } + + // Delete the x-match and xquery properties. Note that the other properties are retained, this is + // deliberate because it's quite often the case that one may wish to add several headers bindings + // that may only differ slightly, it's pretty quick to delete match values that aren't needed. + delete _properties["x-match"]; + delete _properties["xquery"]; + }; + + /** + * This method renders the main add-binding page when it is made visible by the show event being triggered. + * It tries to be fairly clever by populating either the queue name or exchange name depending on where + * add-binding was navigated from. If a queueId or exchangeId isn't available navigation is added to the + * queue-selector or exchange-selector page by adding the arrow class to add-binding-queue-name or + * add-binding-exchange-name. This method then checks the exchange type and provides additional rendering + * necessary for XML or Headers exchanges. + */ + var show = function() { + var location = iTablet.location; + var data = location.data; + if (data == null || location.hash != "#add-binding") { + return; + } + + var queueId = data.queueId; + var exchangeId = data.exchangeId; + + if (queueId) { + var queue = qmfui.Queues.getQueue(queueId); + _queueName = (queue == null) ? "" : queue.name; + _exchangeName = $("#add-binding-exchange-name p").text(); + if (_exchangeName != "None (default)") { + _exchangeName = _exchangeName.split(" (")[0]; // Remove the exchange type from the text. + } + + var exchange = qmfui.Exchanges.getExchangeByName(_exchangeName); + _exchangeType = (exchange == null) ? "" : exchange.type; + + $("#add-binding-queue-name").removeClass("arrow"); + $("#add-binding-exchange-name").addClass("arrow"); + } else { + _queueName = $("#add-binding-queue-name p").text(); + var exchange = qmfui.Exchanges.getExchange(exchangeId); + _exchangeName = (exchange == null) ? "" : exchange.name; + _exchangeType = (exchange == null) ? "" : exchange.type; + + $("#add-binding-queue-name").addClass("arrow"); + $("#add-binding-exchange-name").removeClass("arrow"); + } + + var typeText = (_exchangeType == "") ? "" : " (" + _exchangeType + ")"; + + $("#add-binding-queue-name p").text(_queueName); + $("#add-binding-exchange-name p").text(_exchangeName + typeText); + + if (_exchangeType == "headers") { + // Render the properties and navigation needed to populate Headers bindings. These are dynamically + // populated and updated as Headers key/value properties get added or deleted. + $("#add-binding div.page h1").show().text("Headers"); + $("#add-headers-binding").show(); + $("#add-xml-binding").hide(); + + var list = []; + list.push("<li class='arrow'><a href='#x-match'>Match<p>" + $("#x-match input[checked]").val() + "</p></a></li>"); + + for (var i in _properties) { + var key = i; + var value = _properties[i]; + // Note we add key/value to the query part of the URL too to let showHeaderMatch() populate the values. + list.push("<li class='arrow clickable-icon'><a class='delete' href='#add-header-match?key=" + key + "&value=" + value + "'>" + key + "<p>" + value + "</p></a></li>"); + } + + list.push("<li class='arrow'><a href='#add-header-match'>Add...</a></li>"); + qmfui.renderArray($("#add-headers-binding"), list); + } else if (_exchangeType == "xml") { + $("#add-binding div.page h1").show().text("XML"); + $("#add-xml-binding").show(); + $("#add-headers-binding").hide(); + } else { + $("#add-binding div.page h1").hide(); + $("#add-headers-binding").hide(); + $("#add-xml-binding").hide(); + } + }; + + /** + * This method renders the add-header-match page when its show event is triggered. This page + * needs a show handler because it needs to be dynamically updated with any previously selected key/value pairs. + */ + var showHeaderMatch = function() { + var location = iTablet.location; + var data = location.data; + var key = ""; + var value = ""; + + if (data != null && location.hash == "#add-header-match") { + key = data.key; + value = data.value; + } + + $("#header-match-key").val(key); + $("#header-match-value").val(value); + }; + + /** + * Adds a a header match value from the binding being populated. + * This is triggered by the "Done" button on the add-header-match page. There's some validation logic + * in place to ensure that a key and value are both supplied. + */ + var addHeaderMatch = function() { + var key = $("#header-match-key"); + var keyVal = key.val(); + var value = $("#header-match-value"); + var valueVal = value.val(); + + if (keyVal == "") { + key.addClass("error"); + } else { + key.removeClass("error"); + } + + if (valueVal == "") { + value.addClass("error"); + } else { + value.removeClass("error"); + } + + if (keyVal != "" && valueVal != "") { + _properties[keyVal] = valueVal; + iTablet.location.back(); + } + }; + + /** + * Removes a header match value from the binding being populated. + * This handler is triggered by clicking a <li class="clickable-icon">. Unfortunately there's bit more work + * to do because we only want to respond if the actual icon has been clicked so we need to work out the + * mouse or tap position within the li and check it's less than the icon width. + * Once we're happy that we've clicked the icon we need to work out which binding the <li> relates to. The + * approach to this is a little messy and involves scraping some of the html associated with the <li> + */ + var removeHeaderMatch = function(e) { + var target = e.target; + var jthis = $(target).closest("ul li.clickable-icon"); + + if (jthis.length != 0) { + var ICON_WIDTH = 45; // The width of the icon image plus some padding. + var offset = Math.ceil(jthis.offset().left); + var x = (e.pageX != null) ? e.pageX - offset : // Mouse position. + (e.originalEvent != null) ? e.originalEvent.targetTouches[0].pageX - offset : 0; // Touch pos. + + if (x < ICON_WIDTH) { + var key = jthis.children("a:first")[0].firstChild.nodeValue; + delete _properties[key]; + $("#add-binding").trigger("show"); + } + } + }; + + this.initialise = function() { + $("#add-binding").unbind("show").bind("show", show); + $("#add-header-match").unbind("show").bind("show", showHeaderMatch); + + // Using END_EV avoids the 300ms delay responding to anchor click events that occurs on mobile browsers. + $("#add-binding .right.button").bind(qmfui.END_EV, submit); + $("#add-header-match .right.button").bind(qmfui.END_EV, addHeaderMatch); + + // Always initialise to default value irrespective of browser caching. + $("#x-match-all").click(); + + // Using a click handler here doesn't seem to have the 300ms delay that occurs when using click handlers + // attached to anchors (hence why those use END_EV). It's *probably* because this is a delegating handler. + // JavaScript alert() can interfere with touchend so it's generally better to use click() if it's possible + // to do so without it causing that irritating delay!! + $("#add-headers-binding").click(removeHeaderMatch); + }; +}; // End of qmfui.AddBinding definition + + +/** + * Create a Singleton instance of the QueueSelector class managing the id="queue-selector" page. + */ +qmfui.QueueSelector = new function() { + var _id; + + /** + * This method renders dynamic content in the ExchangeSelector page. This is necessary because the set + * of exchanges to be rendered may change as exchanges are added or deleted. + */ + var show = function() { + var location = iTablet.location; + var data = location.data; + if (data == null || location.hash != "#queue-selector") { + return; + } + + // We pass in the ID of the list ltem that contains the anchor with an href to exchange-selector. + _id = data.id; + + var sourceQueueName = ""; + if (_id == "#add-binding-queue-name") { + $("#queue-selector .header a").text("Add Bin..."); + } else if (_id == "#move-messages-queue-name") { + $("#queue-selector .header a").text("Move Me..."); + sourceQueueName = qmfui.MoveMessages.getSourceQueueName(); // Use this to filter out that queue name. + } + + var queues = qmfui.Console.getQueues(); + var filteredQueues = []; + var currentlySelected = $(_id + " p").text(); + + // Check the status of any Queue that the user may have previously selected prior to hitting "Done". + if (currentlySelected != "None (default)") { + if (qmfui.Queues.getQueueByName(currentlySelected) == null) { // Check if it has been deleted. + alert('The currently selected Queue "' + currentlySelected + '" appears to have been deleted.'); + currentlySelected = "None (default)"; + } + } + + var checked = (currentlySelected == "None (default)") ? "checked" : ""; + filteredQueues.push("<li><label for='queue-selector-queueNone'>None (default)</label><input type='radio' id='queue-selector-queueNone' name='queue-selector' value='None (default)' " + checked + "/></li>"); + + var length = queues.length; + for (var i = 0; i < length; i++) { + var name = queues[i].name; + // We do getQueueByName(name) because the _isQmfQueue property is a "fake" property added by the + // qmfui.Queues class, it's only stored in QMF objects held in the getQueueByName() and getQueue() caches. + var isQmfQueue = qmfui.Queues.getQueueByName(name)._isQmfQueue; + checked = (currentlySelected == name) ? "checked" : ""; + + // Filter out queues bound to QMF exchanges as we don't want to allow additional binding to those. + if (!isQmfQueue && (name != sourceQueueName)) { + filteredQueues.push("<li><label for='queue-selector-queue" + i + "'>" + name + "</label><input type='radio' id='queue-selector-queue" + i + "' name='queue-selector' value='" + name + "' " + checked + "/></li>"); + } + } + + qmfui.renderArray($("#queue-selector-list"), filteredQueues); + $("#queue-selector-list input").change(changeQueue); + }; + + /** + * Event handler for the change event on "#queue-selector-list input". Note that this is bound "dynamically" + * in the show handler because the queue list is created dynamically each time the QueueSelector is shown. + */ + var changeQueue = function(e) { + var jthis = $(e.target); + if (jthis.attr("checked")) { + $(_id + " p").text(jthis.siblings("label").text()); + } + }; + + this.initialise = function() { + $("#queue-selector").unbind("show").bind("show", show); + }; +}; // End of qmfui.QueueSelector definition. + + +//------------------------------------------------------------------------------------------------------------------- +// Generic Graph Rendering Page +//------------------------------------------------------------------------------------------------------------------- + +/** + * Create a Singleton instance of the Graphs class managing the id="graphs" page. + */ +qmfui.Graphs = new function() { + var IS_IE = (navigator.appName == "Microsoft Internet Explorer"); + var IE_VERSION = IS_IE ? /MSIE (\d+)/.exec(navigator.userAgent)[1] : -1; + + var SECONDS_AS_NANOS = 1000000000; // One second represented in nanoseconds. + var MILLIS_AS_NANOS = 1000000; // One millisecond represented in nanoseconds. + var HEIGHT = 300; // Canvas height (including radiused borders). + var BORDER = 10; + + var _ctx = null; + + var _radius = new Image(); + _radius.src = "/itablet/images/ie/radius-10px-sprite.png"; + + /** + * qmfui is pretty much browser neutral as browser quirks have been taken care of by jQuery and the + * iTablet framework but canvas support is an edge case. Wrapping a canvas in a <li> doesn't seem to + * work (<li> is where most of the fake border radius stuff is done), so for canvas we simply use the + * canvas rendering itself and use drawImage() to render radius-10px-sprite.png. We use IE_VERSION + * from iTablet to detect the IE version as we only draw borders for IE 7 & 8 as IE9 has fake border-radius + * support and for IE6 radiused borders haven't been done at all because life is too short...... + */ + var drawBorderRadius = function(context) { + if (IE_VERSION == 8 || IE_VERSION == 7) { + var width = context.canvas.width; + var height = context.canvas.height; + + // Draw the border lines. + context.beginPath(); + // Drawing mid point of a pixel e.g. starting at 0.5 is important for getting one pixel lines. + context.rect(0.5, 0.5, width - 2, height - 2); + context.strokeStyle = "black"; + context.stroke(); // Draw new path + + // Render the radiused borders from the radius-10px-sprite.png sprite using canvas drawImage(). + context.drawImage(_radius, 0, 0, 10, 10, 0, 0, 10, 10); + context.drawImage(_radius, 0, 10, 10, 10, 0, height - 10, 10, 10); + context.drawImage(_radius, 10, 0, 10, 10, width - 10, 0, 10, 10); + context.drawImage(_radius, 10, 10, 10, 10, width - 10, height - 10, 10, 10); + } + }; + + + /** + * Aaaargh. Another IE edge case!! + * IE7 has very quirky behaviour - using $("#graphs-time-selector").innerWidth() is unreliable, it + * fails when page is initially shown and calling it also seems to cause the page to take a long + * time to re-render on resize. Using the width of body doesn't have that issue but getting the + * css left value of graphs is unreliable too!!! so I've just coded the values of LEFT & PAGE_WIDTH. + */ + var getWidth = function() { + if (IE_VERSION == 7) { + var LEFT = 251; // .main css left, + var PAGE_WIDTH = 0.9; // .page 100% - padding left + right + var width = $("body").outerWidth(); + width = Math.floor((width - LEFT) * PAGE_WIDTH - 0.5); + return width; + } else { + return ($("#graphs-time-selector").innerWidth() - 2); // The -2 compensates for the border width. + } + }; + + this.update = function() { + var location = iTablet.location; + var data = location.data; + if (data == null || location.hash != "#graphs") { + return; + } + + // Get the latest update of the selected object and populate the text of the back button. + var object = null; + var backText = "Unknown"; + + if (data.queueId) { + object = qmfui.Queues.getQueue(data.queueId); + backText = "Queue"; + $("#graphs").removeClass("exchange connection"); + } else if (data.exchangeId) { + object = qmfui.Exchanges.getExchange(data.exchangeId); + backText = "Exchange"; + $("#graphs").addClass("exchange").removeClass("connection"); + } else if (data.connectionId) { + object = qmfui.Connections.getConnection(data.connectionId); + backText = "Connect..."; + $("#graphs").addClass("connection").removeClass("exchange"); + } + + if (object == null) { + $("#resource-deleted").show(); + } else { + $("#resource-deleted").hide(); + + var property = data.property; // An index to the particular property to be displayed. + var statistics = object._statistics; + + if (statistics == null) { + return; + } + + var description = statistics.description[property]; // Lookup the description of the property. + var isRate = (description == "msgDepth") ? false : true; // Is the statistic a rate. + + // Populate the page header with the property being graphed. + var header = (object.name == null) ? object.address + " " + description : object.name + " " + description; + $("#graphs .header h1").text(header); + + // Using $("#graphs .header a").text(backText) wipes all child elements so use the following. + $("#graphs .header a")[0].firstChild.nodeValue = backText; + + // Populate the graph title with the property being graphed. + if (isRate) { + description += " " + statistics.getRate(property).toFixed(2) + "/sec" + } else if (!statistics.short.isEmpty()) { + description += " " + statistics.short.getLast()[property]; + } + $("#graphs .page h1").text(description); + + var width = getWidth(); + if (_ctx) { + if (width != _ctx.canvas.width) { + _ctx.canvas.width = width; + } + + _ctx.clearRect(0, 0, width, HEIGHT); // Clear previous image. + drawBorderRadius(_ctx); // Draw fake border radius on old versions of IE. + + _ctx.beginPath(); // Start drawing path for grid. + for (var i = BORDER + 0.5; i < width; i += ((width - (BORDER*2))/10)) { // Draw vertical lines + _ctx.moveTo(i, BORDER); + _ctx.lineTo(i, HEIGHT - BORDER); + } + + for (var i = BORDER + 0.5; i < HEIGHT; i += ((HEIGHT - (BORDER*2))/10)) { // Draw horizontal lines + _ctx.moveTo(BORDER, i); + _ctx.lineTo(width - BORDER, i); + } + _ctx.strokeStyle = "#dddddd"; + _ctx.stroke(); // Draw grid. + } + + var stats = statistics.short; // 10 mins + var period = 600*SECONDS_AS_NANOS; // 600 seconds (ten minutes) in nanoseconds. + var interval = $("#graphs-time-selector input:radio[checked]").val(); + if (interval == "oneHour") { + stats = statistics.medium; // 1 hr + period = 3600*SECONDS_AS_NANOS; // 3600 seconds (one hour) in nanoseconds. + + } else if (interval == "oneDay") { + stats = statistics.long; // 1 day + period = 24*3600*SECONDS_AS_NANOS; // 24*3600 seconds (one day) in nanoseconds. + } + + var size = stats.size(); + var isMinimumSize = isRate ? size > 1 : size > 0; + + if (isMinimumSize && _ctx) { + /* + * For reasons of memory efficiency statistics are stored in ring buffers and attached to certain + * management objects (queue, exchange & connection) we only record statistics for certain properties + * of these management objects. For efficiency each item stored in the ring buffer is an array + * containing the statistics for each stored property with the last array item being the update + * timestamp of the management object. Thus the timestamp for each statistic sample is the last + * item (length - 1) of the array. Getting the length of any item held in the stats ring buffer + * gives us the index to use to lookup the timestamp, we use getLast() for convenience get(0) would + * be fine too. The isMinimumSize test ensures we have at least one item in the stats ring buffer. + */ + var TIMESTAMP = stats.getLast().length - 1; + + var curtime = (+new Date() * MILLIS_AS_NANOS); // Current time represented in nanoseconds. + var minimumTimestamp = curtime - period; // Items with timestamps less than this aren't in this period. + var limit = isRate ? size - 1 : size; // Maximum number of sample points. + var graph = [limit]; // Create an array to hold the data we'll put into the graph plot. + var count = 0; // This will be the actual number of sample points that fall within this period. + var i = 0; // Index into statistics circular buffer. + + // Skip over statistics older than the graph period. Mostly this won't happen, but for things like + // iOS where the browser may sleep/hibernate there may be a considerable gap between samples. + while ((i < limit) && (stats.get(i)[TIMESTAMP] < minimumTimestamp)) i++; + + var maxValue = 0; + for (;i < limit; i++, count++) { // i starts with the minimum index, count starts at zero. + var sample = stats.get(i); // Get each statistic sample from the circular buffer. + var value = sample[property]; // Get the sample value. + var timestamp = sample[TIMESTAMP]; // Get the sample timestamp. + + if (isRate) { + var sample1 = stats.get(i + 1); // Get the next sample so we can calculate the rate. + var value1 = sample1[property]; // Get the next sample's value. + var timestamp1 = sample1[TIMESTAMP];// Get the next sample's timestamp. + value = ((value1 - value)*SECONDS_AS_NANOS)/(timestamp1 - timestamp); // Rate in items/s + } + + var x = width + (timestamp - curtime)*(width/period); + graph[count] = {x: x, y: value}; + + if (value > maxValue) { + maxValue = value; + } + } + + var heightNormaliser = (HEIGHT - (2*BORDER))/maxValue; + + _ctx.beginPath(); // Start drawing path for main graph. + var sample = graph[0]; + _ctx.moveTo(sample.x, (maxValue - sample.y)*heightNormaliser + BORDER); + + for (i = 1; i < count; i++) { + sample = graph[i]; + _ctx.lineTo(sample.x, (maxValue - sample.y)*heightNormaliser + BORDER); + } + _ctx.strokeStyle = "black"; + _ctx.stroke(); // Draw main graph. + + // Draw grid text + _ctx.font = "15px Helvetica, Arial, 'Liberation Sans', FreeSans, sans-serif"; + _ctx.fillStyle = "red"; + _ctx.textBaseline = "middle"; + _ctx.textAlign = "right"; + _ctx.fillText(maxValue.toFixed(2), width - BORDER, BORDER); + _ctx.fillText((maxValue/2).toFixed(2), width - BORDER, HEIGHT/2); + _ctx.fillText("0.00", width - BORDER, HEIGHT - BORDER); + } + } + }; + + this.initialise = function() { + $(document).bind("orientationchange", qmfui.Graphs.update); + $(window).resize(qmfui.Graphs.update); + $("#graphs").unbind("show").bind("show", qmfui.Graphs.update); + $("#graphs-time-selector input").change(qmfui.Graphs.update); + + var canvas = $("#graphs-canvas")[0]; + if (canvas != null) { + // The following "shouldn't" be necessary as the graphs-canvas is part of the static markup and it + // works in the author's IE8 XP and Win7 test VMs without it, however getContext("2d") doesn't seem + // to be working on some IE8 deployments so it's added experimentally to see if it fixes the problem. + if (IS_IE && IE_VERSION == 8 && typeof(G_vmlCanvasManager) != "undefined") { + G_vmlCanvasManager.initElement(canvas); + } + + canvas.height = HEIGHT; + if (canvas.getContext) { // Is canvas supported? + _ctx = canvas.getContext("2d"); + } + } + }; +}; // End of qmfui.Graphs definition + + +//------------------------------------------------------------------------------------------------------------------- + + +/** + * Create a Singleton instance of the Links class managing the id="links" page. + * TODO Add link/bridge features. + */ +qmfui.Links = new function() { + + this.update = function() { + + }; + + this.initialise = function() { + }; +}; // End of qmfui.Links definition + + +//------------------------------------------------------------------------------------------------------------------- + +/** + * Create a Singleton instance of the RouteTopology class managing the id="route-topology" page. + * TODO Add link/bridge features. The idea of the route topology page is to "discover" federated brokers linked + * to a "seed" broker. Not sure if this is actually possible, but it'd be pretty cool. + */ +qmfui.RouteTopology = new function() { + + this.initialise = function() { + }; +}; // End of qmfui.RouteTopology definition + + +//------------------------------------------------------------------------------------------------------------------- + +/** + * Create a Singleton instance of the Events class managing the id="events" page. + */ +qmfui.Events = new function() { + var _events = new util.RingBuffer(20); // Store the Event history in a circular buffer. + + this.getEvent = function(index) { + return _events.get(index); + }; + + this.update = function(workItem) { + if (workItem._type == "EVENT_RECEIVED") { + var params = workItem._params; + var agent = params.agent; + + if (agent._product == "qpidd") { // Only log events from the broker ManagementAgent + _events.put(params.event); + iTablet.renderList($("#events-list"), function(i) { + var event = _events.get(i); + var name = event._schema_id._class_name; + var timestamp = new Date(event._timestamp/1000000).toLocaleString(); + var text = name; + + return "<li class='multiline arrow'><a href='#selected-event?index=" + i + "'>" + i + " " + text + + "<p class='sub'>" + timestamp + "</p></a></li>"; + }, _events.size()); + } + } + }; +}; // End of qmfui.Events definition + +/** + * Create a Singleton instance of the SelectedEvent class managing the id="selected-event" page. + */ +qmfui.SelectedEvent = new function() { + var _severities = ["emerg", "alert", "crit", "err", "warning", "notice", "info", "debug"]; + + this.update = function() { + var location = iTablet.location; + var data = location.data; + if (data == null || location.hash != "#selected-event") { + return; + } + + // Get the latest update of the selected event object. + var index = parseInt(data.index); // The parseInt is important! Without it the index lookup gives odd results.. + var event = qmfui.Events.getEvent(index); + + if (event == null) { + $("#resource-deleted").show(); + } else { + $("#resource-deleted").hide(); + + // Populate the page header with the event name. + var name = event._schema_id._package_name + ":" + event._schema_id._class_name; + $("#selected-event .header h1").text(name); + + var general = []; + var timestamp = new Date(event._timestamp/1000000).toLocaleString(); + general.push("<li><a href='#'>timestamp<p>" + timestamp + "</p></a></li>"); + general.push("<li><a href='#'>severity<p>" + _severities[event._severity] + "</p></a></li>"); + qmfui.renderArray($("#selected-event-list"), general); + + var values = []; + for (var i in event._values) { // Populate with event _values properties. + var value = event._values[i]; + if (i == "args" || i == "properties") { // If there are any args try and display them. + value = util.stringify(value); + } + values.push("<li><a href='#'>" + i + "<p>" + value + "</p></a></li>"); + } + qmfui.renderArray($("#selected-event-values"), values); + } + }; + + this.initialise = function() { + $("#selected-event").unbind("show").bind("show", qmfui.SelectedEvent.update); + }; +}; // End of qmfui.SelectedEvent definition + |