http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/4940e63d/console/stand-alone/plugin/html/qdrCharts.html ---------------------------------------------------------------------- diff --git a/console/stand-alone/plugin/html/qdrCharts.html b/console/stand-alone/plugin/html/qdrCharts.html new file mode 100644 index 0000000..8ba1ffe --- /dev/null +++ b/console/stand-alone/plugin/html/qdrCharts.html @@ -0,0 +1,150 @@ +<div class="main-display row-fluid qdrCharts" ng-controller="QDR.ChartsController"> + <div ng-show="hasCharts()"> + <div ng-repeat="chart in svgCharts" class="chartContainer"> + <p class="chartLabels"> + <button ng-click="delChart(chart)" title="Delete"><i class="icon-trash"></i></button> + <button ng-click="editChart(chart)" title="Configure"><i class="icon-edit"></i></button> + <button ng-click="zoomChart(chart)" title="Zoom {{chart.zoomed ? 'in' : 'out'}}" ng-if="!chart.chart.request().nodeList"><i ng-class="chart.zoomed ? 'icon-zoom-in' : 'icon-zoom-out'"></i></button> + </p><div style="clear:both"></div> + <div id="{{chart.chart.id()}}" class="aChart d3Chart"></div> + </div> + <div ng-init="chartsLoaded()"></div> + </div> + <div ng-hide="hasCharts()" class="centered"> + There are no charts. To add charts to this page, click on a <i class="icon-bar-chart"></i> icon on the <button ng-click="showListPage()"><i class="icon-list "></i> List</button> page. + </div> +</div> + +<!-- + This is the template for the graph dialog that is displayed. It uses the + dialogCtrl controller in qdrCharts.js. +--> +<script type="text/ng-template" id="chart-config-template.html"> +<div class="chartOptions"> + <div class="modal-header"> + <h3 class="modal-title">Chart {{chart.attr() | humanify}}</h3> + </div> + <div class="modal-body"> + <div id="{{svgDivId}}" class="d3Chart"></div> + <uib-tabset> + <uib-tab heading="Type"> + <legend>Chart type</legend> + <label><input type="radio" ng-model="dialogChart.type" value="value" ng-change="chartChanged()" /> Value Chart</label> + <label><input type="radio" ng-model="dialogChart.type" value="rate" ng-change="chartChanged()" /> Rate Chart</label> + <div class="dlg-slider" ng-show="dialogChart.type=='rate'"> + <span>Rate Window: {{rateWindow}} second{{rateWindow > 1 ? "s" : ""}}</span> + <div class="slider" ui-slider="slider.options" ng-model="rateWindow"></div> + </div> + <div style="clear:both;"> </div> + </uib-tab> + <uib-tab heading="Colors"> + <legend>Chart colors</legend> + <div class="colorPicker"> + <div>Area</div><input minicolors="areaColor" id="position-bottom-left" type="text" ng-model="dialogChart.areaColor" /> + </div> + <div class="colorPicker"> + <div>Line</div><input minicolors="lineColor" id="position-bottom-left" type="text" ng-model="dialogChart.lineColor" /> + </div> + <div style="clear:both;"> </div> + </uib-tab> + <uib-tab heading="Duration"> + <legend>Chart duration</legend> + <div class="dlg-slider duration"> + <span>Show data for past {{dialogChart.visibleDuration}} minute{{dialogChart.visibleDuration > 1 ? "s" : ""}}</span> <div class="slider" ui-slider="duration.options" ng-model="dialogChart.visibleDuration"></div> + </div> + <div style="clear:both;"> </div> + + </uib-tab> + </uib-tabset> + </div> + <div class="modal-footer"> + <button class="btn btn-success" type="button" ng-click="apply()">Apply to existing chart</button> + <button class="btn btn-info" type="button" ng-click="copyToDashboard()">Create new chart</button> + <button class="btn btn-primary" type="button" ng-click="okClick()">Close</button> + </div> +</div> +<!-- + <div class="chartOptions" title="Configure chart" ng-controller="QDR.ChartDialogController"> + <p class="dialogHeader">Title: <input type="text" ng-model="userTitle" /></p> + <div id="{{svgDivId}}" class="d3Chart"></div> + <p></p> + <uib-tabset> + <uib-tab heading="Type"> + <legend>Chart type</legend> + <label><input type="radio" ng-model="dialogChart.type" value="value" ng-change="chartChanged()" /> Value Chart</label> + <label><input type="radio" ng-model="dialogChart.type" value="rate" ng-change="chartChanged()" /> Rate Chart</label> + + <div class="dlg-slider" ng-show="dialogChart.type=='rate'"> + <span>Rate Window: {{rateWindow}} second{{rateWindow > 1 ? "s" : ""}}</span> + <div class="slider" ui-slider="slider.options" ng-model="rateWindow"></div> + </div> + <div style="clear:both;"> </div> + </uib-tab> + <uib-tab heading="Colors"> + <legend>Chart colors</legend> + <div class="colorPicker"> + <div>Area</div><input minicolors="areaColor" id="position-bottom-left" type="text" ng-model="dialogChart.areaColor" /> + </div> + <div class="colorPicker"> + <div>Line</div><input minicolors="lineColor" id="position-bottom-left" type="text" ng-model="dialogChart.lineColor" /> + </div> + <div style="clear:both;"> </div> + </uib-tab> + <uib-tab heading="Duration"> + <legend>Chart duration</legend> + <div class="dlg-slider duration"> + <span>Show data for past {{dialogChart.visibleDuration}} minute{{dialogChart.visibleDuration > 1 ? "s" : ""}}</span> <div class="slider" ui-slider="duration.options" ng-model="dialogChart.visibleDuration"></div> + </div> + <div style="clear:both;"> </div> + + </uib-tab> + </uib-tabset> + + <tabset> + <tab heading="Type"> + <div> + <legend>Chart type</legend> + <label> + <input type="radio" ng-model="dialogChart.type" value="value" ng-change="chartChanged()" /> + Value Chart + </label> + <label class="rateGroup"> + <input type="radio" ng-model="dialogChart.type" value="rate" ng-change="chartChanged()" /> Rate Chart + </label> + <div class="dlg-slider" ng-show="dialogChart.type=='rate'"> + <span>Rate Window: {{rateWindow}} second{{rateWindow > 1 ? "s" : ""}}</span> <div class="slider" ui-slider="slider.options" ng-model="rateWindow"></div> + </div> + </div> + <div style="clear:both;"> </div> + </tab> + <tab heading="Colors"> + <div> + <legend>Chart colors</legend> + <div class="colorPicker"> + <div>Area</div><input minicolors="areaColor" id="position-bottom-left" type="text" ng-model="dialogChart.areaColor" /> + </div> + <div class="colorPicker"> + <div>Line</div><input minicolors="lineColor" id="position-bottom-left" type="text" ng-model="dialogChart.lineColor" /> + </div> + </div> + <div style="clear:both;"> </div> + </tab> + <tab heading="Duration"> + <div> + <legend>Chart duration</legend> + </div> + <div class="dlg-slider duration"> + <span>Show data for past {{dialogChart.visibleDuration}} minute{{dialogChart.visibleDuration > 1 ? "s" : ""}}</span> <div class="slider" ui-slider="duration.options" ng-model="dialogChart.visibleDuration"></div> + </div> + <div style="clear:both;"> </div> + </tab> + </tabset> + <div class="okButton"> + <button ng-click="apply()">Apply to existing chart</button> + <button ng-click="copyToDashboard()">Create new chart</button> + <button ng-click="okClick()">Close</button> + </div> + </div> +--> +</script> +
http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/4940e63d/console/stand-alone/plugin/html/qdrConnect.html ---------------------------------------------------------------------- diff --git a/console/stand-alone/plugin/html/qdrConnect.html b/console/stand-alone/plugin/html/qdrConnect.html new file mode 100644 index 0000000..34e452f --- /dev/null +++ b/console/stand-alone/plugin/html/qdrConnect.html @@ -0,0 +1,65 @@ +<div class="row-fluid" ng-controller="QDR.SettingsController"> + <div class="login container" ng-hide="connecting"> + <div class="row" id="login-container"> + <div class="span2 offset4"> + <!-- <div simple-form name="settings" data="formConfig" entity="formEntity"></div> --> + <form class="form-horizontal no-bottom-margin" novalidate name="settings" action="" method="post"> + <fieldset> + <div class="control-group"> + <label class="control-label">Address: </label> + <div class="controls"> + <input type="text" description="Router address" ng-model="formEntity.address" name="address" required="required" autofocus="autofocus" class="ng-pristine ng-valid ng-valid-required"> + <span class="help-block"></span> + </div> + </div> + <div class="control-group"> + <label class="control-label" title="Port to connect to, by default 5673">Port: </label> + <div class="controls"> + <input type="number" description="Router port" tooltip="Ports to connect to, by default 5672" ng-model="formEntity.port" name="port" title="Port to connect to, by default 5673" class="ng-scope ng-pristine ng-valid ng-valid-number"> + <span class="help-block"></span> + </div> + </div> + <!-- + <div class="control-group"> + <label class="control-label">Username: </label> + <div class="controls"> + <input type="text" description="User Name" ng-model="formEntity.username" name="username" class="ng-pristine ng-valid"> + <span class="help-block"></span> + </div> + </div> + <div class="control-group"> + <label class="control-label">Password: </label> + <div class="controls"> + <input type="password" description="Password" ng-model="formEntity.password" name="password" class="ng-pristine ng-valid"> + <span class="help-block"></span> + </div> + </div> + --> + <div class="control-group"> + <label class="control-label" title="Whether or not the connection should be started as soon as you navigate to the console">Autostart: </label> + <div class="controls"> + <input type="checkbox" description="Connect at startup" tooltip="Whether or not the connection should be started as soon as you navigate to the console" ng-model="formEntity.autostart" name="autostart" title="Whether or not the connection should be started as soon as you log into hawtio" class="ng-scope ng-pristine ng-valid"> + <span class="help-block"></span> + </div> + </div> + <input type="submit" style="position: absolute; left: -9999px; width: 1px; height: 1px;"> + </fieldset> + </form> + <p></p> + <div> + <button class="btn btn-primary pull-right" ng-disabled="settings.$invalid" ng-click="connect()">{{buttonText()}}</button> + </div> + </div> + </div> + </div> + + <div class="span4 centered" ng-show="connecting"> + <i class="icon-spin icon-spinner icon-4x"></i> + <p>Please wait, connecting now...</p> + </div> + <div class="centered" ng-show="connectionError"> + <p>There was a connection error: {{connectionErrorText}}</p> + </div> + + <div class="span4"></div> +</div> http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/4940e63d/console/stand-alone/plugin/html/qdrGraphs.html ---------------------------------------------------------------------- diff --git a/console/stand-alone/plugin/html/qdrGraphs.html b/console/stand-alone/plugin/html/qdrGraphs.html new file mode 100644 index 0000000..c311246 --- /dev/null +++ b/console/stand-alone/plugin/html/qdrGraphs.html @@ -0,0 +1,15 @@ +<div ng-controller="QDR.ListController" title="" + class="prefs"> + <div class="row-fluid"> + <div class="tabbable"> + <div value="graphs" + class="tab-pane" + title="graphs"> + <div> + <div id="graph"></div> + + </div> + </div> + </div> + </div> +</div> http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/4940e63d/console/stand-alone/plugin/html/qdrLayout.html ---------------------------------------------------------------------- diff --git a/console/stand-alone/plugin/html/qdrLayout.html b/console/stand-alone/plugin/html/qdrLayout.html new file mode 100644 index 0000000..d164f8a --- /dev/null +++ b/console/stand-alone/plugin/html/qdrLayout.html @@ -0,0 +1,26 @@ +<!-- +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. +--> +<ul class="nav nav-tabs connected" ng-controller="QDR.NavBarController"> + <li ng-repeat="link in breadcrumbs" ng-show="isValid(link)" ng-class='{active : isActive(link.href), "pull-right" : isRight(link), haschart: hasChart(link)}'> + <a ng-href="{{link.href}}{{hash}}" ng-bind-html="link.content | to_trusted"></a> + </li> +</ul> +<div class="row-fluid"> + <div ng-view></div> +</div> http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/4940e63d/console/stand-alone/plugin/html/qdrList.html ---------------------------------------------------------------------- diff --git a/console/stand-alone/plugin/html/qdrList.html b/console/stand-alone/plugin/html/qdrList.html new file mode 100644 index 0000000..156b21a --- /dev/null +++ b/console/stand-alone/plugin/html/qdrList.html @@ -0,0 +1,56 @@ +<!-- +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. +--> +<div class="main-display row-fluid qdrList" ng-controller="QDR.ListController"> + <ul class="nav nav-tabs qdrListNodes"> + <li ng-repeat="node in nodes" ng-click="selectNode(node)" ng-class="{active : isNodeSelected(node.id)}"> <span>{{node.name}}</span> </li> + </ul> + + <div class="row-fluid qdrListActions"> + <ul class="nav nav-tabs ng-scope"> + <li ng-repeat="entity in entities" ng-class="{active : isActionActive(entity.name)}" ng-click="selectAction(entity.name)" > + <a title="{{entity.title}}" data-placement="bottom"> {{entity.humanName}} </a></li> + </ul> + <div class="gridStyle" ng-style="getTableHeight()" ui-grid-auto-resize ui-grid-selection ui-grid="gridDef"></div> + <div class="selectedItems"> + <div ng-style="getDetailsTableHeight()" ui-grid-auto-resize ui-grid-selection ui-grid="details"></div> + </div> + </div> +</div> +<!-- + This is the template for the graph dialog that is displayed. It uses the + dialogCtrl controller in qdrList.js. +--> +<script type="text/ng-template" id="template-from-script.html"> + <div class="modal-header"> + <h3 class="modal-title">Chart {{chart.attr() | humanify}}</h3> + </div> + <div class="modal-body"> + <div id="{{svgDivId}}" class="d3Chart"></div> + </div> + <div class="modal-footer"> + <span ng-hide="isOnChartsPage()"> + <button class="btn btn-success" type="button" ng-click="addChartsPage()"><i class="icon-bar-chart"></i> Add</button> this chart to the Charts page. + </span> + <span ng-show="isOnChartsPage()"> + <button class="btn btn-danger" type="button" ng-click="delChartsPage()">Remove</button> this chart from the <button class="btn btn-success" type="button" ng-click="showChartsPage()"><i class="icon-bar-chart"></i> Charts</button> page. + </span> + <button class="btn btn-primary" type="button" ng-click="ok()">Close</button> + </div> +</script> + http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/4940e63d/console/stand-alone/plugin/html/qdrOverview.html ---------------------------------------------------------------------- diff --git a/console/stand-alone/plugin/html/qdrOverview.html b/console/stand-alone/plugin/html/qdrOverview.html new file mode 100644 index 0000000..d63ea0e --- /dev/null +++ b/console/stand-alone/plugin/html/qdrOverview.html @@ -0,0 +1,83 @@ +<!-- +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. +--> +<div ng-controller="QDR.OverviewController"> + + <div class="treeContainer"> + <div id="overtree"></div> + </div> + + <div class="treeDetails" ng-include="template.url"></div> +</div> + + +<!-- the following scripts are content that gets loaded into the above div that has the temple.url --> +<script type="text/ng-template" id="routers.html"> + <div class="row-fluid"> + <h3>Routers</h3> + <div class="overview"> + <div class="gridStyle" ng-style="getGridHeight(allRouters)" ui-grid-auto-resize ui-grid-selection ui-grid="allRouters"></div> + </div> + </div> +</script> + +<script type="text/ng-template" id="router.html"> + <div class="row-fluid"> + <h3>Router {{router.data.title}}</h3> + <div ng-style="getGridHeight(routerGrid)" ui-grid-auto-resize ui-grid="routerGrid" class="gridStyle noHighlight"></div> + </div> +</script> + +<script type="text/ng-template" id="addresses.html"> + <div class="row-fluid"> + <h3>Addresses</h3> + <div class="overview"> + <div class="gridStyle" ng-style="getGridHeight(addressGrid)" ui-grid-auto-resize ui-grid-selection ui-grid="addressGrid"></div> + </div> + </div> +</script> +<script type="text/ng-template" id="address.html"> + <div class="row-fluid"> + <h3>Address {{address.data.title}}</h3> + <div ng-style="getGridHeight(addressGrid)" ui-grid-auto-resize class="gridStyle noHighlight" ui-grid="addressGrid"></div> + </div> +</script> +<script type="text/ng-template" id="connections.html"> + <div class="row-fluid"> + <h3>Connections</h3> + <div class="overview"> + <div class="gridStyle" ng-style="getGridHeight(allConnectionGrid)" ui-grid-auto-resize ui-grid-selection ui-grid="allConnectionGrid"></div> + </div> + </div> +</script> +<script type="text/ng-template" id="connection.html"> + <div class="row-fluid"> + <h3>Connection {{connection.data.title}}</h3> + <div ng-style="getGridHeight(connectionGrid)" ui-grid-auto-resize class="gridStyle noHighlight" ui-grid="connectionGrid"></div> + </div> +</script> +<script type="text/ng-template" id="logs.html"> + <div class="row-fluid"> + <h3>Logs</h3> + </div> +</script> +<script type="text/ng-template" id="log.html"> + <div class="row-fluid"> + <h3>Log {{log.data.title}}</h3> + </div> +</script> http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/4940e63d/console/stand-alone/plugin/html/qdrSchema.html ---------------------------------------------------------------------- diff --git a/console/stand-alone/plugin/html/qdrSchema.html b/console/stand-alone/plugin/html/qdrSchema.html new file mode 100644 index 0000000..15ebb46 --- /dev/null +++ b/console/stand-alone/plugin/html/qdrSchema.html @@ -0,0 +1,21 @@ +<!-- +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. +--> +<div class="main-display row-fluid" ng-controller="QDR.SchemaController"> + <json-formatter json="schema" open="2"></json-formatter> +</div> \ No newline at end of file http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/4940e63d/console/stand-alone/plugin/img/ZeroClipboard.swf ---------------------------------------------------------------------- diff --git a/console/stand-alone/plugin/img/ZeroClipboard.swf b/console/stand-alone/plugin/img/ZeroClipboard.swf new file mode 100644 index 0000000..a3aaa20 Binary files /dev/null and b/console/stand-alone/plugin/img/ZeroClipboard.swf differ http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/4940e63d/console/stand-alone/plugin/img/ajax-loader.gif ---------------------------------------------------------------------- diff --git a/console/stand-alone/plugin/img/ajax-loader.gif b/console/stand-alone/plugin/img/ajax-loader.gif new file mode 100644 index 0000000..3c2f7c0 Binary files /dev/null and b/console/stand-alone/plugin/img/ajax-loader.gif differ http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/4940e63d/console/stand-alone/plugin/img/dynatree/icons.gif ---------------------------------------------------------------------- diff --git a/console/stand-alone/plugin/img/dynatree/icons.gif b/console/stand-alone/plugin/img/dynatree/icons.gif new file mode 100644 index 0000000..6e237a0 Binary files /dev/null and b/console/stand-alone/plugin/img/dynatree/icons.gif differ http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/4940e63d/console/stand-alone/plugin/img/dynatree/loading.gif ---------------------------------------------------------------------- diff --git a/console/stand-alone/plugin/img/dynatree/loading.gif b/console/stand-alone/plugin/img/dynatree/loading.gif new file mode 100644 index 0000000..2a96840 Binary files /dev/null and b/console/stand-alone/plugin/img/dynatree/loading.gif differ http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/4940e63d/console/stand-alone/plugin/js/navbar.js ---------------------------------------------------------------------- diff --git a/console/stand-alone/plugin/js/navbar.js b/console/stand-alone/plugin/js/navbar.js new file mode 100644 index 0000000..ebda4d4 --- /dev/null +++ b/console/stand-alone/plugin/js/navbar.js @@ -0,0 +1,119 @@ +/* +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. +*/ +/** + * @module QDR + */ +var QDR = (function (QDR) { + + /** + * @property breadcrumbs + * @type {{content: string, title: string, isValid: isValid, href: string}[]} + * + * Data structure that defines the sub-level tabs for + * our plugin, used by the navbar controller to show + * or hide tabs based on some criteria + */ + QDR.breadcrumbs = [ + { + content: '<i class="icon-cogs"></i> Connect', + title: "Connect to a router", + isValid: function () { return true; }, + href: "#connect" + }, + { + content: '<i class="fa fa-home"></i> Overview', + title: "View router overview", + isValid: function (QDRService) { return QDRService.isConnected(); }, + href: "#/overview" + }, + { + content: '<i class="icon-star-empty"></i> Topology', + title: "View router network topology", + isValid: function (QDRService) { return QDRService.isConnected(); }, + href: "#/topology" + }, + { + content: '<i class="icon-list "></i> List', + title: "View router nodes as a list", + isValid: function (QDRService) { return QDRService.isConnected(); }, + href: "#/list" + }, + { + content: '<i class="icon-bar-chart"></i> Charts', + title: "View charts", + isValid: function (QDRService, $location) { return QDRService.isConnected(); }, + href: "#/charts" + }, + { + content: '<i class="icon-align-left"></i> Schema', + title: "View dispatch schema", + isValid: function (QDRService) { return QDRService.isConnected(); }, + href: "#/schema", + right: true + + } + ]; + /** + * @function NavBarController + * + * @param $scope + * @param workspace + * + * The controller for this plugin's navigation bar + * + */ + QDR.module.controller("QDR.NavBarController", ['$scope', '$sce', 'QDRService', 'QDRChartService', '$location', function($scope, $sce, QDRService, QDRChartService, $location) { + + QDR.log.debug("navbar started with location.url: " + $location.url()); + QDR.log.debug("navbar started with window.location.pathname : " + window.location.pathname); + + if ($location.path().startsWith("/topology") + && !QDRService.isConnected()) { + $location.path("/connect"); + } + + if ($location.path().startsWith("/connect") + && QDRService.isConnected()) { + $location.path("/topology"); + } + + $scope.breadcrumbs = QDR.breadcrumbs; + + $scope.isValid = function(link) { + return link.isValid(QDRService, $location); + }; + + $scope.isActive = function(href) { + return href.split("#")[1] == $location.path(); + }; + + $scope.isRight = function (link) { + return angular.isDefined(link.right); + }; + + $scope.hasChart = function (link) { + if (link.href == "#/charts") { + return QDRChartService.charts.some(function (c) { return c.dashboard }); + } + } + }]); + + return QDR; + +} (QDR || {})); http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/4940e63d/console/stand-alone/plugin/js/qdrChartService.js ---------------------------------------------------------------------- diff --git a/console/stand-alone/plugin/js/qdrChartService.js b/console/stand-alone/plugin/js/qdrChartService.js new file mode 100644 index 0000000..6535384 --- /dev/null +++ b/console/stand-alone/plugin/js/qdrChartService.js @@ -0,0 +1,956 @@ +/* +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. +*/ +/** + * @module QDR + */ +var QDR = (function(QDR) { + + // The QDR chart service handles periodic gathering data for charts and displaying the charts + QDR.module.factory("QDRChartService", ['$rootScope', 'QDRService', '$http', '$resource', + function($rootScope, QDRService, $http, $resource) { + + var instance = 0; // counter for chart instances + var bases = []; + var findBase = function (name, attr, request) { + for (var i=0; i<bases.length; ++i) { + var base = bases[i]; + if (base.equals(name, attr, request)) + return base; + } + return null; + } + + function ChartBase(name, attr, request) { + // the base chart attributes + this.name = name; // the record's "name" field + this.attr = attr; // the record's attr field to chart + this.request = request; // the associated request that fetches the data + + // copy the savable properties to an object + this.copyProps = function (o) { + o.name = this.name; + o.attr = this.attr; + this.request.copyProps(o); + } + + this.equals = function (name, attr, request) { + return (this.name == name && this.attr == attr && this.request.equals(request)); + } + }; + + // Object that represents a visible chart + // There can be multiple of these per ChartBase (eg. one rate and one value chart) + function Chart(name, attr, request) { + + var base = findBase(name, attr, request); + if (!base) { + base = new ChartBase(name, attr, request); + bases.push(base); + } + this.base = base; + this.instance = instance++; + this.dashboard = false; // is this chart on the dashboard page + this.type = "value"; // value or rate + this.rateWindow = 1000; // calculate the rate of change over this time interval. higher == smother graph + this.areaColor = "#c0e0ff"; // the chart's area color when not an empty string + this.lineColor = "#4682b4"; // the chart's line color when not an empty string + this.visibleDuration = 10; // number of minutes of data to show (<= base.duration) + this.userTitle = null; // user title overrides title() + + // generate a unique id for this chart + this.id = function () { + var key = this.request().nodeId + this.request().entity + this.name() + this.attr() + "_" + this.instance; + // remove all characters except letters,numbers, and _ + return key.replace(/[^\w]/gi, '') + } + // copy the savable properties to an object + this.copyProps = function (o) { + o.type = this.type; + o.rateWindow = this.rateWindow; + o.areaColor = this.areaColor; + o.lineColor = this.lineColor; + o.visibleDuration = this.visibleDuration; + o.userTitle = this.userTitle; + this.base.copyProps(o); + } + this.name = function (_) { + if (!arguments.length) return this.base.name; + this.base.name = _; + return this; + } + this.attr = function (_) { + if (!arguments.length) return this.base.attr; + this.base.attr = _; + return this; + } + this.nodeId = function (_) { + if (!arguments.length) return this.base.request.nodeId; + this.base.request.nodeId = _; + return this; + } + this.entity = function (_) { + if (!arguments.length) return this.base.request.entity; + this.base.request.entity = _; + return this; + } + this.request = function (_) { + if (!arguments.length) return this.base.request; + this.base.request = _; + return this; + } + this.data = function () { + return this.base.request.data(this.base.name, this.base.attr); // refernce to chart's data array + } + this.interval = function (_) { + if (!arguments.length) return this.base.request.interval; + this.base.request.interval = _; + return this; + } + this.duration = function (_) { + if (!arguments.length) return this.base.request.duration; + this.base.request.duration = _; + return this; + } + this.title = function (_) { + var name = this.request().aggregate ? 'Aggregate' : QDRService.nameFromId(this.nodeId()); + var computed = name + + " " + QDRService.humanify(this.attr()) + + " - " + this.name() + if (!arguments.length) return this.userTitle || computed; + + // don't store computed title in userTitle + if (_ === computed) + _ = null; + this.userTitle = _; + return this; + } + this.title_short = function (_) { + if (!arguments.length) return this.userTitle || this.name(); + return this; + } + this.copy = function () { + var chart = self.registerChart(this.nodeId(), this.entity(), + this.name(), this.attr(), this.interval(), true, this.base.request.aggregate); + chart.type = this.type; + chart.areaColor = this.areaColor; + chart.lineColor = this.lineColor; + chart.rateWindow = this.rateWindow; + chart.visibleDuration = this.visibleDuration; + chart.userTitle = this.userTitle; + return chart; + } + // compare to a chart + this.equals = function (c) { + return (c.instance == this.instance && + c.base.equals(this.base.name, this.base.attr, this.base.request) && + c.type == this.type && + c.rateWindow == this.rateWindow && + c.areaColor == this.areaColor && + c.lineColor == this.lineColor) + } + } + + // Object that represents the management request to fetch and store data for multiple charts + function ChartRequest(nodeId, entity, name, attr, interval, aggregate) { + this.duration = 10; // number of minutes to keep the data + this.nodeId = nodeId; // eg amqp:/_topo/0/QDR.A/$management + this.entity = entity; // eg .router.address + // sorted since the responses will always be sorted + this.aggregate = aggregate; // list of nodeIds for aggregate charts + this.datum = {}; // object containing array of arrays for each attr + // like {attr1: [[date,value],[date,value]...], attr2: [[date,value]...]} + + this.interval = interval; // number of milliseconds between updates to data + this.setTimeoutHandle = null; // used to cancel the next request + // copy the savable properties to an object + + this.data = function (name, attr) { + if (this.datum[name] && this.datum[name][attr]) + return this.datum[name][attr] + return null; + } + this.addAttrName = function (name, attr) { + if (Object.keys(this.datum).indexOf(name) == -1) { + this.datum[name] = {} + } + if (Object.keys(this.datum[name]).indexOf(attr) == -1) { + this.datum[name][attr] = []; + } + } + this.addAttrName(name, attr) + + this.copyProps = function (o) { + o.nodeId = this.nodeId; + o.entity = this.entity; + o.interval = this.interval; + o.aggregate = this.aggregate; + o.duration = this.duration; + } + + this.removeAttr = function (name, attr) { + if (this.datum[name]) { + if (this.datum[name][attr]) { + delete this.datum[name][attr] + } + } + return this.attrs().length; + } + + this.equals = function (r, entity, aggregate) { + if (arguments.length == 3) { + var o = {nodeId: r, entity: entity, aggregate: aggregate} + r = o; + } + return (this.nodeId === r.nodeId && this.entity === r.entity && this.aggregate == r.aggregate) + } + this.names = function () { + return Object.keys(this.datum) + } + this.attrs = function () { + var attrs = {} + Object.keys(this.datum).forEach( function (name) { + Object.keys(this.datum[name]).forEach( function (attr) { + attrs[attr] = 1; + }) + }, this) + return Object.keys(attrs); + } + }; + var self = { + charts: [], // list of charts to gather data for + chartRequests: [], // the management request info (multiple charts can be driven off of a single request + + init: function () { + self.loadCharts(); + }, + + findChartRequest: function (nodeId, entity, aggregate) { + var ret = null; + self.chartRequests.some( function (request) { + if (request.equals(nodeId, entity, aggregate)) { + ret = request; + return true; + } + }) + return ret; + }, + + findCharts: function (name, attr, nodeId, entity) { + return self.charts.filter( function (chart) { + return (chart.name() == name && + chart.attr() == attr && + chart.nodeId() == nodeId && + chart.entity() == entity) + }); + }, + + delChartRequest: function (request) { + for (var i=0; i<self.chartRequests.length; ++i) { + var r = self.chartRequests[i]; + if (request.equals(r)) { + QDR.log.debug("removed request: " + request.nodeId + " " + request.entity); + self.chartRequests.splice(i, 1); + self.stopCollecting(request); + return; + } + } + }, + + delChart: function (chart) { + var foundBases = 0; + for (var i=0; i<self.charts.length; ++i) { + var c = self.charts[i]; + if (c.base === chart.base) + ++foundBases; + if (c.equals(chart)) { + self.charts.splice(i, 1); + if (chart.dashboard) + self.saveCharts(); + } + } + if (foundBases == 1) { + var baseIndex = bases.indexOf(chart.base) + bases.splice(baseIndex, 1); + } + }, + + registerChart: function (nodeId, entity, name, attr, interval, forceCreate, aggregate) { + var request = self.findChartRequest(nodeId, entity, aggregate); + if (request) { + // add any new attr or name to the list + request.addAttrName(name, attr) + } else { + // the nodeId/entity did not already exist, so add a new request and chart + QDR.log.debug("added new request: " + nodeId + " " + entity); + request = new ChartRequest(nodeId, entity, name, attr, interval, aggregate); + self.chartRequests.push(request); + self.startCollecting(request); + } + var charts = self.findCharts(name, attr, nodeId, entity); + var chart; + if (charts.length == 0 || forceCreate) { + chart = new Chart(name, attr, request); + self.charts.push(chart); + } else { + chart = charts[0]; + } + return chart; + }, + + // remove the chart for name/attr + // if all attrs are gone for this request, remove the request + unRegisterChart: function (chart) { + // remove the chart + for (var i=0; i<self.charts.length; ++i) { + var c = self.charts[i]; + if (chart.equals(c)) { + var request = chart.request(); + self.delChart(chart); + if (request) { + // see if any other charts use this attr + for (var i=0; i<self.charts.length; ++i) { + var c = self.charts[i]; + if (c.attr() == chart.attr() && c.request().equals(chart.request())) + return; + } + // no other charts use this attr, so remove it + if (request.removeAttr(chart.name(), chart.attr()) == 0) { + self.stopCollecting(request); + self.delChartRequest(request); + } + } + } + } + }, + + stopCollecting: function (request) { + if (request.setTimeoutHandle) { + clearTimeout(request.setTimeoutHandle); + request.setTimeoutHandle = null; + } + }, + + startCollecting: function (request) { + // Using setTimeout instead of setInterval because the response may take longer than interval + request.setTimeoutHandle = setTimeout(self.sendChartRequest, request.interval, request); + }, + // send the request + sendChartRequest: function (request) { + // ensure the response has the name field so we can associate the response values with the correct chart + var attrs = request.attrs(); + attrs.push("name"); + + // this is called when the response is received + var saveResponse = function (nodeId, entity, response) { + //QDR.log.debug("got chart results for " + nodeId + " " + entity); + // records an array that has data for all names + var records = response.results; + if (!records) + return; + + var now = new Date(); + var cutOff = new Date(now.getTime() - request.duration * 60 * 1000); + // index of the "name" attr in the response + var nameIndex = response.attributeNames.indexOf("name"); + if (nameIndex < 0) + return; + + var names = request.names(); + // for each record returned, find the name/attr for this request and save the data with this timestamp + for (var i=0; i<records.length; ++i) { + var name = records[i][nameIndex]; + // if we want to store the values for some attrs for this name + if (names.indexOf(name) > -1) { + attrs.forEach( function (attr) { + var data = request.data(name, attr) // get a reference to the data array + if (data) { + var attrIndex = response.attributeNames.indexOf(attr) + if (request.aggregate) { + data.push([now, response.aggregates[i][attrIndex].sum, response.aggregates[i][attrIndex].detail]) + } else { + data.push([now, records[i][attrIndex]]) + } + // expire the old data + while (data[0][0] < cutOff) { + data.shift(); + } + } + }) + } + } + } + if (request.aggregate) { + var nodeList = QDRService.nodeIdList() + QDRService.getMultipleNodeInfo(nodeList, request.entity, attrs, saveResponse, request.nodeId); + } else { + QDRService.getNodeInfo(request.nodeId, request.entity, attrs, saveResponse); + } + // it is now safe to send another request + request.setTimeoutHandle = setTimeout(self.sendChartRequest, request.interval, request) + }, + + numCharts: function () { + return self.charts.length; + }, + + isAttrCharted: function (nodeId, entity, name, attr) { + var charts = self.findCharts(name, attr, nodeId, entity); + // if any of the matching charts are on the dashboard page, return true + return charts.some(function (chart) { + return (chart.dashboard) }); + }, + + addDashboard: function (chart) { + chart.dashboard = true; + self.saveCharts(); + }, + delDashboard: function (chart) { + chart.dashboard = false; + self.saveCharts(); + }, + // save the charts to local storage + saveCharts: function () { + var charts = []; + var minCharts = []; + + self.charts.forEach(function (chart) { + var minChart = {}; + // don't save chart unless it is on the dashboard + if (chart.dashboard) { + chart.copyProps(minChart); + minCharts.push(minChart); + } + }) + localStorage["QDRCharts"] = angular.toJson(minCharts); + }, + loadCharts: function () { + var charts = angular.fromJson(localStorage["QDRCharts"]); + if (charts) { + charts.forEach(function (chart) { + if (!chart.interval) + chart.interval = 1000; + if (!chart.duration) + chart.duration = 10; + if (chart.nodeList) + chart.aggregate = true; + var newChart = self.registerChart(chart.nodeId, chart.entity, chart.name, chart.attr, chart.interval, true, chart.aggregate); + newChart.dashboard = true; // we only save the dashboard charts + newChart.type = chart.type; + newChart.rateWindow = chart.rateWindow; + newChart.areaColor = chart.areaColor ? chart.areaColor : "#c0e0ff"; + newChart.lineColor = chart.lineColor ? chart.lineColor : "#4682b4"; + newChart.duration(chart.duration); + newChart.visibleDuration = chart.visibleDuration ? chart.visibleDuration : 10; + if (chart.userTitle) + newChart.title(chart.userTitle); + }) + } + }, + + AreaChart: function (chart, url) { + if (!chart) + return; + + // if this is an aggregate chart, show it stacked + var stacked = chart.request().aggregate; + this.chart = chart; // reference to underlying chart + this.svgchart = null; + if (url) + url = "/dispatch" + url; + else + url = ""; + this.url = url; + + // callback function. called by svgchart when binding data + // the variable 'this' refers to the svg and not the AreaChart, + // but since we are still in the scope of the AreaChart we have access to the passed in chart argument + this.chartData = function () { + + var now = new Date(); + var visibleDate = new Date(now.getTime() - chart.visibleDuration * 60 * 1000); + var data = chart.data(); + var nodeList = QDRService.nodeIdList(); + + if (chart.type == "rate") { + var rateData = []; + var datalen = data.length; + k = 0; // inner loop optimization + for (var i=0; i<datalen; ++i) { + var d = data[i]; + if (d[0] >= visibleDate) { + for (var j=k+1; j<datalen; ++j) { + var d1 = data[j]; + if (d1[0] - d[0] >= chart.rateWindow) { // rateWindow is the timespan to calculate rates + var elapsed = Math.max((d1[0] - d[0]) / 1000, 1); // number of seconds that elapsed + var rd = [d1[0],(d1[1] - d[1])/elapsed] + k = j; // start here next time + // this is a stacked (aggregate) chart + if (stacked) { + var detail = []; + nodeList.forEach( function (node, nodeIndex) { + if (d1[2][nodeIndex] && d[2][nodeIndex]) + detail.push({node: QDRService.nameFromId(node), val: (d1[2][nodeIndex].val- d[2][nodeIndex].val)/elapsed}) + }) + rd.push(detail) + } + rateData.push(rd); + break; + } + } + } + } + // we need at least a point to chart + if (rateData.length == 0) { + rateData[0] = [chart.data()[0][0],0,[{node:'',val:0}]]; + } + return rateData; + } + if (chart.visibleDuration != chart.duration()) { + return data.filter(function (d) { return d[0]>=visibleDate}); + } else + return data; + } + + this.zoom = function (id, zoom) { + if (this.svgchart) { + this.svgchart.attr("zoom", zoom) + d3.select('#' + id) + .data([this.chartData()]) + .call(this.svgchart) + } + } + + // called by the controller on the page that displays the chart + // called whenever the controller wants to redraw the chart + // note: the data is collected independently of how often the chart is redrawn + this.tick = function (id) { + + // can't draw charts that don't have data yet + if (this.chart.data().length == 0) { + return; + } + + // if we haven't created the svg yet + if (!this.svgchart) { + + // make sure the dom element exists on the page + var div = angular.element('#' + id); + if (!div) + return; + + var width = div.width(); + var height = div.height(); + + // make sure the dom element has a size. otherwise we wouldn't see anything anyway + if (!width) + return; + + var tooltipGenerator; + // stacked charts have a different tooltip + if (stacked) { + tooltipGenerator = function (d, color, format) { + var html = "<table class='fo-table'><tbody><tr class='fo-title'>"+ + "<td align='center' colspan='2' nowrap>Time: "+d[0].toTimeString().substring(0, 8)+"</td></tr>" + d[2].forEach( function (detail) { + html += "<tr class='detail'><td align='right' nowrap>" + + detail.node + + "<div class='fo-table-legend' style='background-color: "+color(detail.node)+"'></div>" + + "</td><td>"+format(detail.val)+"</td></tr>" + }) + html += "</tbody></table>" + return html; + } + } else { + tooltipGenerator = function (d, color, format) { + var html = "<table class='fo-table'><tbody><tr class='fo-title'>"+ + "<td align='center'>Time</td><td align='center'>Value</td></tr><tr><td>" + + d[0].toTimeString().substring(0, 8) + + "</td><td>" + + format(d[1]) + + "</td></tr></tbody></table>" + return html; + } + } + // create and initialize the chart + this.svgchart = self.timeSeriesStackedChart(id, width, height, + QDRService.humanify(this.chart.attr()), + this.chart.name(), + QDRService.nameFromId(this.chart.nodeId()), + this.chart.entity(), + stacked) + .tooltipGenerator(tooltipGenerator); + + } + // in case the chart properties have changed, set the new props + this.svgchart + .attr("type", this.chart.type) + .attr("areaColor", this.chart.areaColor) + .attr("lineColor", this.chart.lineColor) + .attr("url", this.url) + .attr("title", this.chart.userTitle); + + // bind the new data and update the chart + d3.select('#' + id) // the div id on the page/dialog + .data([this.chartData()]) + .call(this.svgchart); // the charting function + } + }, + + timeSeriesStackedChart: function (id, width, height, attrName, name, node, entity, stacked) { + var margin = {top: 20, right: 18, bottom: 10, left: 15} + // attrs that can be changed after the chart is created by using + // chart.attr(<attrname>, <attrvalue>); + var attrs = { + attrName: attrName, // like Deliveries to Container. Put at top of chart + name: name, // like router.address/qdrhello Put at bottom of chart with node + node: node, // put at bottom of chart with name + entity: entity, // like .router.address Not used atm + title: "", // user title overrides the node and name at the bottom of the chart + url: "", // needed to reference filters and clip because of angular's location service + type: "value", // value or rate + areaColor: "", // can be set for non-stacked charts + lineColor: "", // can be set for non-stacked charts + zoom: false // should the y-axis range start at 0 or the min data value + } + var width = width - margin.left - margin.right, + height = height - margin.top - margin.bottom + yAxisTransitionDuration = 0 + + var x = d3.time.scale() + var y = d3.scale.linear() + .rangeRound([height, 0]); + // The x-accessor for the path generator; xScale â xValue. + var X = function (d) { return x(d[0]) } + // The x-accessor for the path generator; yScale â yValue. + var Y = function Y(d) { return y(d[1]) } + + var xAxis = d3.svg.axis().scale(x).orient("bottom") + .outerTickSize(6) + .innerTickSize(-(height-margin.top-margin.bottom)) + .tickPadding(2) + .ticks(d3.time.minutes, 2) + var yAxis = d3.svg.axis().scale(y).orient("right") + .outerTickSize(8) + .innerTickSize(-(width-margin.left-margin.right)) + .tickPadding(10) + .ticks(3) + .tickFormat(function(d) { return formatValue(d)}) + + var tooltipGenerator = function (d, color, format) {return ""}; // should be overridden to set an appropriate tooltip + var formatValue = d3.format(".2s"); + var formatPrecise = d3.format(","); + var bisectDate = d3.bisector(function(d) { return d[0]; }).left; + var line = d3.svg.line(); + + var stack = d3.layout.stack() + .offset("zero") + .values(function (d) { return d.values; }) + .x(function (d) { return x(d.date); }) + .y(function (d) { return d.value; }); + + var area = d3.svg.area() + + if (stacked) { + area.interpolate("cardinal") + .x(function (d) { return x(d.date); }) + .y0(function (d) { return y(d.y0); }) + .y1(function (d) { return y(d.y0 + d.y); }); + } else { + area.interpolate("basis").x(X).y1(Y) + line.x(X).y(Y) + } + var color = d3.scale.category20(); + + var sv = d3.select("#"+id).append("svg") + .attr("width", width + margin.left + margin.right) + .attr("height", height + margin.top + margin.bottom) + var svg = sv + .append("g") + .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); + + var clip = svg.append("defs").append("svg:clipPath") + .attr("id", "clip") + .append("svg:rect") + .attr("id", "clip-rect") + .attr("x", "0") + .attr("y", "0") + .attr("width", width) + .attr("height", height); + + // we want all our areas to appear before the axiis + svg.append("g") + .attr("class", "section-container") + + svg.append("g") + .attr("class", "x axis") + + svg.append("g") + .attr("class", "y axis") + + svg.append("text").attr("class", "title") + .attr("x", (width / 2) - (margin.left + margin.right) / 2) + .attr("y", 0 - (margin.top / 2)) + .attr("text-anchor", "middle") + .text(attrs.attrName); + + svg.append("text").attr("class", "legend") + .attr("x", (width / 2) - (margin.left + margin.right) / 2) + .attr("y", height + (margin.bottom / 2) ) + .attr("text-anchor", "middle") + .text(!stacked ? attrs.node + " " + attrs.name : attrs.name); + + var focus = sv.append("g") + .attr("class", "focus") + .style("display", "none"); + + focus.append("circle") + .attr("r", 4.5); + + var focusg = focus.append("g"); + focusg.append("rect") + .attr("class", "mo-guide y") + .attr("width", 1) + .attr("height", height - (margin.top + margin.bottom)); + focusg.append("rect") + .attr("class", "mo-guide x") + .attr("width", width - (margin.left + margin.right)) + .attr("height", 1); + focus.append("foreignObject") + .attr('class', 'svg-tooltip') + .append("xhtml:span"); + + + function chart(selection) { + selection.each(function(data) { + + var seriesArr = [] + if (stacked) { + var detailNames = data[0][2].map(function (detail){ return detail.node }) + var revNames = angular.copy(detailNames).reverse(); + color.domain(revNames); + + var series = {}; + detailNames.forEach(function (name) { + series[name] = {name: name, values:[]}; + seriesArr.unshift(series[name]); // insert at beginning + }); + + data.forEach(function (d) { + detailNames.map(function (name, i) { + series[name].values.push({date: d[0], value: d[2][i] ? d[2][i].val : 0}); + }); + }); + + // this decorates seriesArr with x,y,and y0 properties + stack(seriesArr); + } + + var extent = d3.extent(data, function(d) {return d[0];}); + x.domain(extent) + .range([0, width - margin.left - margin.right]); + + // Update the y-scale. + var min = attrs.zoom ? 0 : d3.min(data, function(d) {return d[1]}); + var max = d3.max(data, function(d) {return d[1]}); + var mean = d3.mean(data, function(d) {return d[1]}); + //max = max * 1.01; + var diff = (max - min); + if (diff == 0) { + max = max + 1; + diff = 1; + } + var ratio = mean != 0 ? diff / mean : 1; + if (ratio < .05) + formatValue = d3.format(".3s") + + if (stacked) { + y.domain([min, max]) + .range([height - margin.top - margin.bottom, 0]); + } else { + y + .domain([min, max]) + .range([height - margin.top - margin.bottom, 0]); + } + if (attrs.type == "rate") { + area.interpolate("basis"); // rate charts look better smoothed + line.interpolate("basis"); + } + else { + area.interpolate("linear"); // don't smooth value charts + line.interpolate("linear"); + } + + // adjust the xaxis based on the range of x values (domain) + var timeSpan = (extent[1] - extent[0]) / (1000 * 60); // number of minutes + if (timeSpan < 1.5) + xAxis.ticks(d3.time.seconds, 10); + else if (timeSpan < 3) + xAxis.ticks(d3.time.seconds, 30); + else if (timeSpan < 8) + xAxis.ticks(d3.time.minutes, 1); + else + xAxis.ticks(d3.time.minutes, 2); + + // adjust the number of yaxis ticks based on the range of y values + if (formatValue(min) === formatValue(max)) + yAxis.ticks(2); + + var container = svg.select('.section-container'); + container.selectAll('.series').remove(); + if (stacked) { + y.domain([Math.min(min, 0), d3.max(seriesArr, function (c) { + return d3.max(c.values, function (d) { return d.y0 + d.y; }); + })]); + + // creates a .series g path for each section in the detail + // since we don't get more sections this selection is only run once + var series = container.selectAll(".series") + .data(seriesArr) + + series.enter().append("g") + .attr("class", "series") + .append("path") + .attr("class", "streamPath") + .style("fill", function (d) { return color(d.name); }) + .style("stroke", "grey"); + + series.exit().remove() + + //series.exit().remove() + // each time the data is updated, update each section + container.selectAll(".series .streamPath").data(seriesArr) + .attr("d", function (d) { return area(d.values); }) + } else { + var series = container.selectAll(".series") + .data([data], function(d) { return d; }) + + series.enter().append("g") + .append("path") + .attr("class", "area") + series.enter().append("path") + .attr("class", "line") + + series.exit().remove() + + // Update the area path. + container.select(".area").data([data]) + .attr("d", area.y0(y.range()[0])) + .style("fill", attrs.areaColor); + + //Update the line path. + container.select(".line").data([data]) + .attr("d", line) + .style("stroke", attrs.lineColor) + } + // Update the x-axis. + svg.select(".x.axis") + .attr("transform", "translate(0," + (height - margin.top - margin.bottom + 1) + ")") + .call(xAxis); + + svg.select(".y.axis") + .transition().duration(yAxisTransitionDuration) // animate the y axis + .attr("transform", "translate(" + (width - margin.right - margin.left) + ",0)") + .call(yAxis); + yAxisTransitionDuration = 1000 // only do a transition after the chart is 1st drawn + + // TODO: fix this + // need to recreate this every update... not sure why + var overlay = sv.select(".overlay"); + if (!overlay.empty()) + overlay.remove(); + sv.append("rect") + .attr("class", "overlay") + .attr("width", width) + .attr("height", height) + .on("mouseover", function () {focus.style("display", null)}) + .on("mouseout", function () {focus.style("display", "none")}) + .on("mousemove", mousemove) + + function mousemove() { + var x0 = x.invert(d3.mouse(this)[0] - margin.left); + var i = bisectDate(data, x0, 1); + if (i < data.length && i > 0) { + var d0 = data[i - 1]; + var d1 = data[i]; + // set d to the data that is closest to the mouse position + var d = x0 - d0[0] > d1[0] - x0 ? d1 : d0; + focus.attr("transform", "translate(" + (x(d[0]) + margin.left) + "," + (y(d[1]) + margin.top) + ")"); + + var tipFormat = formatPrecise; + if (attrs.type === "rate") + tipFormat = d3.format(".2n") + // set the tooltip html and position it + focus.select('.svg-tooltip span') + .html(tooltipGenerator(d, color, tipFormat)) + + var foBounds = focus.select('table')[0][0].getBoundingClientRect(); + var mx = x(d[0]); // mouse x + var my = y(d[1]); // mouse y + + // perfer to put the tooltip in the nw corner relative to the focus circle + var foy = -foBounds.height; + var fox = -foBounds.width; + // off the left side + if (mx - foBounds.width - margin.left < 0) + fox = 0; + // above the top + if (my - foBounds.height - margin.top < 0) + foy = 0; + // won't fit above or below, just put it at bottom + if (my + foBounds.height > height) + foy = -(foBounds.height - (height - my)); + + focus.select('.svg-tooltip') + .attr('x', fox).attr('y', foy); + + // position the guide lines + focus.select(".mo-guide.y") + .attr("y", -my); + focus.select(".mo-guide.x") + .attr("x", -mx); + + } else { + focus.attr("transform", "translate(-10,-10)"); + } + } + + }) + + + } + chart.attr = function (attrName, value) { + if (arguments.length < 2) + return arguments.length == 1 ? attrs[attrName] : chart; + if (angular.isDefined(attrs[attrName])) + attrs[attrName] = value; + return chart; + } + chart.tooltipGenerator = function (_) { + tooltipGenerator = _; + return chart; + } + + return chart; + } + } + return self; + }]); + + return QDR; +}(QDR || {})); http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/4940e63d/console/stand-alone/plugin/js/qdrCharts.js ---------------------------------------------------------------------- diff --git a/console/stand-alone/plugin/js/qdrCharts.js b/console/stand-alone/plugin/js/qdrCharts.js new file mode 100644 index 0000000..e484b07 --- /dev/null +++ b/console/stand-alone/plugin/js/qdrCharts.js @@ -0,0 +1,230 @@ +/* +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. +*/ +/** + * @module QDR + */ +/** + * @module QDR + */ +var QDR = (function (QDR) { + + /** + * @method ChartsController + * @param $scope + * @param QDRServer + * @param QDRChartServer + * + * Controller that handles the QDR charts page + */ + QDR.module.controller("QDR.ChartsController", ['$scope', 'QDRService', 'QDRChartService', '$uibModal', '$location', + function($scope, QDRService, QDRChartService, $uibModal, $location) { + var updateTimer = null; + + QDR.log.debug("started Charts controller"); + if (!angular.isDefined(QDRService.schema)) + return; + + $scope.svgCharts = []; + // create an svg object for each chart + QDRChartService.charts.filter(function (chart) {return chart.dashboard}).forEach(function (chart) { + var svgChart = new QDRChartService.AreaChart(chart, $location.$$path) + svgChart.zoomed = false; + $scope.svgCharts.push(svgChart); + }) + + // redraw the charts every second + var updateCharts = function () { + $scope.svgCharts.forEach(function (svgChart) { + svgChart.tick(svgChart.chart.id()); // on this page we are using the chart.id() as the div id in which to render the chart + }) + updateHandle = setTimeout(updateCharts, 1100); + } + $scope.chartsLoaded = function () { + setTimeout(updateCharts, 0); + } + + $scope.zoomChart = function (chart) { + chart.zoomed = !chart.zoomed; + chart.zoom(chart.chart.id(), chart.zoomed); + } + $scope.showListPage = function () { + $location.path("/list"); + }; + + $scope.hasCharts = function () { + return QDRChartService.numCharts() > 0; + }; + + $scope.editChart = function (chart) { + doDialog("chart-config-template.html", chart.chart); + }; + + $scope.delChart = function (chart) { + QDRChartService.unRegisterChart(chart.chart); + // remove from svgCharts + $scope.svgCharts.forEach(function (svgChart, i) { + if (svgChart === chart) { + delete $scope.svgCharts.splice(i, 1); + } + }) + }; + + // called from dialog when we want to clone the dialog chart + // the chart argument here is a QDRChartService chart + $scope.addChart = function (chart) { + $scope.svgCharts.push(new QDRChartService.AreaChart(chart, $location.$$path)); + }; + + $scope.$on("$destroy", function( event ) { + if (updateTimer) { + cancelTimer(updateTimer); + updateTimer = null; + } + for (var i=$scope.svgCharts.length-1; i>=0; --i) { + delete $scope.svgCharts.splice(i, 1); + } + }); + + function doDialog(template, chart) { + + var modalInstance = $uibModal.open({ + animation: true, + templateUrl: template, + controller: 'QDR.ChartDialogController', + resolve: { + chart: function () { + return chart; + }, + dashboard: function () { + return $scope; + } + } + }); + }; + + }]); + + QDR.module.controller("QDR.ChartDialogController", function($scope, QDRChartService, $location, $uibModalInstance, $rootScope, chart, dashboard) { + var dialogSvgChart = null; + $scope.svgDivId = "dialogChart"; // the div id for the svg chart + + $scope.updateTimer = null; + $scope.chart = chart; // the underlying chart object from the dashboard + $scope.dialogChart = $scope.chart.copy(); // the chart object for this dialog + $scope.userTitle = $scope.chart.title(); + + $scope.$watch('userTitle', function(newValue, oldValue) { + if (newValue !== oldValue) { + $scope.dialogChart.title(newValue); + } + }) + // the stored rateWindow is in milliseconds, but the slider is in seconds + $scope.rateWindow = $scope.chart.rateWindow / 1000; + + var cleanup = function () { + if ($scope.updateTimer) { + clearTimeout($scope.updateTimer); + $scope.updateTimer = null; + } + QDRChartService.unRegisterChart($scope.dialogChart); // remove the chart + } + $scope.okClick = function () { + cleanup(); + $uibModalInstance.close(); + }; + + // initialize the rateWindow slider + $scope.slider = { + 'options': { + min: 1, + max: 10, + step: 1, + tick: true, + stop: function (event, ui) { + $scope.dialogChart.rateWindow = ui.value * 1000; + if (dialogSvgChart) + dialogSvgChart.tick($scope.svgDivId); + } + } + }; + + $scope.visibleDuration = + $scope.duration = { + 'options': { + min: 1, + max: 10, + step: 1, + tick: true, + stop: function (event, ui) { + if (dialogSvgChart) + dialogSvgChart.tick($scope.svgDivId); + } + } + }; + + // handle the Apply button click + // update the dashboard chart's properties + $scope.apply = function () { + $scope.chart.areaColor = $scope.dialogChart.areaColor; + $scope.chart.lineColor = $scope.dialogChart.lineColor; + $scope.chart.type = $scope.dialogChart.type; + $scope.chart.rateWindow = $scope.dialogChart.rateWindow; + $scope.chart.title($scope.dialogChart.title()); + $scope.chart.visibleDuration = $scope.dialogChart.visibleDuration; + QDRChartService.saveCharts(); + } + + // add a new chart to the dashboard based on the current dialog settings + $scope.copyToDashboard = function () { + var chart = $scope.dialogChart.copy(); + // set the new chart's dashboard state + QDRChartService.addDashboard(chart); + // notify the chart controller that it needs to display a new chart + dashboard.addChart(chart); + } + + // update the chart on the popup dialog + var updateDialogChart = function () { + // draw the chart using the current data + if (dialogSvgChart) + dialogSvgChart.tick($scope.svgDivId); + + // draw the chart again in 1 second + $scope.updateTimer = setTimeout(updateDialogChart, 1000); + } + + var showChart = function () { + // ensure the div for our chart is loaded in the dom + var div = angular.element("#dialogChart"); + if (!div.width()) { + setTimeout(showChart, 100); + return; + } + dialogSvgChart = new QDRChartService.AreaChart($scope.dialogChart, $location.$$path); + updateDialogChart(); + } + showChart(); + + + }); + + return QDR; + +}(QDR || {})); + --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
