diff options
| author | Alex Rudyy <orudyy@apache.org> | 2013-03-20 17:07:58 +0000 |
|---|---|---|
| committer | Alex Rudyy <orudyy@apache.org> | 2013-03-20 17:07:58 +0000 |
| commit | 5e8965d59c1fe8cf80ce34862ddad3d3a861f83a (patch) | |
| tree | c353f3f53110a420690fb837f0d4fbe6be5e69c7 /java/broker-plugins | |
| parent | 39d132c6ecf8bc5f0e6e0e70bf7706d0b63c7995 (diff) | |
| download | qpid-python-5e8965d59c1fe8cf80ce34862ddad3d3a861f83a.tar.gz | |
QPID-4661: Add UI into java broker web management console to edit broker attributes
git-svn-id: https://svn.apache.org/repos/asf/qpid/trunk/qpid@1458956 13f79535-47bb-0310-9956-ffa450edef68
Diffstat (limited to 'java/broker-plugins')
4 files changed, 577 insertions, 26 deletions
diff --git a/java/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/rest/RestServlet.java b/java/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/rest/RestServlet.java index 8b74eb1dce..4f6f122876 100644 --- a/java/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/rest/RestServlet.java +++ b/java/broker-plugins/management-http/src/main/java/org/apache/qpid/server/management/plugin/servlet/rest/RestServlet.java @@ -341,6 +341,26 @@ public class RestServlet extends AbstractServlet } } + if (names.isEmpty()) + { + if (_hierarchy.length == 0) + { + try + { + doUpdate(getBroker(), providedObject); + response.setStatus(HttpServletResponse.SC_OK); + } + catch(RuntimeException e) + { + setResponseStatus(response, e); + } + return; + } + else + { + throw new ServletException("Cannot identify request target object"); + } + } providedObject.put("name", names.get(names.size()-1)); diff --git a/java/broker-plugins/management-http/src/main/java/resources/js/qpid/common/util.js b/java/broker-plugins/management-http/src/main/java/resources/js/qpid/common/util.js index b20169c94d..6f3049ec0d 100644 --- a/java/broker-plugins/management-http/src/main/java/resources/js/qpid/common/util.js +++ b/java/broker-plugins/management-http/src/main/java/resources/js/qpid/common/util.js @@ -18,8 +18,22 @@ * under the License. * */ -define(["dojo/_base/xhr"], - function (xhr) { + +define(["dojo/_base/xhr", + "dojo/_base/event", + "dojo/_base/json", + "dojo/_base/lang", + "dijit/Dialog", + "dijit/form/Form", + "dijit/form/Button", + "dijit/form/RadioButton", + "dijit/form/CheckBox", + "dojox/layout/TableContainer", + "dojox/validate/us", + "dojox/validate/web", + "dojo/domReady!" + ], + function (xhr, event, json, lang) { var util = {}; if (Array.isArray) { util.isArray = function (object) { @@ -122,5 +136,112 @@ define(["dojo/_base/xhr"], return (type === "PlainPasswordFile" || type === "Base64MD5PasswordFile"); }; + util.showSetAttributesDialog = function(attributeWidgetFactories, data, putURL, dialogTitle) + { + var layout = new dojox.layout.TableContainer({ + cols: 1, + "labelWidth": "300", + showLabels: true, + orientation: "horiz", + customClass: "formLabel" + }); + var submitButton = new dijit.form.Button({label: "Submit", type: "submit"}); + var form = new dijit.form.Form(); + form.domNode.appendChild(layout.domNode); + form.domNode.appendChild(submitButton.domNode); + var widgets = {}; + var requiredFor ={}; + for(var i in attributeWidgetFactories) + { + var attributeWidgetFactory = attributeWidgetFactories[i]; + var widget = attributeWidgetFactory.createWidget(data); + var name = attributeWidgetFactory.name ? attributeWidgetFactory.name : widget.name; + widgets[name] = widget; + widget.initialValue = widget.value; + layout.addChild(widget); + if (attributeWidgetFactory.hasOwnProperty("requiredFor")) + { + requiredFor[attributeWidgetFactory.requiredFor] = widget; + } + } + + // add onchange handler to set required property for dependent widget + for(var widgetName in requiredFor) + { + var dependent = requiredFor[widgetName]; + var widget = widgets[widgetName]; + if (widget.value) + { + dependent.set("required", true); + } + if (widget) + { + widget.dependent = dependent; + widget.on("change", function(newValue){ + this.dependent.set("required", newValue != ""); + }); + } + } + var setAttributesDialog = new dijit.Dialog({ + title: dialogTitle, + content: form, + style: "width: 600px" + }); + form.on("submit", function(e) + { + event.stop(e); + try + { + if(form.validate()) + { + var values = {}; + for(var i in widgets) + { + var widget = widgets[i]; + var value = widget.value; + var propName = widget.name; + if ((widget instanceof dijit.form.CheckBox || widget instanceof dijit.form.RadioButton)) + { + values[ propName ] = widget.checked; + } + else if (value != "" || (widget.initialValue && value != widget.initialValue)) + { + values[ propName ] = value ? value: null; + } + } + + var that = this; + xhr.put({url: putURL, sync: true, handleAs: "json", + headers: { "Content-Type": "application/json"}, + putData: json.toJson(values), + load: function(x) {that.success = true; }, + error: function(error) {that.success = false; that.failureReason = error;}}); + if(this.success === true) + { + setAttributesDialog.destroy(); + } + else + { + alert("Error:" + this.failureReason); + } + return false; + } + else + { + alert('Form contains invalid data. Please correct first'); + return false; + } + } + catch(e) + { + alert("Unexpected exception:" + e.message); + return false; + } + }); + form.connectChildren(true); + setAttributesDialog.show(); + + }; + return util; });
\ No newline at end of file diff --git a/java/broker-plugins/management-http/src/main/java/resources/js/qpid/management/Broker.js b/java/broker-plugins/management-http/src/main/java/resources/js/qpid/management/Broker.js index 98d442bf14..e479deddbc 100644 --- a/java/broker-plugins/management-http/src/main/java/resources/js/qpid/management/Broker.js +++ b/java/broker-plugins/management-http/src/main/java/resources/js/qpid/management/Broker.js @@ -35,6 +35,11 @@ define(["dojo/_base/xhr", "dojox/grid/enhanced/plugins/IndirectSelection", "dijit/layout/AccordionContainer", "dijit/layout/AccordionPane", + "dijit/form/FilteringSelect", + "dijit/form/NumberSpinner", + "dijit/form/ValidationTextBox", + "dijit/form/CheckBox", + "dojo/store/Memory", "dojo/domReady!"], function (xhr, parser, query, connect, properties, updater, util, UpdatableStore, EnhancedGrid, registry, addAuthenticationProvider, addVirtualHost, addPort) { @@ -45,7 +50,301 @@ define(["dojo/_base/xhr", if(parent) { this.modelObj.parent = {}; this.modelObj.parent[ parent.type] = parent; - } + } + this.attributeWidgetFactories = [{ + name: "name", + createWidget: function(brokerData) { + return new dijit.form.ValidationTextBox({ + required: true, + value: brokerData.name, + disabled: true, + label: "Name*:", + name: "name"}) + } + }, { + name: "defaultAuthenticationProvider", + createWidget: function(brokerData) { + var providers = brokerData.authenticationproviders; + var data = []; + if (providers) { + for (var i=0; i< providers.length; i++) { + data.push({id: providers[i].name, name: providers[i].name}); + } + } + var providersStore = new dojo.store.Memory({ data: data }); + return new dijit.form.FilteringSelect({ + required: true, + store: providersStore, + value: brokerData.defaultAuthenticationProvider, + label: "Default Authentication Provider*:", + name: "defaultAuthenticationProvider"}) + } + }, { + name: "defaultVirtualHost", + createWidget: function(brokerData) { + var hosts = brokerData.virtualhosts; + var data = []; + if (hosts) { + for (var i=0; i< hosts.length; i++) { + data.push({id: hosts[i].name, name: hosts[i].name}); + } + } + var hostsStore = new dojo.store.Memory({ data: data }); + return new dijit.form.FilteringSelect({ + required: true, store: hostsStore, + value: brokerData.defaultVirtualHost, + label: "Default Virtual Host*:", + name: "defaultVirtualHost"}) + } + }, { + name: "aclFile", + createWidget: function(brokerData) { + return new dijit.form.ValidationTextBox({ + required: false, + value: brokerData.aclFile, + label: "ACL file location:", + name: "aclFile"}) + } + }, { + name: "groupFile", + createWidget: function(brokerData) + { + return new dijit.form.ValidationTextBox({ + required: false, + value: brokerData.groupFile, + label: "Group file location:", + name: "groupFile"}); + } + }, { + name: "keyStorePath", + createWidget: function(brokerData) { + return new dijit.form.ValidationTextBox({ + required: false, + value: brokerData.keyStorePath, + label: "Path to keystore:", + name: "keyStorePath"}); + } + }, { + name: "keyStoreCertAlias", + createWidget: function(brokerData) { + return new dijit.form.ValidationTextBox({ + required: false, + value: brokerData.keyStoreCertAlias, + label: "Keystore certificate alias:", + name: "keyStoreCertAlias"}); + } + }, { + name: "keyStorePassword", + requiredFor: "keyStorePath", + createWidget: function(brokerData) { + return new dijit.form.ValidationTextBox({ + required: false, + label: "Keystore password:", + invalidMessage: "Missed keystore password", + name: "keyStorePassword"}); + } + }, { + name: "trustStorePath", + createWidget: function(brokerData) + { + return new dijit.form.ValidationTextBox({ + required: false, + value: brokerData.trustStorePath, + label: "Path to truststore:", + name: "trustStorePath"}); + } + }, { + name: "trustStorePassword", + requiredFor: "trustStorePath", + createWidget: function(brokerData) { + return new dijit.form.ValidationTextBox({ + required: false, + label: "Truststore password:", + invalidMessage: "Missed trustore password", + name: "trustStorePassword"}); + } + }, { + name: "peerStorePath", + createWidget: function(brokerData) { + return new dijit.form.ValidationTextBox({ + required: false, + value: brokerData.peerStorePath, + label: "Path to peerstore:", + name: "peerStorePath"}); + } + }, { + name: "peerStorePassword", + requiredFor: "peerStorePath", + createWidget: function(brokerData) { + return new dijit.form.ValidationTextBox({ + required: false, + label: "Peerstore password:", + invalidMessage: "Missed peerstore password", + name: "peerStorePassword"}); + } + }, { + name: "alertThresholdQueueDepth", + createWidget: function(brokerData) { + return new dijit.form.ValidationTextBox({ + trim: "true", + regexp: "[0-9]+", + invalidMessage: "Invalid value", + required: false, + value: brokerData.alertThresholdQueueDepth, + placeholder: "Count of messages", + label: "Queue depth alert threshold:", + name: "alertThresholdQueueDepth" + }); + } + }, { + name: "alertThresholdMessageAge", + createWidget: function(brokerData) { + return new dijit.form.ValidationTextBox({ + trim: "true", + regexp: "[0-9]+", + invalidMessage: "Invalid value", + required: false, + value: brokerData.alertThresholdMessageAge, + placeholder: "Time in ms", + label: "Queue message age alert threshold:", + name: "alertThresholdMessageAge" + }); + } + }, { + name: "alertThresholdMessageSize", + createWidget: function(brokerData) { + return new dijit.form.ValidationTextBox({ + trim: "true", + regexp: "[0-9]+", + invalidMessage: "Invalid value", + required: false, + value: brokerData.alertThresholdMessageSize, + placeholder: "Size in bytes", + label: "Queue message size alert threshold:", + name: "alertThresholdMessageSize" + }); + } + }, { + name: "alertRepeatGap", + createWidget: function(brokerData) { + return new dijit.form.ValidationTextBox({ + trim: "true", + regexp: "[0-9]+", + invalidMessage: "Invalid value", + required: false, + value: brokerData.alertThresholdMessageSize, + value: brokerData.alertRepeatGap, + placeholder: "Time in ms", + label: "Queue alert repeat gap:", + name: "alertRepeatGap" + }); + } + }, { + name: "maximumDeliveryAttempts", + createWidget: function(brokerData) { + return new dijit.form.ValidationTextBox({ + trim: "true", + regexp: "[0-9]+", + invalidMessage: "Invalid value", + required: false, + value: brokerData.maximumDeliveryAttempts, + placeholder: "Count of messages", + label: "Queue maximum delivery retries:", + name: "maximumDeliveryAttempts" + }); + } + }, { + name: "deadLetterQueueEnabled", + createWidget: function(brokerData) { + return new dijit.form.CheckBox({ + required: false, + checked: brokerData.deadLetterQueueEnabled, + value: "true", + label: "Dead letter queue enabled:", + name: "deadLetterQueueEnabled", + }); + } + }, { + name: "queueFlowControlSizeBytes", + createWidget: function(brokerData) { + return new dijit.form.ValidationTextBox({ + trim: "true", + regexp: "[0-9]+", + invalidMessage: "Invalid value", + required: false, + value: brokerData.queueFlowControlSizeBytes, + placeholder: "Size in bytes", + label: "Queue flow capacity:", + name: "queueFlowControlSizeBytes", + }); + } + }, { + name: "queueFlowResumeSizeBytes", + createWidget: function(brokerData) { + return new dijit.form.ValidationTextBox({ + trim: "true", + regexp: "[0-9]+", + invalidMessage: "Invalid value", + required: false, + value: brokerData.queueFlowResumeSizeBytes, + placeholder: "Size in bytes", + label: "Queue flow resume capacity:", + name: "queueFlowResumeSizeBytes", + }); + } + }, { + name: "sessionCountLimit", + createWidget: function(brokerData) + { + return new dijit.form.NumberSpinner({ + invalidMessage: "Invalid value", + required: false, + value: brokerData.sessionCountLimit, + smallDelta: 1, + constraints: {min:1,max:65535,places:0, pattern: "#####"}, + label: "Connection session limit:", + name: "sessionCountLimit" + }); + } + }, { + name: "heartBeatDelay", + createWidget: function(brokerData) { + return new dijit.form.ValidationTextBox({ + trim: "true", + regexp: "[0-9]+", + invalidMessage: "Invalid value", + required: false, + value: brokerData.heartBeatDelay, + placeholder: "Time in ms", + label: "Heart beat delay:", + name: "heartBeatDelay" + }); + } + }, { + name: "statisticsReportingPeriod", + createWidget: function(brokerData) { + return new dijit.form.ValidationTextBox({ + trim: "true", + regexp: "[0-9]+", + invalidMessage: "Invalid value", + required: false, + value: brokerData.statisticsReportingPeriod, + placeholder: "Time in ms", + label: "Statistics reporting period:", + name: "statisticsReportingPeriod" + }); + } + }, { + name: "statisticsReportingResetEnabled", + createWidget: function(brokerData) + { + return new dijit.form.CheckBox({ + required: false, checked: brokerData.statisticsReportingResetEnabled, value: "true", + label: "Statistics reporting period enabled:", + name: "statisticsReportingResetEnabled" + }); + } + } ]; } Broker.prototype.getTitle = function() @@ -62,7 +361,7 @@ define(["dojo/_base/xhr", contentPane.containerNode.innerHTML = data; parser.parse(contentPane.containerNode); - that.brokerUpdater = new BrokerUpdater(contentPane.containerNode, that.modelObj, that.controller); + that.brokerUpdater = new BrokerUpdater(contentPane.containerNode, that.modelObj, that.controller, that.attributeWidgetFactories); updater.add( that.brokerUpdater ); @@ -109,6 +408,18 @@ define(["dojo/_base/xhr", "Are you sure you want to delete port"); } ); + + var editButton = query(".editBroker", contentPane.containerNode)[0]; + connect.connect(registry.byNode(editButton), "onClick", + function(evt){ + util.showSetAttributesDialog( + that.attributeWidgetFactories, + that.brokerUpdater.brokerData, + "rest/broker", + "Set broker attributes"); + } + ); + }}); }; @@ -116,16 +427,11 @@ define(["dojo/_base/xhr", updater.remove( this.brokerUpdater ); }; - function BrokerUpdater(node, brokerObj, controller) + function BrokerUpdater(node, brokerObj, controller, attributes) { this.controller = controller; - this.name = query(".broker-name", node)[0]; - /*this.state = dom.byId("state"); - this.durable = dom.byId("durable"); - this.lifetimePolicy = dom.byId("lifetimePolicy"); - */ this.query = "rest/broker"; - + this.attributes = attributes; var that = this; xhr.get({url: this.query, sync: properties.useSyncGet, handleAs: "json"}) @@ -259,11 +565,28 @@ define(["dojo/_base/xhr", BrokerUpdater.prototype.updateHeader = function() { - this.name.innerHTML = this.brokerData[ "name" ]; - /* this.state.innerHTML = this.brokerData[ "state" ]; - this.durable.innerHTML = this.brokerData[ "durable" ]; - this.lifetimePolicy.innerHTML = this.brokerData[ "lifetimePolicy" ]; -*/ + var brokerData = this.brokerData; + for(var i in this.attributes) + { + var propertyName = this.attributes[i].name; + var element = dojo.byId("brokerAttribute." + propertyName); + if (element) + { + if (brokerData.hasOwnProperty(propertyName)) + { + var container = dojo.byId("brokerAttribute." + propertyName + ".container"); + if (container) + { + container.style.display = "block"; + } + element.innerHTML = brokerData [propertyName]; + } + else + { + element.innerHTML = ""; + } + } + } }; BrokerUpdater.prototype.update = function() @@ -295,7 +618,5 @@ define(["dojo/_base/xhr", }; - - return Broker; }); diff --git a/java/broker-plugins/management-http/src/main/java/resources/showBroker.html b/java/broker-plugins/management-http/src/main/java/resources/showBroker.html index 60c514e262..8faae08e1e 100644 --- a/java/broker-plugins/management-http/src/main/java/resources/showBroker.html +++ b/java/broker-plugins/management-http/src/main/java/resources/showBroker.html @@ -19,14 +19,103 @@ - --> <div class="broker"> - <span>Name:</span><span class="broker-name" style="position:absolute; left:6em"></span> - <br/> -<!-- <span>State:</span><span class="broker-state" style="position:absolute; left:6em"></span> - <br/> - <span>Durable:</span><span class="broker-durable" style="position:absolute; left:6em"></span> - <br/> - <span>Lifespan:</span><span class="broker-lifetimePolicy" style="position:absolute; left:6em" ></span> - <br/> --> + <div data-dojo-type="dijit.TitlePane" data-dojo-props="title: 'Broker Attributes', open: false"> + <div id="brokerAttributes" style="clear:both"> + <div id="brokerAttribute.name.container" style="display: none; clear:both"> + <div class="formLabel-labelCell" style="float:left; width: 250px;">Broker Name:</div> + <div id="brokerAttribute.name" style="float:left;"></div> + </div> + <div id="brokerAttribute.defaultAuthenticationProvider.container" style="display: none; clear:both"> + <div class="formLabel-labelCell" style="float:left; width: 250px;">Default authentication provider:</div> + <div id="brokerAttribute.defaultAuthenticationProvider" style="float:left;"></div> + </div> + <div id="brokerAttribute.defaultVirtualHost.container" style="display: none; clear:both; clear:both;"> + <div class="formLabel-labelCell" style="float:left; width: 250px;">Default virtual host:</div> + <div id="brokerAttribute.defaultVirtualHost" style="float:left;"></div> + </div> + <div id="brokerAttribute.aclFile.container" style="display: none; clear:both"> + <div class="formLabel-labelCell" style="float:left; width: 250px;">ACL file location:</div> + <div id="brokerAttribute.aclFile" style="float:left;"></div> + </div> + <div id="brokerAttribute.groupFile.container" style="display: none; clear:both"> + <div class="formLabel-labelCell" style="float:left; width: 250px;">Group file location:</div> + <div id="brokerAttribute.groupFile" style="float:left;"></div> + </div> + <div id="brokerAttribute.keyStorePath.container" style="display: none; clear:both"> + <div class="formLabel-labelCell" style="float:left; width: 250px;">Path to keystore:</div> + <div id="brokerAttribute.keyStorePath" style="float:left;"></div><br/> + <div class="formLabel-labelCell" style="float:left; width: 250px;">Keystore alias:</div> + <div id="brokerAttribute.keyStoreCertAlias" style="float:left;"></div> + </div> + <div id="brokerAttribute.trustStorePath.container" style="display: none; clear:both"> + <div class="formLabel-labelCell" style="float:left; width: 250px;">Path to truststore:</div> + <div id="brokerAttribute.trustStorePath" style="float:left;"></div> + </div> + <div id="brokerAttribute.peerStorePath.container" style="display: none; clear:both"> + <div class="formLabel-labelCell" style="float:left; width: 250px;">Path to peerstore:</div> + <div id="brokerAttribute.peerStorePath" style="float:left;"></div> + </div> + <div id="brokerAttribute.statisticsReportingPeriod.container" style="display: none; clear:both"> + <div class="formLabel-labelCell" style="float:left; width: 250px;">Statistics reporting period:</div> + <div id="brokerAttribute.statisticsReportingPeriod" style="float:left;"></div> + </div> + <div id="brokerAttribute.statisticsReportingResetEnabled.container" style="display: none; clear:both"> + <div class="formLabel-labelCell" style="float:left; width: 250px;">Statistics reporting period enabled:</div> + <div id="brokerAttribute.statisticsReportingResetEnabled" style="float:left;"></div> + </div> + <div style="clear:both"></div> + <br/> + <div data-dojo-type="dijit.TitlePane" data-dojo-props="title: 'Queue Attributes', open: true"> + <div id="brokerAttribute.alertThresholdQueueDepth.container" style="clear:both"> + <div class="formLabel-labelCell" style="float:left; width: 250px;">Queue depth alert threshold:</div> + <div id="brokerAttribute.alertThresholdQueueDepth" style="float:left;"></div> + </div> + <div id="brokerAttribute.alertThresholdMessageAge.container" style="clear:both"> + <div class="formLabel-labelCell" style="float:left; width: 250px;">Queue message age alert threshold:</div> + <div id="brokerAttribute.alertThresholdMessageAge" style="float:left;"></div> ms + </div> + <div id="brokerAttribute.alertThresholdMessageSize.container" style="clear:both"> + <div class="formLabel-labelCell" style="float:left; width: 250px;">Queue message size alert threshold:</div> + <div id="brokerAttribute.alertThresholdMessageSize" style="float:left;"></div> + </div> + <div id="brokerAttribute.alertRepeatGap.container" style="clear:both"> + <div class="formLabel-labelCell" style="float:left; width: 250px;">Queue alert alert repeat gap:</div> + <div id="brokerAttribute.alertRepeatGap" style="float:left;"></div> ms + </div> + <div id="brokerAttribute.maximumDeliveryAttempts.container" style="clear:both"> + <div class="formLabel-labelCell" style="float:left; width: 250px;">Queue maximum delivery retries:</div> + <div id="brokerAttribute.maximumDeliveryAttempts" style="float:left;"></div> + </div> + <div id="brokerAttribute.deadLetterQueueEnabled.container" style="clear:both"> + <div class="formLabel-labelCell" style="float:left; width: 250px;">Dead letter queue enabled:</div> + <div id="brokerAttribute.deadLetterQueueEnabled" style="float:left;"></div> + </div> + <div id="brokerAttribute.queueFlowControlSizeBytes.container" style="clear:both"> + <div class="formLabel-labelCell" style="float:left; width: 250px;">Queue flow capacity:</div> + <div id="brokerAttribute.queueFlowControlSizeBytes" style="float:left;"></div> + </div> + <div id="brokerAttribute.queueFlowResumeSizeBytes.container" style="clear:both"> + <div class="formLabel-labelCell" style="float:left; width: 250px;">Queue flow resume capacity:</div> + <div id="brokerAttribute.queueFlowResumeSizeBytes" style="float:left;"></div> + </div> + <div style="clear:both"></div> + </div> + <br/> + <div data-dojo-type="dijit.TitlePane" data-dojo-props="title: 'Connection attributes', open: true"> + <div id="brokerAttribute.sessionCountLimit.container" style="clear:both"> + <div class="formLabel-labelCell" style="float:left; width: 250px;">Connection session limit:</div> + <div id="brokerAttribute.sessionCountLimit" style="float:left;"></div> + </div> + <div id="brokerAttribute.heartBeatDelay.container" style="clear:both"> + <div class="formLabel-labelCell" style="float:left; width: 250px;">Heart beat delay:</div> + <div id="brokerAttribute.heartBeatDelay" style="float:left;"></div> ms + </div> + <div style="clear:both"></div> + </div> + </div> + <br/> + <button data-dojo-type="dijit.form.Button" class="editBroker">Edit</button> + </div> <br/> <div data-dojo-type="dijit.TitlePane" data-dojo-props="title: 'Virtual Hosts'"> <div class="broker-virtualhosts"></div> |
