├── .editorconfig ├── .gitattributes ├── .gitignore ├── .jshintrc ├── .travis.yml ├── CONTRIBUTING.md ├── Gruntfile.js ├── LICENSE ├── README.md ├── bower.json ├── demo ├── index.html ├── scripts │ └── demo.js ├── styles │ └── demo.css └── views │ └── view.html ├── dist └── malhar-angular-widgets.js ├── docs └── AngularJSDashboard.png ├── karma-e2e.conf.js ├── karma.conf.js ├── package.json ├── src ├── models │ ├── random.js │ └── websocket.js ├── modules.js ├── service │ ├── visibility.js │ └── websocket.js ├── vendor │ ├── gauge_vendor.js │ └── visibly.js └── widgets │ ├── barChart │ └── barChart.js │ ├── gauge │ └── gauge.js │ ├── historicalChart │ └── historicalChart.js │ ├── lineChart │ └── lineChart.js │ ├── metricsChart │ └── metricsChart.js │ ├── nvd3LineChart │ └── nvd3LineChart.js │ ├── pieChart │ └── pieChart.js │ ├── random │ └── random.js │ ├── scopeWatch │ └── scopeWatch.js │ ├── select │ └── select.js │ ├── time │ └── time.js │ └── topN │ └── topN.js ├── template └── widgets │ ├── barChart │ └── barChart.html │ ├── historicalChart │ └── historicalChart.html │ ├── metricsChart │ └── metricsChart.html │ ├── nvd3LineChart │ └── nvd3LineChart.html │ ├── pieChart │ └── pieChart.html │ ├── random │ └── random.html │ ├── scopeWatch │ └── scopeWatch.html │ ├── select │ └── select.html │ ├── time │ └── time.html │ └── topN │ └── topN.html └── test ├── .jshintrc ├── runner.html └── spec ├── webSocket_test.js └── widgets └── main.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # Change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # We recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .tmp 3 | .sass-cache 4 | .idea 5 | .DS_Store 6 | bower_components 7 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": true, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "regexp": true, 16 | "undef": true, 17 | "unused": true, 18 | "strict": true, 19 | "trailing": true, 20 | "smarttabs": true, 21 | "globals": { 22 | "angular": false, 23 | "jQuery": false, 24 | "_": false, 25 | "d3": false, 26 | "google": false, 27 | "Gauge": false 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | before_script: 5 | - 'npm install -g bower grunt-cli' 6 | - 'bower install' 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing Guidelines 2 | ======================= 3 | This project welcomes new contributors. 4 | 5 | Licensing 6 | --------- 7 | You acknowledge that your submissions to DataTorrent on this repository are made pursuant the terms of the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0.html) and constitute "Contributions," as defined therein, and you represent and warrant that you have the right and authority to do so. 8 | 9 | When adding **new javascript files**, please include the following Apache v2.0 license header at the top of the file, with the fields enclosed by brackets "[]" replaced with your own identifying information. **(Don't include the brackets!)**: 10 | 11 | ```JavaScript 12 | /* 13 | * Copyright (c) [XXXX] [NAME OF COPYRIGHT OWNER] 14 | * 15 | * Licensed under the Apache License, Version 2.0 (the "License"); 16 | * you may not use this file except in compliance with the License. 17 | * You may obtain a copy of the License at 18 | * 19 | * http://www.apache.org/licenses/LICENSE-2.0 20 | * 21 | * Unless required by applicable law or agreed to in writing, software 22 | * distributed under the License is distributed on an "AS IS" BASIS, 23 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 24 | * See the License for the specific language governing permissions and 25 | * limitations under the License. 26 | */ 27 | ``` -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (grunt) { 4 | require('load-grunt-tasks')(grunt); 5 | require('time-grunt')(grunt); 6 | 7 | grunt.initConfig({ 8 | ngtemplates: { 9 | dashboard: { 10 | options: { 11 | module: 'ui.widgets' 12 | }, 13 | src: 'template/widgets/{,*/}*.html', 14 | dest: 'template/templates.js' 15 | } 16 | }, 17 | karma: { 18 | unit: { 19 | configFile: 'karma.conf.js', 20 | singleRun: true 21 | } 22 | }, 23 | concat: { 24 | dist: { 25 | src: [ 26 | 'src/modules.js', 27 | 'src/**/*.js', 28 | 'template/templates.js' 29 | ], 30 | dest: 'dist/malhar-angular-widgets.js' 31 | } 32 | }, 33 | watch: { 34 | files: [ 35 | 'src/**/*.js', 36 | 'template/widgets/{,*/}*.html' 37 | ], 38 | tasks: ['ngtemplates', 'concat'], 39 | livereload: { 40 | options: { 41 | livereload: '<%= connect.options.livereload %>' 42 | }, 43 | files: [ 44 | 'demo/{,*/}*.html', 45 | 'demo/{,*/}*.css', 46 | 'demo/{,*/}*.js', 47 | 'dist/*.js' 48 | ] 49 | } 50 | }, 51 | jshint: { 52 | options: { 53 | jshintrc: '.jshintrc', 54 | ignores: [ 55 | 'src/vendor/{,*/}*.js' 56 | ] 57 | }, 58 | all: [ 59 | 'Gruntfile.js', 60 | 'demo/*.js', 61 | 'src/**/*.js' 62 | ] 63 | }, 64 | copy: { 65 | dist: { 66 | files: [{ 67 | expand: true, 68 | flatten: true, 69 | src: ['src/angular-ui-dashboard.css'], 70 | dest: 'dist' 71 | }] 72 | } 73 | }, 74 | clean: { 75 | dist: { 76 | files: [{ 77 | src: [ 78 | 'dist/*' 79 | ] 80 | }] 81 | }, 82 | templates: { 83 | src: ['<%= ngtemplates.dashboard.dest %>'] 84 | } 85 | }, 86 | connect: { 87 | options: { 88 | port: 9001, 89 | // Change this to '0.0.0.0' to access the server from outside. 90 | hostname: 'localhost', 91 | livereload: 35739 92 | }, 93 | livereload: { 94 | options: { 95 | open: true, 96 | base: [ 97 | '.', 98 | 'demo' 99 | ] 100 | } 101 | } 102 | } 103 | }); 104 | 105 | grunt.registerTask('test', [ 106 | 'ngtemplates', 107 | 'karma' 108 | ]); 109 | 110 | grunt.registerTask('demo', [ 111 | 'connect:livereload', 112 | 'watch' 113 | ]); 114 | 115 | grunt.registerTask('default', [ 116 | 'clean:dist', 117 | 'jshint', 118 | 'ngtemplates', 119 | 'karma', 120 | 'concat', 121 | 'clean:templates' 122 | ]); 123 | }; 124 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2014 DataTorrent, Inc. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | angular-widgets 2 | ================= 3 | 4 | [![Build Status](https://travis-ci.org/DataTorrent/malhar-angular-widgets.svg?branch=master)](https://travis-ci.org/DataTorrent/malhar-angular-widgets) 5 | 6 | Widget (Line Chart, Bar Chart, Top N and more). 7 | 8 | [Online Demo](http://datatorrent.github.io/malhar-dashboard-webapp/#/) using the library [[source code](https://github.com/DataTorrent/malhar-dashboard-webapp)] 9 | 10 | ![AngularJS Dashboard](docs/AngularJSDashboard.png "AngularJS Dashboard") 11 | 12 | ## Running Demo Application 13 | Install dependencies: 14 | 15 | ``` bash 16 | $ npm install 17 | ``` 18 | 19 | Install Bower dependencies: 20 | 21 | ``` bash 22 | $ bower install 23 | ``` 24 | 25 | Run demo server: 26 | 27 | ``` bash 28 | $ grunt demo 29 | ``` 30 | 31 | Application will be available at http://localhost:9001 32 | 33 | ## Building Application 34 | 35 | Application is built with Grunt. 36 | 37 | ``` bash 38 | $ npm install -g grunt-cli 39 | $ grunt 40 | ``` 41 | 42 | ## Contributing 43 | 44 | This project welcomes new contributors. 45 | 46 | You acknowledge that your submissions to DataTorrent on this repository are made pursuant the terms of the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0.html) and constitute "Contributions," as defined therein, and you represent and warrant that you have the right and authority to do so. 47 | 48 | When **adding new javascript files**, please prepend the Apache v2.0 license header, which can be found in [CONTRIBUTING.md file](https://github.com/DataTorrent/malhar-angular-widgets/blob/master/CONTRIBUTING.md). 49 | 50 | ## Links 51 | 52 | [malhar-angular-dashboard](https://github.com/DataTorrent/malhar-angular-dashboard) AngularJS Dashboard directive. 53 | 54 | [malhar-dashboard-webapp](https://github.com/DataTorrent/malhar-dashboard-webapp) Demo using dashboard and widget library 55 | 56 | [Node.js](http://nodejs.org/) Software platform built on JavaScript runtime 57 | 58 | [AngularJS](http://angularjs.org/) JavaScript framework 59 | 60 | [ui-sortable](https://github.com/angular-ui/ui-sortable) AngularJS UI Sortable 61 | 62 | [Bower](http://bower.io/) Package manager for the web 63 | 64 | [Grunt](http://gruntjs.com/) JavaScript Task Runner 65 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "malhar-angular-widgets", 3 | "version": "0.2.0", 4 | "license": "Apache License, v2.0", 5 | "dependencies": { 6 | "angular": "~1.3", 7 | "jquery": "~2.0.3", 8 | "angular-sanitize": "~1.3", 9 | "bootstrap": "~3.0.3", 10 | "angular-bootstrap": "0.11.0", 11 | "angular-route": "~1.3", 12 | "malhar-angular-table": ">=1.2.0", 13 | "d3": "3.4.4", 14 | "nvd3": "~1.1.15-beta", 15 | "angularjs-nvd3-directives": "~0.0.7", 16 | "malhar-angular-dashboard": "https://github.com/DataTorrent/malhar-angular-dashboard.git#master", 17 | "visibilityjs": "~1.2.1", 18 | "pines-notify": "~2.0.1", 19 | "angular-pines-notify": "~1.0.0" 20 | }, 21 | "devDependencies": { 22 | "angular-mocks": "~1.3", 23 | "angular-scenario": "~1.3" 24 | }, 25 | "resolutions": { 26 | "jquery": "~2.0.3", 27 | "d3": "3.4.4", 28 | "angular": ">=1.3", 29 | "angular-bootstrap": "0.12.x", 30 | "bootstrap": "~3.3.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /demo/scripts/demo.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 DataTorrent, Inc. ALL Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | angular.module('app', [ 20 | 'ngRoute', 21 | 'ui.dashboard', 22 | 'ui.widgets', 23 | 'ui.models' 24 | ]) 25 | .config(function ($routeProvider) { 26 | $routeProvider 27 | .when('/', { 28 | templateUrl: 'views/view.html', 29 | controller: 'DemoCtrl' 30 | }) 31 | .otherwise({ 32 | redirectTo: '/' 33 | }); 34 | }) 35 | .controller('DemoCtrl', function ($scope, $interval, RandomTopNDataModel, RandomTimeSeriesDataModel, 36 | RandomMinutesDataModel, RandomNVD3TimeSeriesDataModel, RandomMetricsTimeSeriesDataModel) { 37 | var widgetDefinitions = [ 38 | { 39 | name: 'time', 40 | directive: 'wt-time', 41 | style: { 42 | width: '33%' 43 | } 44 | }, 45 | { 46 | name: 'random', 47 | directive: 'wt-random', 48 | style: { 49 | width: '33%' 50 | } 51 | }, 52 | { 53 | name: 'scope-watch', 54 | directive: 'wt-scope-watch', 55 | attrs: { 56 | value: 'randomValue' 57 | }, 58 | style: { 59 | width: '34%' 60 | } 61 | }, 62 | { 63 | name: 'Metrics Chart', 64 | directive: 'wt-metrics-chart', 65 | dataAttrName: 'data', 66 | dataModelType: RandomMetricsTimeSeriesDataModel, 67 | style: { 68 | width: '50%' 69 | } 70 | }, 71 | { 72 | name: 'NVD3 Line Chart', 73 | directive: 'wt-nvd3-line-chart', 74 | dataAttrName: 'data', 75 | dataModelType: RandomNVD3TimeSeriesDataModel, 76 | dataModelArgs: { 77 | rate: 40 78 | }, 79 | attrs: { 80 | 'show-time-range': true 81 | }, 82 | style: { 83 | width: '50%' 84 | } 85 | }, 86 | { 87 | name: 'Line Chart', 88 | directive: 'wt-line-chart', 89 | dataAttrName: 'chart', 90 | dataModelType: RandomTimeSeriesDataModel, 91 | style: { 92 | width: '50%' 93 | } 94 | }, 95 | { 96 | name: 'Bar Chart', 97 | directive: 'wt-bar-chart', 98 | dataAttrName: 'data', 99 | dataModelType: RandomMinutesDataModel, 100 | dataModelArgs: { 101 | limit: 1000 102 | }, 103 | style: { 104 | width: '50%' 105 | } 106 | }, 107 | { 108 | name: 'topN', 109 | directive: 'wt-top-n', 110 | dataAttrName: 'data', 111 | dataModelType: RandomTopNDataModel 112 | }, 113 | { 114 | name: 'gauge', 115 | directive: 'wt-gauge', 116 | attrs: { 117 | value: 'percentage' 118 | }, 119 | style: { 120 | width: '250px' 121 | } 122 | }, 123 | ]; 124 | 125 | var defaultWidgets = [ 126 | { name: 'time' }, 127 | { name: 'random' }, 128 | { name: 'scope-watch' }, 129 | { name: 'Metrics Chart' }, 130 | { name: 'NVD3 Line Chart' }, 131 | { name: 'Line Chart' }, 132 | { name: 'Bar Chart' }, 133 | { name: 'topN' }, 134 | { name: 'gauge' } 135 | ]; 136 | 137 | $scope.dashboardOptions = { 138 | widgetButtons: true, 139 | widgetDefinitions: widgetDefinitions, 140 | defaultWidgets: defaultWidgets 141 | }; 142 | 143 | // random scope value (scope-watch widget) 144 | $interval(function () { 145 | $scope.randomValue = Math.random(); 146 | }, 500); 147 | 148 | // percentage (gauge widget, progressbar widget) 149 | $scope.percentage = 5; 150 | $interval(function () { 151 | $scope.percentage = ($scope.percentage + 10) % 100; 152 | }, 1000); 153 | 154 | // line chart widget 155 | $interval(function () { 156 | $scope.topN = _.map(_.range(0, 10), function (index) { 157 | return { 158 | name: 'item' + index, 159 | value: Math.floor(Math.random() * 100) 160 | }; 161 | }); 162 | }, 500); 163 | }); 164 | 165 | -------------------------------------------------------------------------------- /demo/styles/demo.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 15px; 3 | } 4 | 5 | .top-n .grid { 6 | height: 350px; 7 | border: 1px solid rgb(212, 212, 212); 8 | } 9 | 10 | .line-chart, .bar-chart { 11 | height: 220px 12 | } -------------------------------------------------------------------------------- /demo/views/view.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
-------------------------------------------------------------------------------- /dist/malhar-angular-widgets.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 DataTorrent, Inc. ALL Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | angular.module('ui.widgets', ['datatorrent.mlhrTable', 'nvd3ChartDirectives']); 18 | angular.module('ui.websocket', ['ui.visibility', 'ui.notify']); 19 | angular.module('ui.models', ['ui.visibility', 'ui.websocket']); 20 | 21 | /* 22 | * Copyright (c) 2014 DataTorrent, Inc. ALL Rights Reserved. 23 | * 24 | * Licensed under the Apache License, Version 2.0 (the "License"); 25 | * you may not use this file except in compliance with the License. 26 | * You may obtain a copy of the License at 27 | * 28 | * http://www.apache.org/licenses/LICENSE-2.0 29 | * 30 | * Unless required by applicable law or agreed to in writing, software 31 | * distributed under the License is distributed on an "AS IS" BASIS, 32 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 33 | * See the License for the specific language governing permissions and 34 | * limitations under the License. 35 | */ 36 | 37 | 'use strict'; 38 | 39 | angular.module('ui.models') 40 | .factory('RandomBaseDataModel', function (WidgetDataModel, Visibility) { 41 | function RandomBaseDataModel() { 42 | } 43 | 44 | RandomBaseDataModel.prototype = Object.create(WidgetDataModel.prototype); 45 | RandomBaseDataModel.prototype.constructor = WidgetDataModel; 46 | 47 | angular.extend(RandomBaseDataModel.prototype, { 48 | init: function () { 49 | this.stopUpdates = false; 50 | this.visibilityListener = Visibility.change(function (e, state) { 51 | if (state === 'hidden') { 52 | this.stopUpdates = true; 53 | } else { 54 | this.stopUpdates = false; 55 | } 56 | }.bind(this)); 57 | }, 58 | 59 | updateScope: function (data) { 60 | if (!this.stopUpdates) { 61 | WidgetDataModel.prototype.updateScope.call(this, data); 62 | } 63 | }, 64 | 65 | destroy: function () { 66 | WidgetDataModel.prototype.destroy.call(this); 67 | Visibility.unbind(this.visibilityListener); 68 | } 69 | }); 70 | 71 | return RandomBaseDataModel; 72 | }) 73 | .factory('RandomPercentageDataModel', function (RandomBaseDataModel, $interval) { 74 | function RandomPercentageDataModel() { 75 | } 76 | 77 | RandomPercentageDataModel.prototype = Object.create(RandomBaseDataModel.prototype); 78 | 79 | RandomPercentageDataModel.prototype.init = function () { 80 | var value = 50; 81 | 82 | this.intervalPromise = $interval(function () { 83 | value += Math.random() * 40 - 20; 84 | value = value < 0 ? 0 : value > 100 ? 100 : value; 85 | 86 | this.updateScope(value); 87 | }.bind(this), 500); 88 | }; 89 | 90 | RandomPercentageDataModel.prototype.destroy = function () { 91 | RandomBaseDataModel.prototype.destroy.call(this); 92 | $interval.cancel(this.intervalPromise); 93 | }; 94 | 95 | return RandomPercentageDataModel; 96 | }) 97 | .factory('RandomTopNDataModel', function (RandomBaseDataModel, $interval) { 98 | function RandomTopNDataModel() { 99 | } 100 | 101 | RandomTopNDataModel.prototype = Object.create(RandomBaseDataModel.prototype); 102 | 103 | RandomTopNDataModel.prototype.init = function () { 104 | this.intervalPromise = $interval(function () { 105 | var topTen = _.map(_.range(0, 10), function (index) { 106 | return { 107 | name: 'item' + index, 108 | value: Math.floor(Math.random() * 100) 109 | }; 110 | }); 111 | this.updateScope(topTen); 112 | }.bind(this), 500); 113 | }; 114 | 115 | RandomTopNDataModel.prototype.destroy = function () { 116 | RandomBaseDataModel.prototype.destroy.call(this); 117 | $interval.cancel(this.intervalPromise); 118 | }; 119 | 120 | return RandomTopNDataModel; 121 | }) 122 | .factory('RandomBaseTimeSeriesDataModel', function (RandomBaseDataModel, $interval) { 123 | function RandomTimeSeriesDataModel(options) { 124 | this.upperBound = (options && options.upperBound) ? options.upperBound : 100; 125 | this.rate = (options && options.rate) ? options.rate : Math.round(this.upperBound / 2); 126 | } 127 | 128 | RandomTimeSeriesDataModel.prototype = Object.create(RandomBaseDataModel.prototype); 129 | RandomTimeSeriesDataModel.prototype.constructor = RandomBaseDataModel; 130 | 131 | angular.extend(RandomTimeSeriesDataModel.prototype, { 132 | init: function () { 133 | RandomBaseDataModel.prototype.init.call(this); 134 | 135 | var max = 30; 136 | var upperBound = this.upperBound; 137 | var data = []; 138 | var chartValue = Math.round(upperBound / 2); 139 | var rate = this.rate; 140 | 141 | function nextValue() { 142 | chartValue += Math.random() * rate - rate / 2; 143 | chartValue = chartValue < 0 ? 0 : chartValue > upperBound ? upperBound : chartValue; 144 | return Math.round(chartValue); 145 | } 146 | 147 | var now = Date.now(); 148 | for (var i = max - 1; i >= 0; i--) { 149 | data.push({ 150 | timestamp: now - i * 1000, 151 | value: nextValue() 152 | }); 153 | } 154 | 155 | this.updateScope(data); 156 | 157 | this.intervalPromise = $interval(function () { 158 | if (data.length >= max) { 159 | data.shift(); 160 | } 161 | data.push({ 162 | timestamp: Date.now(), 163 | value: nextValue() 164 | }); 165 | 166 | this.updateScope(data); 167 | }.bind(this), 1000); 168 | }, 169 | 170 | destroy: function () { 171 | RandomBaseDataModel.prototype.destroy.call(this); 172 | $interval.cancel(this.intervalPromise); 173 | } 174 | }); 175 | 176 | return RandomTimeSeriesDataModel; 177 | }) 178 | .factory('RandomTimeSeriesDataModel', function (RandomBaseTimeSeriesDataModel) { 179 | function RandomTimeSeriesDataModel(options) { 180 | RandomBaseTimeSeriesDataModel.call(this, options); 181 | } 182 | 183 | RandomTimeSeriesDataModel.prototype = Object.create(RandomBaseTimeSeriesDataModel.prototype); 184 | 185 | angular.extend(RandomTimeSeriesDataModel.prototype, { 186 | updateScope: function (data) { 187 | var chart = { 188 | data: data, 189 | max: 30, 190 | chartOptions: { 191 | vAxis: {} 192 | } 193 | }; 194 | 195 | RandomBaseTimeSeriesDataModel.prototype.updateScope.call(this, chart); 196 | } 197 | }); 198 | 199 | return RandomTimeSeriesDataModel; 200 | }) 201 | .factory('RandomMetricsTimeSeriesDataModel', function (RandomBaseTimeSeriesDataModel) { 202 | function RandomMetricsTimeSeriesDataModel(options) { 203 | RandomBaseTimeSeriesDataModel.call(this, options); 204 | } 205 | 206 | RandomMetricsTimeSeriesDataModel.prototype = Object.create(RandomBaseTimeSeriesDataModel.prototype); 207 | 208 | angular.extend(RandomMetricsTimeSeriesDataModel.prototype, { 209 | updateScope: function (data) { 210 | var chart = [ 211 | { 212 | key: 'Stream1', 213 | values: data 214 | }, 215 | { 216 | key: 'Stream2', 217 | values: _.map(data, function (item) { 218 | return { timestamp: item.timestamp, value: item.value + 10 }; 219 | }) 220 | } 221 | ]; 222 | 223 | RandomBaseTimeSeriesDataModel.prototype.updateScope.call(this, chart); 224 | } 225 | }); 226 | 227 | return RandomMetricsTimeSeriesDataModel; 228 | }) 229 | .factory('RandomNVD3TimeSeriesDataModel', function (RandomBaseTimeSeriesDataModel) { 230 | function RandomTimeSeriesDataModel(options) { 231 | RandomBaseTimeSeriesDataModel.call(this, options); 232 | } 233 | 234 | RandomTimeSeriesDataModel.prototype = Object.create(RandomBaseTimeSeriesDataModel.prototype); 235 | 236 | angular.extend(RandomTimeSeriesDataModel.prototype, { 237 | updateScope: function (data) { 238 | var chart = [ 239 | { 240 | key: 'Data', 241 | values: data 242 | } 243 | ]; 244 | 245 | RandomBaseTimeSeriesDataModel.prototype.updateScope.call(this, chart); 246 | } 247 | }); 248 | 249 | return RandomTimeSeriesDataModel; 250 | }) 251 | .factory('RandomMinutesDataModel', function (RandomBaseDataModel, $interval) { 252 | function RandomTimeSeriesDataModel(options) { 253 | this.limit = (options && options.limit) ? options.limit : 500; 254 | } 255 | 256 | RandomTimeSeriesDataModel.prototype = Object.create(RandomBaseDataModel.prototype); 257 | 258 | RandomTimeSeriesDataModel.prototype.init = function () { 259 | this.generateChart(); 260 | this.intervalPromise = $interval(this.generateChart.bind(this), 2000); 261 | }; 262 | 263 | RandomTimeSeriesDataModel.prototype.generateChart = function () { 264 | var minuteCount = 30; 265 | var data = []; 266 | var limit = this.limit; 267 | var chartValue = limit / 2; 268 | 269 | function nextValue() { 270 | chartValue += Math.random() * (limit * 0.4) - (limit * 0.2); 271 | chartValue = chartValue < 0 ? 0 : chartValue > limit ? limit : chartValue; 272 | return chartValue; 273 | } 274 | 275 | var now = Date.now(); 276 | 277 | for (var i = minuteCount - 1; i >= 0; i--) { 278 | data.push({ 279 | timestamp: now - i * 1000 * 60, 280 | value: nextValue() 281 | }); 282 | } 283 | 284 | var widgetData = [ 285 | { 286 | key: 'Data', 287 | values: data 288 | } 289 | ]; 290 | 291 | this.updateScope(widgetData); 292 | }; 293 | 294 | RandomTimeSeriesDataModel.prototype.destroy = function () { 295 | RandomBaseDataModel.prototype.destroy.call(this); 296 | $interval.cancel(this.intervalPromise); 297 | }; 298 | 299 | return RandomTimeSeriesDataModel; 300 | }); 301 | /* 302 | * Copyright (c) 2014 DataTorrent, Inc. ALL Rights Reserved. 303 | * 304 | * Licensed under the Apache License, Version 2.0 (the "License"); 305 | * you may not use this file except in compliance with the License. 306 | * You may obtain a copy of the License at 307 | * 308 | * http://www.apache.org/licenses/LICENSE-2.0 309 | * 310 | * Unless required by applicable law or agreed to in writing, software 311 | * distributed under the License is distributed on an "AS IS" BASIS, 312 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 313 | * See the License for the specific language governing permissions and 314 | * limitations under the License. 315 | */ 316 | 317 | 'use strict'; 318 | 319 | angular.module('ui.models') 320 | .factory('WebSocketDataModel', function (WidgetDataModel, webSocket) { 321 | function WebSocketDataModel() { 322 | } 323 | 324 | WebSocketDataModel.prototype = Object.create(WidgetDataModel.prototype); 325 | 326 | WebSocketDataModel.prototype.init = function () { 327 | this.topic = null; 328 | this.callback = null; 329 | if (this.dataModelOptions && this.dataModelOptions.defaultTopic) { 330 | this.update(this.dataModelOptions.defaultTopic); 331 | } 332 | }; 333 | 334 | WebSocketDataModel.prototype.update = function (newTopic) { 335 | var that = this; 336 | 337 | if (this.topic && this.callback) { 338 | webSocket.unsubscribe(this.topic, this.callback); 339 | } 340 | 341 | this.callback = function (message) { 342 | that.updateScope(message); 343 | that.widgetScope.$apply(); 344 | }; 345 | 346 | this.topic = newTopic; 347 | webSocket.subscribe(this.topic, this.callback, this.widgetScope); 348 | }; 349 | 350 | WebSocketDataModel.prototype.destroy = function () { 351 | WidgetDataModel.prototype.destroy.call(this); 352 | 353 | if (this.topic && this.callback) { 354 | webSocket.unsubscribe(this.topic, this.callback); 355 | } 356 | }; 357 | 358 | return WebSocketDataModel; 359 | }) 360 | .factory('TimeSeriesDataModel', function (WebSocketDataModel) { 361 | function TimeSeriesDataModel() { 362 | } 363 | 364 | TimeSeriesDataModel.prototype = Object.create(WebSocketDataModel.prototype); 365 | 366 | TimeSeriesDataModel.prototype.init = function () { 367 | WebSocketDataModel.prototype.init.call(this); 368 | }; 369 | 370 | TimeSeriesDataModel.prototype.update = function (newTopic) { 371 | WebSocketDataModel.prototype.update.call(this, newTopic); 372 | this.items = []; 373 | }; 374 | 375 | TimeSeriesDataModel.prototype.updateScope = function (value) { 376 | value = _.isArray(value) ? value[0] : value; 377 | 378 | this.items.push({ 379 | timestamp: parseInt(value.timestamp, 10), //TODO 380 | value: parseInt(value.value, 10) //TODO 381 | }); 382 | 383 | if (this.items.length > 100) { //TODO 384 | this.items.shift(); 385 | } 386 | 387 | var chart = { 388 | data: this.items, 389 | max: 30 390 | }; 391 | 392 | WebSocketDataModel.prototype.updateScope.call(this, chart); 393 | this.data = []; 394 | }; 395 | 396 | return TimeSeriesDataModel; 397 | }) 398 | .factory('PieChartDataModel', function (WebSocketDataModel) { 399 | function PieChartDataModel() { 400 | } 401 | 402 | PieChartDataModel.prototype = Object.create(WebSocketDataModel.prototype); 403 | 404 | PieChartDataModel.prototype.init = function () { 405 | WebSocketDataModel.prototype.init.call(this); 406 | this.data = []; 407 | }; 408 | 409 | PieChartDataModel.prototype.update = function (newTopic) { 410 | WebSocketDataModel.prototype.update.call(this, newTopic); 411 | }; 412 | 413 | PieChartDataModel.prototype.updateScope = function (value) { 414 | var sum = _.reduce(value, function (memo, item) { 415 | return memo + parseFloat(item.value); 416 | }, 0); 417 | 418 | var sectors = _.map(value, function (item) { 419 | return { 420 | key: item.label, 421 | y: item.value / sum 422 | }; 423 | }); 424 | 425 | sectors = _.sortBy(sectors, function (item) { 426 | return item.key; 427 | }); 428 | 429 | WebSocketDataModel.prototype.updateScope.call(this, sectors); 430 | }; 431 | 432 | return PieChartDataModel; 433 | }); 434 | 435 | /* 436 | * Copyright (c) 2014 DataTorrent, Inc. ALL Rights Reserved. 437 | * 438 | * Licensed under the Apache License, Version 2.0 (the "License"); 439 | * you may not use this file except in compliance with the License. 440 | * You may obtain a copy of the License at 441 | * 442 | * http://www.apache.org/licenses/LICENSE-2.0 443 | * 444 | * Unless required by applicable law or agreed to in writing, software 445 | * distributed under the License is distributed on an "AS IS" BASIS, 446 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 447 | * See the License for the specific language governing permissions and 448 | * limitations under the License. 449 | */ 450 | 451 | 'use strict'; 452 | 453 | angular.module('ui.visibility', []) 454 | .factory('Visibility', function ($window) { 455 | return $window.Visibility; 456 | }); 457 | /* 458 | * Copyright (c) 2014 DataTorrent, Inc. ALL Rights Reserved. 459 | * 460 | * Licensed under the Apache License, Version 2.0 (the "License"); 461 | * you may not use this file except in compliance with the License. 462 | * You may obtain a copy of the License at 463 | * 464 | * http://www.apache.org/licenses/LICENSE-2.0 465 | * 466 | * Unless required by applicable law or agreed to in writing, software 467 | * distributed under the License is distributed on an "AS IS" BASIS, 468 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 469 | * See the License for the specific language governing permissions and 470 | * limitations under the License. 471 | */ 472 | 473 | 'use strict'; 474 | 475 | angular.module('ui.websocket') 476 | .factory('Visibility', function ($window) { 477 | return $window.Visibility; 478 | }) 479 | .provider('webSocket', function () { 480 | var visibilityTimeout = 20000; 481 | var maxConnectionAttempts = 5; 482 | var connectionAttemptInterval = 2000; 483 | var webSocketURL; 484 | var explicitConnect; 485 | var closeRequested; 486 | 487 | return { 488 | $get: function ($q, $rootScope, $timeout, notificationService, Visibility, $log, $window) { 489 | 490 | var socket; 491 | 492 | var webSocketService; 493 | 494 | var deferred = $q.defer(); 495 | 496 | var webSocketError = false; 497 | 498 | var connectionAttempts = 0; 499 | 500 | var onopen = function () { 501 | $log.info('WebSocket connection has been made. URL: ', webSocketURL); 502 | connectionAttempts = 0; 503 | deferred.resolve(); 504 | $rootScope.$apply(); 505 | }; 506 | 507 | var onclose = function() { 508 | // Reset the deferred 509 | deferred = $q.defer(); 510 | 511 | // Check if this close was requested 512 | if (!closeRequested) { 513 | 514 | // Check connectionAttempts count 515 | if (connectionAttempts < maxConnectionAttempts) { 516 | // Try to re-establish connection 517 | var url = this.url; 518 | connectionAttempts++; 519 | $timeout(function() { 520 | $log.info('Attempting to reconnect to websocket'); 521 | webSocketService.connect(url); 522 | }, connectionAttemptInterval); 523 | } 524 | 525 | else { 526 | $log.error('Could not re-establish the WebSocket connection.'); 527 | notificationService.notify({ 528 | type: 'error', 529 | title: 'Could not re-establish WebSocket Connection', 530 | text: 'The dashboard lost contact with your DataTorrent Gateway for over ' + 531 | Math.round((maxConnectionAttempts * connectionAttemptInterval)/1000) + 532 | ' seconds. Double-check your connection and try refreshing the page.' 533 | }); 534 | } 535 | 536 | } 537 | 538 | // Otherwise reset some flags 539 | else { 540 | connectionAttempts = 0; 541 | closeRequested = false; 542 | } 543 | }; 544 | 545 | // TODO: listeners for error and close, exposed on 546 | // service itself 547 | var onerror = function () { 548 | webSocketError = true; 549 | $log.error('WebSocket encountered an error'); 550 | }; 551 | 552 | var onmessage = function (event) { 553 | if (stopUpdates) { // stop updates if page is inactive 554 | return; 555 | } 556 | 557 | var message = JSON.parse(event.data); 558 | 559 | var topic = message.topic; 560 | 561 | if (topicMap.hasOwnProperty(topic)) { 562 | if ($window.WS_DEBUG) { 563 | if ($window.WS_DEBUG === true) { 564 | $log.debug('WebSocket ', topic, ' => ', message.data); 565 | } 566 | else { 567 | var search = new RegExp($window.WS_DEBUG + ''); 568 | if (search.test(topic)) { 569 | $log.debug('WebSocket ', topic, ' => ', message.data); 570 | } 571 | } 572 | } 573 | topicMap[topic].fire(message.data); 574 | } 575 | }; 576 | 577 | var topicMap = {}; // topic -> [callbacks] mapping 578 | 579 | var stopUpdates = false; 580 | 581 | 582 | if (Visibility.isSupported()) { 583 | var timeoutPromise; 584 | 585 | Visibility.change(angular.bind(this, function (e, state) { 586 | if (state === 'hidden') { 587 | timeoutPromise = $timeout(function () { 588 | stopUpdates = true; 589 | timeoutPromise = null; 590 | }, visibilityTimeout); 591 | } else { 592 | stopUpdates = false; 593 | 594 | if (timeoutPromise) { 595 | $timeout.cancel(timeoutPromise); 596 | } 597 | 598 | $log.debug('visible'); 599 | } 600 | })); 601 | } 602 | 603 | webSocketService = { 604 | send: function (message) { 605 | var msg = JSON.stringify(message); 606 | 607 | deferred.promise.then(function () { 608 | $log.debug('send ' + msg); 609 | socket.send(msg); 610 | }); 611 | }, 612 | 613 | subscribe: function (topic, callback, $scope) { 614 | var callbacks = topicMap[topic]; 615 | 616 | // If a jQuery.Callbacks object has not been created for this 617 | // topic, one should be created and a "subscribe" message 618 | // should be sent. 619 | if (!callbacks) { 620 | 621 | // send the subscribe message 622 | var message = { type: 'subscribe', topic: topic }; 623 | this.send(message); 624 | 625 | // create the Callbacks object 626 | callbacks = jQuery.Callbacks(); 627 | topicMap[topic] = callbacks; 628 | } 629 | 630 | // When scope is provided... 631 | if ($scope) { 632 | 633 | // ...it's $digest method should be called 634 | // after the callback has been triggered, so 635 | // we have to wrap the function. 636 | var wrappedCallback = function () { 637 | callback.apply({}, arguments); 638 | $scope.$digest(); 639 | }; 640 | callbacks.add(wrappedCallback); 641 | 642 | // We should also be listening for the destroy 643 | // event so we can automatically unsubscribe. 644 | $scope.$on('$destroy', angular.bind(this, function () { 645 | this.unsubscribe(topic, wrappedCallback); 646 | })); 647 | 648 | return wrappedCallback; 649 | } 650 | else { 651 | callbacks.add(callback); 652 | return callback; 653 | } 654 | }, 655 | // Unsubscribe callback is optional. If callback is omitted, all callbacks are removed. 656 | unsubscribe: function (topic, callback) { 657 | if (topicMap.hasOwnProperty(topic)) { 658 | var callbacks = topicMap[topic]; 659 | if (callback) { 660 | callbacks.remove(callback); 661 | } 662 | 663 | // If no callback is provided proceed to delete all existing callbacks 664 | // Or check callbacks.has() which will return false if there are no more handlers 665 | // registered in this callbacks collection. 666 | if (!callback || !callbacks.has()) { 667 | 668 | // Send the unsubscribe message first 669 | var message = { type: 'unsubscribe', topic: topic }; 670 | this.send(message); 671 | 672 | // Then remove the callbacks object for this topic 673 | delete topicMap[topic]; 674 | 675 | } 676 | } 677 | }, 678 | 679 | disconnect: function() { 680 | if (!socket) { 681 | return; 682 | } 683 | closeRequested = true; 684 | socket.close(); 685 | }, 686 | 687 | connect: function(url) { 688 | if (!url) { 689 | if (webSocketURL) { 690 | url = webSocketURL; 691 | } 692 | else { 693 | throw new TypeError('No WebSocket connection URL specified in connect method'); 694 | } 695 | } 696 | 697 | if (socket && socket.readyState === $window.WebSocket.OPEN) { 698 | $log.info('webSocket.connect called, but webSocket connection already established.'); 699 | return; 700 | } 701 | 702 | socket = new $window.WebSocket(url); 703 | // deferred = $q.defer(); 704 | socket.onopen = onopen; 705 | socket.onclose = onclose; 706 | socket.onerror = onerror; 707 | socket.onmessage = onmessage; 708 | 709 | // resubscribe to topics 710 | // send the subscribe message 711 | for (var topic in topicMap) { 712 | if (topicMap.hasOwnProperty(topic)) { 713 | var message = { type: 'subscribe', topic: topic }; 714 | this.send(message); 715 | } 716 | } 717 | } 718 | }; 719 | 720 | if (!explicitConnect) { 721 | webSocketService.connect(); 722 | } 723 | 724 | return webSocketService; 725 | }, 726 | 727 | setVisibilityTimeout: function (timeout) { 728 | visibilityTimeout = timeout; 729 | }, 730 | 731 | setWebSocketURL: function (wsURL) { 732 | webSocketURL = wsURL; 733 | }, 734 | 735 | setExplicitConnection: function(flag) { 736 | explicitConnect = flag; 737 | }, 738 | 739 | setMaxConnectionAttempts: function(max) { 740 | maxConnectionAttempts = max; 741 | }, 742 | 743 | setConnectionAttemptInterval: function(interval) { 744 | maxConnectionAttempts = interval; 745 | } 746 | }; 747 | }); 748 | 749 | /** 750 | * Copied from https://github.com/lithiumtech/angular_and_d3 751 | */ 752 | 753 | /* jshint ignore:start */ 754 | 755 | function Gauge(element, configuration) 756 | { 757 | this.element = element; 758 | 759 | var self = this; // for internal d3 functions 760 | 761 | this.configure = function(configuration) 762 | { 763 | this.config = configuration; 764 | 765 | this.config.size = this.config.size * 0.9; 766 | 767 | this.config.raduis = this.config.size * 0.97 / 2; 768 | this.config.cx = this.config.size / 2; 769 | this.config.cy = this.config.size / 2; 770 | 771 | this.config.min = undefined != configuration.min ? configuration.min : 0; 772 | this.config.max = undefined != configuration.max ? configuration.max : 100; 773 | this.config.range = this.config.max - this.config.min; 774 | 775 | this.config.majorTicks = configuration.majorTicks || 5; 776 | this.config.minorTicks = configuration.minorTicks || 2; 777 | 778 | this.config.greenColor = configuration.greenColor || "#109618"; 779 | this.config.yellowColor = configuration.yellowColor || "#FF9900"; 780 | this.config.redColor = configuration.redColor || "#DC3912"; 781 | 782 | this.config.transitionDuration = configuration.transitionDuration || 500; 783 | } 784 | 785 | this.render = function() 786 | { 787 | this.body = d3.select( this.element ) 788 | .append("svg:svg") 789 | .attr("class", "gauge") 790 | .attr("width", this.config.size) 791 | .attr("height", this.config.size); 792 | 793 | this.body.append("svg:circle") 794 | .attr("cx", this.config.cx) 795 | .attr("cy", this.config.cy) 796 | .attr("r", this.config.raduis) 797 | .style("fill", "#ccc") 798 | .style("stroke", "#000") 799 | .style("stroke-width", "0.5px"); 800 | 801 | this.body.append("svg:circle") 802 | .attr("cx", this.config.cx) 803 | .attr("cy", this.config.cy) 804 | .attr("r", 0.9 * this.config.raduis) 805 | .style("fill", "#fff") 806 | .style("stroke", "#e0e0e0") 807 | .style("stroke-width", "2px"); 808 | 809 | for (var index in this.config.greenZones) 810 | { 811 | this.drawBand(this.config.greenZones[index].from, this.config.greenZones[index].to, self.config.greenColor); 812 | } 813 | 814 | for (var index in this.config.yellowZones) 815 | { 816 | this.drawBand(this.config.yellowZones[index].from, this.config.yellowZones[index].to, self.config.yellowColor); 817 | } 818 | 819 | for (var index in this.config.redZones) 820 | { 821 | this.drawBand(this.config.redZones[index].from, this.config.redZones[index].to, self.config.redColor); 822 | } 823 | 824 | if (undefined != this.config.label) 825 | { 826 | var fontSize = Math.round(this.config.size / 9); 827 | this.body.append("svg:text") 828 | .attr("x", this.config.cx) 829 | .attr("y", this.config.cy / 2 + fontSize / 2) 830 | .attr("dy", fontSize / 2) 831 | .attr("text-anchor", "middle") 832 | .text(this.config.label) 833 | .style("font-size", fontSize + "px") 834 | .style("fill", "#333") 835 | .style("stroke-width", "0px"); 836 | } 837 | 838 | var fontSize = Math.round(this.config.size / 16); 839 | var majorDelta = this.config.range / (this.config.majorTicks - 1); 840 | for (var major = this.config.min; major <= this.config.max; major += majorDelta) 841 | { 842 | var minorDelta = majorDelta / this.config.minorTicks; 843 | for (var minor = major + minorDelta; minor < Math.min(major + majorDelta, this.config.max); minor += minorDelta) 844 | { 845 | var point1 = this.valueToPoint(minor, 0.75); 846 | var point2 = this.valueToPoint(minor, 0.85); 847 | 848 | this.body.append("svg:line") 849 | .attr("x1", point1.x) 850 | .attr("y1", point1.y) 851 | .attr("x2", point2.x) 852 | .attr("y2", point2.y) 853 | .style("stroke", "#666") 854 | .style("stroke-width", "1px"); 855 | } 856 | 857 | var point1 = this.valueToPoint(major, 0.7); 858 | var point2 = this.valueToPoint(major, 0.85); 859 | 860 | this.body.append("svg:line") 861 | .attr("x1", point1.x) 862 | .attr("y1", point1.y) 863 | .attr("x2", point2.x) 864 | .attr("y2", point2.y) 865 | .style("stroke", "#333") 866 | .style("stroke-width", "2px"); 867 | 868 | if (major == this.config.min || major == this.config.max) 869 | { 870 | var point = this.valueToPoint(major, 0.63); 871 | 872 | this.body.append("svg:text") 873 | .attr("x", point.x) 874 | .attr("y", point.y) 875 | .attr("dy", fontSize / 3) 876 | .attr("text-anchor", major == this.config.min ? "start" : "end") 877 | .text(major) 878 | .style("font-size", fontSize + "px") 879 | .style("fill", "#333") 880 | .style("stroke-width", "0px"); 881 | } 882 | } 883 | 884 | var pointerContainer = this.body.append("svg:g").attr("class", "pointerContainer"); 885 | 886 | var midValue = (this.config.min + this.config.max) / 2; 887 | 888 | var pointerPath = this.buildPointerPath(midValue); 889 | 890 | var pointerLine = d3.svg.line() 891 | .x(function(d) { return d.x }) 892 | .y(function(d) { return d.y }) 893 | .interpolate("basis"); 894 | 895 | pointerContainer.selectAll("path") 896 | .data([pointerPath]) 897 | .enter() 898 | .append("svg:path") 899 | .attr("d", pointerLine) 900 | .style("fill", "#dc3912") 901 | .style("stroke", "#c63310") 902 | .style("fill-opacity", 0.7) 903 | 904 | pointerContainer.append("svg:circle") 905 | .attr("cx", this.config.cx) 906 | .attr("cy", this.config.cy) 907 | .attr("r", 0.12 * this.config.raduis) 908 | .style("fill", "#4684EE") 909 | .style("stroke", "#666") 910 | .style("opacity", 1); 911 | 912 | var fontSize = Math.round(this.config.size / 10); 913 | pointerContainer.selectAll("text") 914 | .data([midValue]) 915 | .enter() 916 | .append("svg:text") 917 | .attr("x", this.config.cx) 918 | .attr("y", this.config.size - this.config.cy / 4 - fontSize) 919 | .attr("dy", fontSize / 2) 920 | .attr("text-anchor", "middle") 921 | .style("font-size", fontSize + "px") 922 | .style("fill", "#000") 923 | .style("stroke-width", "0px"); 924 | 925 | this.redraw(this.config.min, 0); 926 | } 927 | 928 | this.buildPointerPath = function(value) 929 | { 930 | var delta = this.config.range / 13; 931 | 932 | var head = valueToPoint(value, 0.85); 933 | var head1 = valueToPoint(value - delta, 0.12); 934 | var head2 = valueToPoint(value + delta, 0.12); 935 | 936 | var tailValue = value - (this.config.range * (1/(270/360)) / 2); 937 | var tail = valueToPoint(tailValue, 0.28); 938 | var tail1 = valueToPoint(tailValue - delta, 0.12); 939 | var tail2 = valueToPoint(tailValue + delta, 0.12); 940 | 941 | return [head, head1, tail2, tail, tail1, head2, head]; 942 | 943 | function valueToPoint(value, factor) 944 | { 945 | var point = self.valueToPoint(value, factor); 946 | point.x -= self.config.cx; 947 | point.y -= self.config.cy; 948 | return point; 949 | } 950 | } 951 | 952 | this.drawBand = function(start, end, color) 953 | { 954 | if (0 >= end - start) return; 955 | 956 | this.body.append("svg:path") 957 | .style("fill", color) 958 | .attr("d", d3.svg.arc() 959 | .startAngle(this.valueToRadians(start)) 960 | .endAngle(this.valueToRadians(end)) 961 | .innerRadius(0.65 * this.config.raduis) 962 | .outerRadius(0.85 * this.config.raduis)) 963 | .attr("transform", function() { return "translate(" + self.config.cx + ", " + self.config.cy + ") rotate(270)" }); 964 | } 965 | 966 | this.redraw = function(value, transitionDuration) 967 | { 968 | var pointerContainer = this.body.select(".pointerContainer"); 969 | 970 | pointerContainer.selectAll("text").text(Math.round(value)); 971 | 972 | var pointer = pointerContainer.selectAll("path"); 973 | pointer.transition() 974 | .duration(undefined != transitionDuration ? transitionDuration : this.config.transitionDuration) 975 | //.delay(0) 976 | //.ease("linear") 977 | //.attr("transform", function(d) 978 | .attrTween("transform", function() 979 | { 980 | var pointerValue = value; 981 | if (value > self.config.max) pointerValue = self.config.max + 0.02*self.config.range; 982 | else if (value < self.config.min) pointerValue = self.config.min - 0.02*self.config.range; 983 | var targetRotation = (self.valueToDegrees(pointerValue) - 90); 984 | var currentRotation = self._currentRotation || targetRotation; 985 | self._currentRotation = targetRotation; 986 | 987 | return function(step) 988 | { 989 | var rotation = currentRotation + (targetRotation-currentRotation)*step; 990 | return "translate(" + self.config.cx + ", " + self.config.cy + ") rotate(" + rotation + ")"; 991 | } 992 | }); 993 | } 994 | 995 | this.valueToDegrees = function(value) 996 | { 997 | // thanks @closealert 998 | //return value / this.config.range * 270 - 45; 999 | return value / this.config.range * 270 - (this.config.min / this.config.range * 270 + 45); 1000 | } 1001 | 1002 | this.valueToRadians = function(value) 1003 | { 1004 | return this.valueToDegrees(value) * Math.PI / 180; 1005 | } 1006 | 1007 | this.valueToPoint = function(value, factor) 1008 | { 1009 | return { x: this.config.cx - this.config.raduis * factor * Math.cos(this.valueToRadians(value)), 1010 | y: this.config.cy - this.config.raduis * factor * Math.sin(this.valueToRadians(value)) }; 1011 | } 1012 | 1013 | // initialization 1014 | this.configure(configuration); 1015 | } 1016 | 1017 | /* jshint ignore:end */ 1018 | /*! 1019 | * visibly - v0.6 Aug 2011 - Page Visibility API Polyfill 1020 | * http://github.com/addyosmani 1021 | * Copyright (c) 2011 Addy Osmani 1022 | * Dual licensed under the MIT and GPL licenses. 1023 | * 1024 | * Methods supported: 1025 | * visibly.onVisible(callback) 1026 | * visibly.onHidden(callback) 1027 | * visibly.hidden() 1028 | * visibly.visibilityState() 1029 | * visibly.visibilitychange(callback(state)); 1030 | */ 1031 | 1032 | ;(function () { 1033 | 1034 | window.visibly = { 1035 | q: document, 1036 | p: undefined, 1037 | prefixes: ['webkit', 'ms','o','moz','khtml'], 1038 | props: ['VisibilityState', 'visibilitychange', 'Hidden'], 1039 | m: ['focus', 'blur'], 1040 | visibleCallbacks: [], 1041 | hiddenCallbacks: [], 1042 | genericCallbacks:[], 1043 | _callbacks: [], 1044 | cachedPrefix:"", 1045 | fn:null, 1046 | 1047 | onVisible: function (_callback) { 1048 | if(typeof _callback == 'function' ){ 1049 | this.visibleCallbacks.push(_callback); 1050 | } 1051 | }, 1052 | onHidden: function (_callback) { 1053 | if(typeof _callback == 'function' ){ 1054 | this.hiddenCallbacks.push(_callback); 1055 | } 1056 | }, 1057 | getPrefix:function(){ 1058 | if(!this.cachedPrefix){ 1059 | for(var l=0;b=this.prefixes[l++];){ 1060 | if(b + this.props[2] in this.q){ 1061 | this.cachedPrefix = b; 1062 | return this.cachedPrefix; 1063 | } 1064 | } 1065 | } 1066 | }, 1067 | 1068 | visibilityState:function(){ 1069 | return this._getProp(0); 1070 | }, 1071 | hidden:function(){ 1072 | return this._getProp(2); 1073 | }, 1074 | visibilitychange:function(fn){ 1075 | if(typeof fn == 'function' ){ 1076 | this.genericCallbacks.push(fn); 1077 | } 1078 | 1079 | var n = this.genericCallbacks.length; 1080 | if(n){ 1081 | if(this.cachedPrefix){ 1082 | while(n--){ 1083 | this.genericCallbacks[n].call(this, this.visibilityState()); 1084 | } 1085 | }else{ 1086 | while(n--){ 1087 | this.genericCallbacks[n].call(this, arguments[0]); 1088 | } 1089 | } 1090 | } 1091 | 1092 | }, 1093 | isSupported: function (index) { 1094 | return ((this.cachedPrefix + this.props[2]) in this.q); 1095 | }, 1096 | _getProp:function(index){ 1097 | return this.q[this.cachedPrefix + this.props[index]]; 1098 | }, 1099 | _execute: function (index) { 1100 | if (index) { 1101 | this._callbacks = (index == 1) ? this.visibleCallbacks : this.hiddenCallbacks; 1102 | var n = this._callbacks.length; 1103 | while(n--){ 1104 | this._callbacks[n](); 1105 | } 1106 | } 1107 | }, 1108 | _visible: function () { 1109 | window.visibly._execute(1); 1110 | window.visibly.visibilitychange.call(window.visibly, 'visible'); 1111 | }, 1112 | _hidden: function () { 1113 | window.visibly._execute(2); 1114 | window.visibly.visibilitychange.call(window.visibly, 'hidden'); 1115 | }, 1116 | _nativeSwitch: function () { 1117 | this[this._getProp(2) ? '_hidden' : '_visible'](); 1118 | }, 1119 | _listen: function () { 1120 | try { /*if no native page visibility support found..*/ 1121 | if (!(this.isSupported())) { 1122 | if (this.q.addEventListener) { /*for browsers without focusin/out support eg. firefox, opera use focus/blur*/ 1123 | window.addEventListener(this.m[0], this._visible, 1); 1124 | window.addEventListener(this.m[1], this._hidden, 1); 1125 | } else { /*IE <10s most reliable focus events are onfocusin/onfocusout*/ 1126 | if (this.q.attachEvent) { 1127 | this.q.attachEvent('onfocusin', this._visible); 1128 | this.q.attachEvent('onfocusout', this._hidden); 1129 | } 1130 | } 1131 | } else { /*switch support based on prefix detected earlier*/ 1132 | this.q.addEventListener(this.cachedPrefix + this.props[1], function () { 1133 | window.visibly._nativeSwitch.apply(window.visibly, arguments); 1134 | }, 1); 1135 | } 1136 | } catch (e) {} 1137 | }, 1138 | init: function () { 1139 | this.getPrefix(); 1140 | this._listen(); 1141 | } 1142 | }; 1143 | 1144 | this.visibly.init(); 1145 | })(); 1146 | 1147 | /* 1148 | * Copyright (c) 2014 DataTorrent, Inc. ALL Rights Reserved. 1149 | * 1150 | * Licensed under the Apache License, Version 2.0 (the "License"); 1151 | * you may not use this file except in compliance with the License. 1152 | * You may obtain a copy of the License at 1153 | * 1154 | * http://www.apache.org/licenses/LICENSE-2.0 1155 | * 1156 | * Unless required by applicable law or agreed to in writing, software 1157 | * distributed under the License is distributed on an "AS IS" BASIS, 1158 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 1159 | * See the License for the specific language governing permissions and 1160 | * limitations under the License. 1161 | */ 1162 | 1163 | 'use strict'; 1164 | 1165 | angular.module('ui.widgets') 1166 | .directive('wtBarChart', function ($filter) { 1167 | return { 1168 | restrict: 'A', 1169 | replace: true, 1170 | templateUrl: 'template/widgets/barChart/barChart.html', 1171 | scope: { 1172 | data: '=data' 1173 | }, 1174 | controller: function ($scope) { 1175 | var filter = $filter('date'); 1176 | 1177 | $scope.xAxisTickFormatFunction = function () { 1178 | return function(d) { 1179 | return filter(d, 'HH:mm'); 1180 | }; 1181 | }; 1182 | 1183 | $scope.xFunction = function(){ 1184 | return function(d) { 1185 | return d.timestamp; 1186 | }; 1187 | }; 1188 | $scope.yFunction = function(){ 1189 | return function(d) { 1190 | return d.value; 1191 | }; 1192 | }; 1193 | }, 1194 | link: function postLink(scope) { 1195 | scope.$watch('data', function (data) { 1196 | if (data && data[0] && data[0].values && (data[0].values.length > 1)) { 1197 | var timeseries = _.sortBy(data[0].values, function (item) { 1198 | return item.timestamp; 1199 | }); 1200 | 1201 | var start = timeseries[0].timestamp; 1202 | var end = timeseries[timeseries.length - 1].timestamp; 1203 | scope.start = start; 1204 | scope.end = end; 1205 | } 1206 | }); 1207 | } 1208 | }; 1209 | }); 1210 | /* 1211 | * Copyright (c) 2014 DataTorrent, Inc. ALL Rights Reserved. 1212 | * 1213 | * Licensed under the Apache License, Version 2.0 (the "License"); 1214 | * you may not use this file except in compliance with the License. 1215 | * You may obtain a copy of the License at 1216 | * 1217 | * http://www.apache.org/licenses/LICENSE-2.0 1218 | * 1219 | * Unless required by applicable law or agreed to in writing, software 1220 | * distributed under the License is distributed on an "AS IS" BASIS, 1221 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 1222 | * See the License for the specific language governing permissions and 1223 | * limitations under the License. 1224 | */ 1225 | 1226 | 'use strict'; 1227 | 1228 | angular.module('ui.widgets') 1229 | .directive('wtGauge', function () { 1230 | return { 1231 | replace: true, 1232 | scope: { 1233 | label: '@', 1234 | min: '=', 1235 | max: '=', 1236 | value: '=' 1237 | }, 1238 | link: function (scope, element, attrs) { 1239 | var config = { 1240 | size: 200, 1241 | label: attrs.label, 1242 | min: undefined !== scope.min ? scope.min : 0, 1243 | max: undefined !== scope.max ? scope.max : 100, 1244 | minorTicks: 5 1245 | }; 1246 | 1247 | var range = config.max - config.min; 1248 | config.yellowZones = [ 1249 | { from: config.min + range * 0.75, to: config.min + range * 0.9 } 1250 | ]; 1251 | config.redZones = [ 1252 | { from: config.min + range * 0.9, to: config.max } 1253 | ]; 1254 | 1255 | scope.gauge = new Gauge(element[0], config); 1256 | scope.gauge.render(); 1257 | 1258 | function update(value) { 1259 | var percentage; 1260 | if (_.isString(value)) { 1261 | percentage = parseFloat(value); 1262 | } else if (_.isNumber(value)) { 1263 | percentage = value; 1264 | } 1265 | 1266 | if (!_.isUndefined(percentage)) { 1267 | scope.gauge.redraw(percentage); 1268 | } 1269 | } 1270 | 1271 | update(0); 1272 | 1273 | scope.$watch('value', function (value) { 1274 | if (scope.gauge) { 1275 | update(value); 1276 | } 1277 | }); 1278 | } 1279 | }; 1280 | }); 1281 | /* 1282 | * Copyright (c) 2014 DataTorrent, Inc. ALL Rights Reserved. 1283 | * 1284 | * Licensed under the Apache License, Version 2.0 (the "License"); 1285 | * you may not use this file except in compliance with the License. 1286 | * You may obtain a copy of the License at 1287 | * 1288 | * http://www.apache.org/licenses/LICENSE-2.0 1289 | * 1290 | * Unless required by applicable law or agreed to in writing, software 1291 | * distributed under the License is distributed on an "AS IS" BASIS, 1292 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 1293 | * See the License for the specific language governing permissions and 1294 | * limitations under the License. 1295 | */ 1296 | 1297 | 'use strict'; 1298 | 1299 | angular.module('ui.widgets') 1300 | .directive('wtHistoricalChart', function () { 1301 | return { 1302 | restrict: 'A', 1303 | replace: true, 1304 | templateUrl: 'template/widgets/historicalChart/historicalChart.html', 1305 | scope: { 1306 | chart: '=' 1307 | }, 1308 | controller: function ($scope) { 1309 | $scope.mode = 'MINUTES'; 1310 | 1311 | $scope.changeMode = function (mode) { 1312 | $scope.mode = mode; 1313 | $scope.$emit('modeChanged', mode); 1314 | }; 1315 | } 1316 | }; 1317 | }); 1318 | /* 1319 | * Copyright (c) 2014 DataTorrent, Inc. ALL Rights Reserved. 1320 | * 1321 | * Licensed under the Apache License, Version 2.0 (the "License"); 1322 | * you may not use this file except in compliance with the License. 1323 | * You may obtain a copy of the License at 1324 | * 1325 | * http://www.apache.org/licenses/LICENSE-2.0 1326 | * 1327 | * Unless required by applicable law or agreed to in writing, software 1328 | * distributed under the License is distributed on an "AS IS" BASIS, 1329 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 1330 | * See the License for the specific language governing permissions and 1331 | * limitations under the License. 1332 | */ 1333 | 1334 | 'use strict'; 1335 | 1336 | angular.module('ui.widgets') 1337 | .directive('wtLineChart', function () { 1338 | return { 1339 | template: '
', 1340 | scope: { 1341 | chart: '=' 1342 | }, 1343 | replace: true, 1344 | link: function postLink(scope, element) { 1345 | var lineChart = new google.visualization.LineChart(element[0]); 1346 | 1347 | function draw(chart) { 1348 | var data = chart.data; 1349 | 1350 | var table = new google.visualization.DataTable(); 1351 | table.addColumn('datetime'); 1352 | table.addColumn('number'); 1353 | table.addRows(data.length); 1354 | 1355 | var view = new google.visualization.DataView(table); 1356 | 1357 | for (var i = 0; i < data.length; i++) { 1358 | var item = data[i]; 1359 | table.setCell(i, 0, new Date(item.timestamp)); 1360 | var value = parseFloat(item.value); 1361 | table.setCell(i, 1, value); 1362 | } 1363 | 1364 | var chartOptions = { 1365 | legend: 'none', 1366 | vAxis: { minValue: 0, maxValue: 100 } 1367 | //chartArea: { top: 20, left: 30, height: 240 } 1368 | }; 1369 | 1370 | if (chart.max) { 1371 | var lastTimestamp; 1372 | 1373 | if (data.length) { 1374 | var last = data[data.length - 1]; 1375 | lastTimestamp = last.timestamp; 1376 | } else { 1377 | lastTimestamp = Date.now(); 1378 | } 1379 | 1380 | var max = new Date(lastTimestamp); 1381 | var min = new Date(lastTimestamp - (chart.max - 1) * 1000); 1382 | 1383 | angular.extend(chartOptions, { 1384 | hAxis: { viewWindow: { min: min, max: max }} 1385 | }); 1386 | } 1387 | 1388 | if (chart.chartOptions) { 1389 | angular.extend(chartOptions, chart.chartOptions); 1390 | } 1391 | 1392 | lineChart.draw(view, chartOptions); 1393 | } 1394 | 1395 | scope.$watch('chart', function (chart) { 1396 | if (!chart) { 1397 | chart = { 1398 | data: [], 1399 | max: 30 1400 | }; 1401 | } 1402 | 1403 | if (chart.data) { 1404 | draw(chart); 1405 | } 1406 | }); 1407 | } 1408 | }; 1409 | }); 1410 | /* 1411 | * Copyright (c) 2014 DataTorrent, Inc. ALL Rights Reserved. 1412 | * 1413 | * Licensed under the Apache License, Version 2.0 (the "License"); 1414 | * you may not use this file except in compliance with the License. 1415 | * You may obtain a copy of the License at 1416 | * 1417 | * http://www.apache.org/licenses/LICENSE-2.0 1418 | * 1419 | * Unless required by applicable law or agreed to in writing, software 1420 | * distributed under the License is distributed on an "AS IS" BASIS, 1421 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 1422 | * See the License for the specific language governing permissions and 1423 | * limitations under the License. 1424 | */ 1425 | 1426 | 'use strict'; 1427 | 1428 | angular.module('ui.widgets') 1429 | .directive('wtMetricsChart', function ($filter, MetricsChartHistory) { 1430 | return { 1431 | restrict: 'A', 1432 | replace: true, 1433 | templateUrl: 'template/widgets/metricsChart/metricsChart.html', 1434 | scope: { 1435 | data: '=?', 1436 | metrics: '=', 1437 | controller: '=' 1438 | }, 1439 | controller: function ($scope) { 1440 | var filter = $filter('date'); 1441 | var yAxisFilter = $filter('number'); 1442 | 1443 | $scope.xAxisTickFormatFunction = function () { 1444 | return function (d) { 1445 | return filter(d, 'mm:ss'); 1446 | }; 1447 | }; 1448 | 1449 | $scope.yAxisTickFormatFunction = function () { 1450 | return function (d) { 1451 | return yAxisFilter(d); 1452 | }; 1453 | }; 1454 | 1455 | $scope.xFunction = function () { 1456 | return function (d) { 1457 | return d.timestamp; 1458 | }; 1459 | }; 1460 | $scope.yFunction = function () { 1461 | return function (d) { 1462 | return d.value; 1463 | }; 1464 | }; 1465 | 1466 | $scope.chartCallback = function () { // callback to access nvd3 chart 1467 | //console.log('chartCallback'); 1468 | //console.log(arguments); 1469 | //console.log(chart.legend.dispatch.); 1470 | //chart.legend.dispatch.on('legendClick', function(newState) { 1471 | // console.log(newState); 1472 | //}); 1473 | }; 1474 | 1475 | $scope.maxTimeLimit = 300; 1476 | 1477 | $scope.options = [ 1478 | { 1479 | value: 30, 1480 | label: 'last 30 seconds' 1481 | }, 1482 | { 1483 | value: 60, 1484 | label: 'last minute' 1485 | }, 1486 | { 1487 | value: 120, 1488 | label: 'last two minutes' 1489 | }, 1490 | { 1491 | value: $scope.maxTimeLimit, 1492 | label: 'last 5 minutes' 1493 | } 1494 | ]; 1495 | $scope.timeFrame = $scope.options[0]; 1496 | 1497 | 1498 | var chartHistory = null; 1499 | if ($scope.controller) { 1500 | chartHistory = new MetricsChartHistory($scope, $scope.metrics, $scope.maxTimeLimit, $scope.timeFrame.value); 1501 | $scope.controller.addPoint = function (point) { 1502 | chartHistory.addPoint(point); 1503 | }; 1504 | } 1505 | 1506 | $scope.timeFrameChanged = function (newTimeFrame) { 1507 | if (chartHistory) { 1508 | chartHistory.updateChart(Date.now(), newTimeFrame.value); 1509 | } 1510 | }; 1511 | }, 1512 | link: function postLink(scope) { 1513 | scope.data = []; 1514 | } 1515 | }; 1516 | }) 1517 | .factory('MetricsChartHistory', function () { 1518 | function MetricsChartHistory(scope, metrics, maxTimeLimit, timeLimit) { 1519 | this.scope = scope; 1520 | this.metrics = metrics; 1521 | this.maxTimeLimit = maxTimeLimit; 1522 | this.timeLimit = timeLimit; 1523 | this.history = []; 1524 | 1525 | this.series = []; 1526 | 1527 | _.each(metrics, function (metric) { 1528 | this.series.push({ 1529 | key: metric.key, 1530 | disabled: !metric.visible, 1531 | color: metric.color 1532 | }); 1533 | }.bind(this)); 1534 | } 1535 | 1536 | angular.extend(MetricsChartHistory.prototype, { 1537 | updateHistory: function (now, point) { 1538 | var historyStartTime = now - this.maxTimeLimit * 1000; 1539 | 1540 | var ind = _.findIndex(this.history, function (historyPoint) { 1541 | return historyPoint.timestamp >= historyStartTime; 1542 | }); 1543 | if (ind > 1) { 1544 | this.history = _.rest(this.history, ind - 1); 1545 | } 1546 | 1547 | var historyPoint = { 1548 | timestamp: now, 1549 | data: point 1550 | }; 1551 | this.history.push(historyPoint); 1552 | }, 1553 | 1554 | updateChart: function (now, timeLimit) { 1555 | this.timeLimit = timeLimit; 1556 | 1557 | var startTime = now - 1000 * timeLimit; 1558 | 1559 | var history = _.filter(this.history, function (historyPoint) { //TODO optimize 1560 | return historyPoint.timestamp >= startTime; 1561 | }); 1562 | 1563 | _.each(this.metrics, function (metric, index) { 1564 | var metricKey = metric.key; 1565 | 1566 | var values = _.map(history, function (historyPoint) { 1567 | return { 1568 | timestamp: historyPoint.timestamp, 1569 | value: Math.round(parseInt(historyPoint.data[metricKey], 10)) 1570 | }; 1571 | }); 1572 | 1573 | this.series[index].values = values; 1574 | }.bind(this)); 1575 | 1576 | /* 1577 | //TODO this is workaround to have fixed x axis scale when no enough date is available 1578 | chart.push({ 1579 | key: 'Left Value', 1580 | values: [ 1581 | {timestamp: startTime, value: 0} 1582 | ] 1583 | }); 1584 | */ 1585 | 1586 | /* 1587 | var max = _.max(history, function (historyPoint) { //TODO optimize 1588 | return historyPoint.stats.tuplesEmittedPSMA; //TODO 1589 | }); 1590 | 1591 | chart.push({ 1592 | key: 'Upper Value', 1593 | values: [ 1594 | {timestamp: now - 30 * 1000, value: Math.round(max.value * 1.2)} 1595 | ] 1596 | }); 1597 | */ 1598 | 1599 | if (history.length > 1) { 1600 | this.scope.data = _.clone(this.series); 1601 | this.scope.start = Math.min(startTime, _.first(history).timestamp); 1602 | this.scope.end = _.last(history).timestamp; 1603 | } 1604 | }, 1605 | 1606 | addPoint: function (point) { 1607 | var now = Date.now(); 1608 | this.updateHistory(now, point); 1609 | 1610 | this.updateChart(now, this.timeLimit); 1611 | } 1612 | }); 1613 | 1614 | return MetricsChartHistory; 1615 | }); 1616 | /* 1617 | * Copyright (c) 2014 DataTorrent, Inc. ALL Rights Reserved. 1618 | * 1619 | * Licensed under the Apache License, Version 2.0 (the "License"); 1620 | * you may not use this file except in compliance with the License. 1621 | * You may obtain a copy of the License at 1622 | * 1623 | * http://www.apache.org/licenses/LICENSE-2.0 1624 | * 1625 | * Unless required by applicable law or agreed to in writing, software 1626 | * distributed under the License is distributed on an "AS IS" BASIS, 1627 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 1628 | * See the License for the specific language governing permissions and 1629 | * limitations under the License. 1630 | */ 1631 | 1632 | 'use strict'; 1633 | 1634 | angular.module('ui.widgets') 1635 | .directive('wtNvd3LineChart', function ($filter) { 1636 | return { 1637 | restrict: 'A', 1638 | replace: true, 1639 | templateUrl: 'template/widgets/nvd3LineChart/nvd3LineChart.html', 1640 | scope: { 1641 | data: '=data', 1642 | showLegend: '@', 1643 | showTimeRange: '=?', 1644 | timeAxisFormat: '=?' 1645 | }, 1646 | controller: function ($scope) { 1647 | var filter = $filter('date'); 1648 | var numberFilter = $filter('number'); 1649 | 1650 | $scope.xAxisTickFormatFunction = function () { 1651 | return function (d) { 1652 | return filter(d, $scope.timeAxisFormat); 1653 | }; 1654 | }; 1655 | 1656 | $scope.yAxisTickFormatFunction = function () { 1657 | return function (d) { 1658 | if (d > 999) { 1659 | var value; 1660 | var scale; 1661 | if (d < 999999) { 1662 | value = Math.round(d/1000); 1663 | scale = 'k'; 1664 | } else { 1665 | value = Math.round(d/1000000); 1666 | scale = 'm'; 1667 | } 1668 | return numberFilter(value) + scale; 1669 | } else { 1670 | return numberFilter(d); 1671 | } 1672 | }; 1673 | }; 1674 | 1675 | $scope.xFunction = function () { 1676 | return function (d) { 1677 | return d.timestamp; 1678 | }; 1679 | }; 1680 | $scope.yFunction = function () { 1681 | return function (d) { 1682 | return d.value; 1683 | }; 1684 | }; 1685 | }, 1686 | link: function postLink(scope, element, attrs) { 1687 | if (!_.has(attrs, 'showTimeRange')) { 1688 | scope.showTimeRange = true; 1689 | } 1690 | 1691 | scope.timeAxisFormat = scope.timeAxisFormat || 'HH:mm'; 1692 | 1693 | scope.$watch('data', function (data) { 1694 | if (data && data[0] && data[0].values && (data[0].values.length > 1)) { 1695 | var timeseries = _.sortBy(data[0].values, function (item) { 1696 | return item.timestamp; 1697 | }); 1698 | 1699 | var start = timeseries[0].timestamp; 1700 | var end = timeseries[timeseries.length - 1].timestamp; 1701 | scope.start = start; 1702 | scope.end = end; 1703 | } 1704 | }); 1705 | } 1706 | }; 1707 | }); 1708 | /* 1709 | * Copyright (c) 2014 DataTorrent, Inc. ALL Rights Reserved. 1710 | * 1711 | * Licensed under the Apache License, Version 2.0 (the "License"); 1712 | * you may not use this file except in compliance with the License. 1713 | * You may obtain a copy of the License at 1714 | * 1715 | * http://www.apache.org/licenses/LICENSE-2.0 1716 | * 1717 | * Unless required by applicable law or agreed to in writing, software 1718 | * distributed under the License is distributed on an "AS IS" BASIS, 1719 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 1720 | * See the License for the specific language governing permissions and 1721 | * limitations under the License. 1722 | */ 1723 | 1724 | 'use strict'; 1725 | 1726 | angular.module('ui.widgets') 1727 | .directive('wtPieChart', function () { 1728 | return { 1729 | restrict: 'A', 1730 | replace: true, 1731 | templateUrl: 'template/widgets/pieChart/pieChart.html', 1732 | scope: { 1733 | data: '=data' 1734 | }, 1735 | controller: function ($scope) { 1736 | $scope.xFunction = function(){ 1737 | return function(d) { 1738 | return d.key; 1739 | }; 1740 | }; 1741 | $scope.yFunction = function(){ 1742 | return function(d) { 1743 | return d.y; 1744 | }; 1745 | }; 1746 | 1747 | $scope.descriptionFunction = function(){ 1748 | return function(d){ 1749 | return d.key + ' ' + d.y; 1750 | }; 1751 | }; 1752 | } 1753 | }; 1754 | }); 1755 | /* 1756 | * Copyright (c) 2014 DataTorrent, Inc. ALL Rights Reserved. 1757 | * 1758 | * Licensed under the Apache License, Version 2.0 (the "License"); 1759 | * you may not use this file except in compliance with the License. 1760 | * You may obtain a copy of the License at 1761 | * 1762 | * http://www.apache.org/licenses/LICENSE-2.0 1763 | * 1764 | * Unless required by applicable law or agreed to in writing, software 1765 | * distributed under the License is distributed on an "AS IS" BASIS, 1766 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 1767 | * See the License for the specific language governing permissions and 1768 | * limitations under the License. 1769 | */ 1770 | 1771 | 'use strict'; 1772 | 1773 | angular.module('ui.widgets') 1774 | .directive('wtRandom', function ($interval) { 1775 | return { 1776 | restrict: 'A', 1777 | replace: true, 1778 | templateUrl: 'template/widgets/random/random.html', 1779 | link: function postLink(scope) { 1780 | function update() { 1781 | scope.number = Math.floor(Math.random() * 100); 1782 | } 1783 | 1784 | var promise = $interval(update, 500); 1785 | 1786 | scope.$on('$destroy', function () { 1787 | $interval.cancel(promise); 1788 | }); 1789 | } 1790 | }; 1791 | }); 1792 | /* 1793 | * Copyright (c) 2014 DataTorrent, Inc. ALL Rights Reserved. 1794 | * 1795 | * Licensed under the Apache License, Version 2.0 (the "License"); 1796 | * you may not use this file except in compliance with the License. 1797 | * You may obtain a copy of the License at 1798 | * 1799 | * http://www.apache.org/licenses/LICENSE-2.0 1800 | * 1801 | * Unless required by applicable law or agreed to in writing, software 1802 | * distributed under the License is distributed on an "AS IS" BASIS, 1803 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 1804 | * See the License for the specific language governing permissions and 1805 | * limitations under the License. 1806 | */ 1807 | 1808 | 'use strict'; 1809 | 1810 | angular.module('ui.widgets') 1811 | .directive('wtScopeWatch', function () { 1812 | return { 1813 | restrict: 'A', 1814 | replace: true, 1815 | templateUrl: 'template/widgets/scopeWatch/scopeWatch.html', 1816 | scope: { 1817 | scopeValue: '=value', 1818 | valueClass: '@valueClass' 1819 | } 1820 | }; 1821 | }); 1822 | /* 1823 | * Copyright (c) 2014 DataTorrent, Inc. ALL Rights Reserved. 1824 | * 1825 | * Licensed under the Apache License, Version 2.0 (the "License"); 1826 | * you may not use this file except in compliance with the License. 1827 | * You may obtain a copy of the License at 1828 | * 1829 | * http://www.apache.org/licenses/LICENSE-2.0 1830 | * 1831 | * Unless required by applicable law or agreed to in writing, software 1832 | * distributed under the License is distributed on an "AS IS" BASIS, 1833 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 1834 | * See the License for the specific language governing permissions and 1835 | * limitations under the License. 1836 | */ 1837 | 1838 | 'use strict'; 1839 | 1840 | angular.module('ui.widgets') 1841 | .directive('wtSelect', function () { 1842 | return { 1843 | restrict: 'A', 1844 | replace: true, 1845 | templateUrl: 'template/widgets/select/select.html', 1846 | scope: { 1847 | label: '@', 1848 | value: '=', 1849 | options: '=' 1850 | }, 1851 | controller: function ($scope) { 1852 | $scope.prevValue = function () { 1853 | var index = $scope.options.indexOf($scope.value); 1854 | var nextIndex = (index - 1 + $scope.options.length) % $scope.options.length; 1855 | $scope.value = $scope.options[nextIndex]; 1856 | }; 1857 | 1858 | $scope.nextValue = function () { 1859 | var index = $scope.options.indexOf($scope.value); 1860 | var nextIndex = (index + 1) % $scope.options.length; 1861 | $scope.value = $scope.options[nextIndex]; 1862 | }; 1863 | } 1864 | }; 1865 | }); 1866 | /* 1867 | * Copyright (c) 2014 DataTorrent, Inc. ALL Rights Reserved. 1868 | * 1869 | * Licensed under the Apache License, Version 2.0 (the "License"); 1870 | * you may not use this file except in compliance with the License. 1871 | * You may obtain a copy of the License at 1872 | * 1873 | * http://www.apache.org/licenses/LICENSE-2.0 1874 | * 1875 | * Unless required by applicable law or agreed to in writing, software 1876 | * distributed under the License is distributed on an "AS IS" BASIS, 1877 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 1878 | * See the License for the specific language governing permissions and 1879 | * limitations under the License. 1880 | */ 1881 | 1882 | 'use strict'; 1883 | 1884 | angular.module('ui.widgets') 1885 | .directive('wtTime', function ($interval) { 1886 | return { 1887 | restrict: 'A', 1888 | replace: true, 1889 | templateUrl: 'template/widgets/time/time.html', 1890 | link: function (scope) { 1891 | function update() { 1892 | scope.time = new Date().toLocaleTimeString(); 1893 | } 1894 | 1895 | var promise = $interval(update, 500); 1896 | 1897 | scope.$on('$destroy', function () { 1898 | $interval.cancel(promise); 1899 | }); 1900 | } 1901 | }; 1902 | }); 1903 | /* 1904 | * Copyright (c) 2014 DataTorrent, Inc. ALL Rights Reserved. 1905 | * 1906 | * Licensed under the Apache License, Version 2.0 (the "License"); 1907 | * you may not use this file except in compliance with the License. 1908 | * You may obtain a copy of the License at 1909 | * 1910 | * http://www.apache.org/licenses/LICENSE-2.0 1911 | * 1912 | * Unless required by applicable law or agreed to in writing, software 1913 | * distributed under the License is distributed on an "AS IS" BASIS, 1914 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 1915 | * See the License for the specific language governing permissions and 1916 | * limitations under the License. 1917 | */ 1918 | 1919 | 'use strict'; 1920 | 1921 | angular.module('ui.widgets') 1922 | .directive('wtTopN', function () { 1923 | return { 1924 | restrict: 'A', 1925 | replace: true, 1926 | templateUrl: 'template/widgets/topN/topN.html', 1927 | scope: { 1928 | data: '=' 1929 | }, 1930 | controller: function ($scope) { 1931 | $scope.tableOptions = { 1932 | initialSorts: [ 1933 | { id: 'value', dir: '-' } 1934 | ] 1935 | }; 1936 | $scope.columns = [ 1937 | { id: 'name', key: 'name', label: 'Name' }, 1938 | { id: 'value', key: 'value', label: 'Value', sort: 'number' } 1939 | ]; 1940 | }, 1941 | link: function postLink(scope) { 1942 | scope.$watch('data', function (data) { 1943 | if (data) { 1944 | scope.items = data; 1945 | } 1946 | }); 1947 | } 1948 | }; 1949 | }); 1950 | angular.module("ui.widgets").run(["$templateCache", function($templateCache) { 1951 | 1952 | $templateCache.put("template/widgets/barChart/barChart.html", 1953 | "
\n" + 1954 | "
\n" + 1955 | " {{start|date:'HH:mm:ss'}} - {{end|date:'HH:mm:ss'}} \n" + 1956 | "
\n" + 1957 | " \n" + 1966 | " \n" + 1967 | "
" 1968 | ); 1969 | 1970 | $templateCache.put("template/widgets/historicalChart/historicalChart.html", 1971 | "
\n" + 1972 | "
\n" + 1973 | "
\n" + 1974 | " \n" + 1976 | " \n" + 1978 | "
\n" + 1979 | "
\n" + 1980 | "
\n" + 1981 | "
" 1982 | ); 1983 | 1984 | $templateCache.put("template/widgets/metricsChart/metricsChart.html", 1985 | "
\n" + 1986 | "
\n" + 1987 | " {{start|date:'HH:mm:ss'}} - {{end|date:'HH:mm:ss'}} \n" + 1988 | " \n" + 1991 | "
\n" + 1992 | " \n" + 2006 | " \n" + 2007 | "
" 2008 | ); 2009 | 2010 | $templateCache.put("template/widgets/nvd3LineChart/nvd3LineChart.html", 2011 | "
\n" + 2012 | "
\n" + 2013 | " {{start|date:'HH:mm:ss'}} - {{end|date:'HH:mm:ss'}} \n" + 2014 | "
\n" + 2015 | " \n" + 2029 | " \n" + 2030 | "
" 2031 | ); 2032 | 2033 | $templateCache.put("template/widgets/pieChart/pieChart.html", 2034 | "
\n" + 2035 | "\n" + 2044 | "\n" + 2045 | "
" 2046 | ); 2047 | 2048 | $templateCache.put("template/widgets/random/random.html", 2049 | "
\n" + 2050 | " Random Number\n" + 2051 | "
{{number}}
\n" + 2052 | "
" 2053 | ); 2054 | 2055 | $templateCache.put("template/widgets/scopeWatch/scopeWatch.html", 2056 | "
\n" + 2057 | " Value\n" + 2058 | "
{{scopeValue || 'no data'}}
\n" + 2059 | "
" 2060 | ); 2061 | 2062 | $templateCache.put("template/widgets/select/select.html", 2063 | "
\n" + 2064 | " {{label}} \n" + 2066 | " \n" + 2069 | " \n" + 2072 | "
" 2073 | ); 2074 | 2075 | $templateCache.put("template/widgets/time/time.html", 2076 | "
\n" + 2077 | " Time\n" + 2078 | "
{{time}}
\n" + 2079 | "
" 2080 | ); 2081 | 2082 | $templateCache.put("template/widgets/topN/topN.html", 2083 | "
\n" + 2084 | " \n" + 2088 | " \n" + 2089 | "
" 2090 | ); 2091 | 2092 | }]); 2093 | -------------------------------------------------------------------------------- /docs/AngularJSDashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dtpublic/malhar-angular-widgets/df8f3dacb0f6737334753c6b6df9f00dd16e7d6d/docs/AngularJSDashboard.png -------------------------------------------------------------------------------- /karma-e2e.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // http://karma-runner.github.io/0.10/config/configuration-file.html 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | // base path, that will be used to resolve files and exclude 7 | basePath: '', 8 | 9 | // testing framework to use (jasmine/mocha/qunit/...) 10 | frameworks: ['ng-scenario'], 11 | 12 | // list of files / patterns to load in the browser 13 | files: [ 14 | 'test/e2e/**/*.js' 15 | ], 16 | 17 | // list of files / patterns to exclude 18 | exclude: [], 19 | 20 | // web server port 21 | port: 8080, 22 | 23 | // level of logging 24 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 25 | logLevel: config.LOG_INFO, 26 | 27 | 28 | // enable / disable watching file and executing tests whenever any file changes 29 | autoWatch: false, 30 | 31 | 32 | // Start these browsers, currently available: 33 | // - Chrome 34 | // - ChromeCanary 35 | // - Firefox 36 | // - Opera 37 | // - Safari (only Mac) 38 | // - PhantomJS 39 | // - IE (only Windows) 40 | browsers: ['Chrome'], 41 | 42 | 43 | // Continuous Integration mode 44 | // if true, it capture browsers, run tests and exit 45 | singleRun: false 46 | 47 | // Uncomment the following lines if you are using grunt's server to run the tests 48 | // proxies: { 49 | // '/': 'http://localhost:9000/' 50 | // }, 51 | // URL root prevent conflicts with the site root 52 | // urlRoot: '_karma_' 53 | }); 54 | }; 55 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // http://karma-runner.github.io/0.10/config/configuration-file.html 3 | 4 | module.exports = function(config) { 5 | config.set({ 6 | // base path, that will be used to resolve files and exclude 7 | basePath: '', 8 | 9 | // testing framework to use (jasmine/mocha/qunit/...) 10 | frameworks: ['jasmine'], 11 | 12 | // list of files / patterns to load in the browser 13 | files: [ 14 | 'bower_components/jquery/jquery.js', 15 | 'bower_components/jquery-ui/jquery-ui.js', 16 | 'bower_components/lodash/dist/lodash.js', 17 | 'bower_components/hamsterjs/hamster.js', 18 | 'bower_components/angular/angular.js', 19 | 'bower_components/angular-mocks/angular-mocks.js', 20 | 'bower_components/malhar-angular-table/dist/mlhr-table.js', 21 | 'bower_components/angular-ui-sortable/sortable.js', 22 | 'bower_components/angular-sanitize/angular-sanitize.js', 23 | 'bower_components/angular-mousewheel/mousewheel.js', 24 | 'bower_components/angularjs-nvd3-directives/dist/angularjs-nvd3-directives.min.js', 25 | 'bower_components/pines-notify/pnotify.core.js', 26 | 'bower_components/angular-pines-notify/src/pnotify.js', 27 | 'bower_components/visibilityjs/lib/visibility.core.js', 28 | 'src/vendor/{,*/}*.js', 29 | 'src/modules.js', 30 | 'src/service/{,*/}*.js', 31 | 'src/widgets/{,*/}*.js', 32 | 'src/models/{,*/}*.js', 33 | 'template/templates.js', 34 | 'test/mock/**/*.js', 35 | 'test/spec/**/*.js' 36 | ], 37 | 38 | // list of files / patterns to exclude 39 | exclude: [], 40 | 41 | // web server port 42 | port: 8080, 43 | 44 | // level of logging 45 | // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG 46 | logLevel: config.LOG_INFO, 47 | 48 | 49 | // enable / disable watching file and executing tests whenever any file changes 50 | autoWatch: false, 51 | 52 | // Start these browsers, currently available: 53 | // - Chrome 54 | // - ChromeCanary 55 | // - Firefox 56 | // - Opera 57 | // - Safari (only Mac) 58 | // - PhantomJS 59 | // - IE (only Windows) 60 | browsers: ['PhantomJS'], 61 | 62 | // Continuous Integration mode 63 | // if true, it capture browsers, run tests and exit 64 | singleRun: false 65 | }); 66 | }; 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-ui-widgets", 3 | "version": "0.2.0", 4 | "author": "https://github.com/DataTorrent/malhar-angular-widgets/graphs/contributors", 5 | "homepage": "https://github.com/DataTorrent/malhar-angular-widgets", 6 | "license": "Apache License, v2.0", 7 | "devDependencies": { 8 | "grunt": "~0.4.1", 9 | "grunt-contrib-copy": "~0.4.1", 10 | "grunt-contrib-concat": "~0.3.0", 11 | "grunt-contrib-jshint": "~0.6.0", 12 | "grunt-contrib-connect": "~0.5.0", 13 | "grunt-contrib-clean": "~0.5.0", 14 | "grunt-contrib-watch": "~0.5.2", 15 | "grunt-angular-templates": "~0.3.0", 16 | "load-grunt-tasks": "~0.1.0", 17 | "time-grunt": "~0.1.0", 18 | "karma-ng-scenario": "~0.1.0", 19 | "grunt-karma": "~0.6.2", 20 | "karma-script-launcher": "~0.1.0", 21 | "karma-chrome-launcher": "~0.1.2", 22 | "karma-firefox-launcher": "~0.1.3", 23 | "karma-html2js-preprocessor": "~0.1.0", 24 | "karma-jasmine": "~0.1.5", 25 | "karma-coffee-preprocessor": "~0.1.2", 26 | "karma-phantomjs-launcher": "~0.1.1", 27 | "karma": "~0.10.9", 28 | "karma-ng-html2js-preprocessor": "~0.1.0" 29 | }, 30 | "engines": { 31 | "node": ">=0.8.0" 32 | }, 33 | "scripts": { 34 | "test": "grunt test" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/models/random.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 DataTorrent, Inc. ALL Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | angular.module('ui.models') 20 | .factory('RandomBaseDataModel', function (WidgetDataModel, Visibility) { 21 | function RandomBaseDataModel() { 22 | } 23 | 24 | RandomBaseDataModel.prototype = Object.create(WidgetDataModel.prototype); 25 | RandomBaseDataModel.prototype.constructor = WidgetDataModel; 26 | 27 | angular.extend(RandomBaseDataModel.prototype, { 28 | init: function () { 29 | this.stopUpdates = false; 30 | this.visibilityListener = Visibility.change(function (e, state) { 31 | if (state === 'hidden') { 32 | this.stopUpdates = true; 33 | } else { 34 | this.stopUpdates = false; 35 | } 36 | }.bind(this)); 37 | }, 38 | 39 | updateScope: function (data) { 40 | if (!this.stopUpdates) { 41 | WidgetDataModel.prototype.updateScope.call(this, data); 42 | } 43 | }, 44 | 45 | destroy: function () { 46 | WidgetDataModel.prototype.destroy.call(this); 47 | Visibility.unbind(this.visibilityListener); 48 | } 49 | }); 50 | 51 | return RandomBaseDataModel; 52 | }) 53 | .factory('RandomPercentageDataModel', function (RandomBaseDataModel, $interval) { 54 | function RandomPercentageDataModel() { 55 | } 56 | 57 | RandomPercentageDataModel.prototype = Object.create(RandomBaseDataModel.prototype); 58 | 59 | RandomPercentageDataModel.prototype.init = function () { 60 | var value = 50; 61 | 62 | this.intervalPromise = $interval(function () { 63 | value += Math.random() * 40 - 20; 64 | value = value < 0 ? 0 : value > 100 ? 100 : value; 65 | 66 | this.updateScope(value); 67 | }.bind(this), 500); 68 | }; 69 | 70 | RandomPercentageDataModel.prototype.destroy = function () { 71 | RandomBaseDataModel.prototype.destroy.call(this); 72 | $interval.cancel(this.intervalPromise); 73 | }; 74 | 75 | return RandomPercentageDataModel; 76 | }) 77 | .factory('RandomTopNDataModel', function (RandomBaseDataModel, $interval) { 78 | function RandomTopNDataModel() { 79 | } 80 | 81 | RandomTopNDataModel.prototype = Object.create(RandomBaseDataModel.prototype); 82 | 83 | RandomTopNDataModel.prototype.init = function () { 84 | this.intervalPromise = $interval(function () { 85 | var topTen = _.map(_.range(0, 10), function (index) { 86 | return { 87 | name: 'item' + index, 88 | value: Math.floor(Math.random() * 100) 89 | }; 90 | }); 91 | this.updateScope(topTen); 92 | }.bind(this), 500); 93 | }; 94 | 95 | RandomTopNDataModel.prototype.destroy = function () { 96 | RandomBaseDataModel.prototype.destroy.call(this); 97 | $interval.cancel(this.intervalPromise); 98 | }; 99 | 100 | return RandomTopNDataModel; 101 | }) 102 | .factory('RandomBaseTimeSeriesDataModel', function (RandomBaseDataModel, $interval) { 103 | function RandomTimeSeriesDataModel(options) { 104 | this.upperBound = (options && options.upperBound) ? options.upperBound : 100; 105 | this.rate = (options && options.rate) ? options.rate : Math.round(this.upperBound / 2); 106 | } 107 | 108 | RandomTimeSeriesDataModel.prototype = Object.create(RandomBaseDataModel.prototype); 109 | RandomTimeSeriesDataModel.prototype.constructor = RandomBaseDataModel; 110 | 111 | angular.extend(RandomTimeSeriesDataModel.prototype, { 112 | init: function () { 113 | RandomBaseDataModel.prototype.init.call(this); 114 | 115 | var max = 30; 116 | var upperBound = this.upperBound; 117 | var data = []; 118 | var chartValue = Math.round(upperBound / 2); 119 | var rate = this.rate; 120 | 121 | function nextValue() { 122 | chartValue += Math.random() * rate - rate / 2; 123 | chartValue = chartValue < 0 ? 0 : chartValue > upperBound ? upperBound : chartValue; 124 | return Math.round(chartValue); 125 | } 126 | 127 | var now = Date.now(); 128 | for (var i = max - 1; i >= 0; i--) { 129 | data.push({ 130 | timestamp: now - i * 1000, 131 | value: nextValue() 132 | }); 133 | } 134 | 135 | this.updateScope(data); 136 | 137 | this.intervalPromise = $interval(function () { 138 | if (data.length >= max) { 139 | data.shift(); 140 | } 141 | data.push({ 142 | timestamp: Date.now(), 143 | value: nextValue() 144 | }); 145 | 146 | this.updateScope(data); 147 | }.bind(this), 1000); 148 | }, 149 | 150 | destroy: function () { 151 | RandomBaseDataModel.prototype.destroy.call(this); 152 | $interval.cancel(this.intervalPromise); 153 | } 154 | }); 155 | 156 | return RandomTimeSeriesDataModel; 157 | }) 158 | .factory('RandomTimeSeriesDataModel', function (RandomBaseTimeSeriesDataModel) { 159 | function RandomTimeSeriesDataModel(options) { 160 | RandomBaseTimeSeriesDataModel.call(this, options); 161 | } 162 | 163 | RandomTimeSeriesDataModel.prototype = Object.create(RandomBaseTimeSeriesDataModel.prototype); 164 | 165 | angular.extend(RandomTimeSeriesDataModel.prototype, { 166 | updateScope: function (data) { 167 | var chart = { 168 | data: data, 169 | max: 30, 170 | chartOptions: { 171 | vAxis: {} 172 | } 173 | }; 174 | 175 | RandomBaseTimeSeriesDataModel.prototype.updateScope.call(this, chart); 176 | } 177 | }); 178 | 179 | return RandomTimeSeriesDataModel; 180 | }) 181 | .factory('RandomMetricsTimeSeriesDataModel', function (RandomBaseTimeSeriesDataModel) { 182 | function RandomMetricsTimeSeriesDataModel(options) { 183 | RandomBaseTimeSeriesDataModel.call(this, options); 184 | } 185 | 186 | RandomMetricsTimeSeriesDataModel.prototype = Object.create(RandomBaseTimeSeriesDataModel.prototype); 187 | 188 | angular.extend(RandomMetricsTimeSeriesDataModel.prototype, { 189 | updateScope: function (data) { 190 | var chart = [ 191 | { 192 | key: 'Stream1', 193 | values: data 194 | }, 195 | { 196 | key: 'Stream2', 197 | values: _.map(data, function (item) { 198 | return { timestamp: item.timestamp, value: item.value + 10 }; 199 | }) 200 | } 201 | ]; 202 | 203 | RandomBaseTimeSeriesDataModel.prototype.updateScope.call(this, chart); 204 | } 205 | }); 206 | 207 | return RandomMetricsTimeSeriesDataModel; 208 | }) 209 | .factory('RandomNVD3TimeSeriesDataModel', function (RandomBaseTimeSeriesDataModel) { 210 | function RandomTimeSeriesDataModel(options) { 211 | RandomBaseTimeSeriesDataModel.call(this, options); 212 | } 213 | 214 | RandomTimeSeriesDataModel.prototype = Object.create(RandomBaseTimeSeriesDataModel.prototype); 215 | 216 | angular.extend(RandomTimeSeriesDataModel.prototype, { 217 | updateScope: function (data) { 218 | var chart = [ 219 | { 220 | key: 'Data', 221 | values: data 222 | } 223 | ]; 224 | 225 | RandomBaseTimeSeriesDataModel.prototype.updateScope.call(this, chart); 226 | } 227 | }); 228 | 229 | return RandomTimeSeriesDataModel; 230 | }) 231 | .factory('RandomMinutesDataModel', function (RandomBaseDataModel, $interval) { 232 | function RandomTimeSeriesDataModel(options) { 233 | this.limit = (options && options.limit) ? options.limit : 500; 234 | } 235 | 236 | RandomTimeSeriesDataModel.prototype = Object.create(RandomBaseDataModel.prototype); 237 | 238 | RandomTimeSeriesDataModel.prototype.init = function () { 239 | this.generateChart(); 240 | this.intervalPromise = $interval(this.generateChart.bind(this), 2000); 241 | }; 242 | 243 | RandomTimeSeriesDataModel.prototype.generateChart = function () { 244 | var minuteCount = 30; 245 | var data = []; 246 | var limit = this.limit; 247 | var chartValue = limit / 2; 248 | 249 | function nextValue() { 250 | chartValue += Math.random() * (limit * 0.4) - (limit * 0.2); 251 | chartValue = chartValue < 0 ? 0 : chartValue > limit ? limit : chartValue; 252 | return chartValue; 253 | } 254 | 255 | var now = Date.now(); 256 | 257 | for (var i = minuteCount - 1; i >= 0; i--) { 258 | data.push({ 259 | timestamp: now - i * 1000 * 60, 260 | value: nextValue() 261 | }); 262 | } 263 | 264 | var widgetData = [ 265 | { 266 | key: 'Data', 267 | values: data 268 | } 269 | ]; 270 | 271 | this.updateScope(widgetData); 272 | }; 273 | 274 | RandomTimeSeriesDataModel.prototype.destroy = function () { 275 | RandomBaseDataModel.prototype.destroy.call(this); 276 | $interval.cancel(this.intervalPromise); 277 | }; 278 | 279 | return RandomTimeSeriesDataModel; 280 | }); -------------------------------------------------------------------------------- /src/models/websocket.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 DataTorrent, Inc. ALL Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | angular.module('ui.models') 20 | .factory('WebSocketDataModel', function (WidgetDataModel, webSocket) { 21 | function WebSocketDataModel() { 22 | } 23 | 24 | WebSocketDataModel.prototype = Object.create(WidgetDataModel.prototype); 25 | 26 | WebSocketDataModel.prototype.init = function () { 27 | this.topic = null; 28 | this.callback = null; 29 | if (this.dataModelOptions && this.dataModelOptions.defaultTopic) { 30 | this.update(this.dataModelOptions.defaultTopic); 31 | } 32 | }; 33 | 34 | WebSocketDataModel.prototype.update = function (newTopic) { 35 | var that = this; 36 | 37 | if (this.topic && this.callback) { 38 | webSocket.unsubscribe(this.topic, this.callback); 39 | } 40 | 41 | this.callback = function (message) { 42 | that.updateScope(message); 43 | that.widgetScope.$apply(); 44 | }; 45 | 46 | this.topic = newTopic; 47 | webSocket.subscribe(this.topic, this.callback, this.widgetScope); 48 | }; 49 | 50 | WebSocketDataModel.prototype.destroy = function () { 51 | WidgetDataModel.prototype.destroy.call(this); 52 | 53 | if (this.topic && this.callback) { 54 | webSocket.unsubscribe(this.topic, this.callback); 55 | } 56 | }; 57 | 58 | return WebSocketDataModel; 59 | }) 60 | .factory('TimeSeriesDataModel', function (WebSocketDataModel) { 61 | function TimeSeriesDataModel() { 62 | } 63 | 64 | TimeSeriesDataModel.prototype = Object.create(WebSocketDataModel.prototype); 65 | 66 | TimeSeriesDataModel.prototype.init = function () { 67 | WebSocketDataModel.prototype.init.call(this); 68 | }; 69 | 70 | TimeSeriesDataModel.prototype.update = function (newTopic) { 71 | WebSocketDataModel.prototype.update.call(this, newTopic); 72 | this.items = []; 73 | }; 74 | 75 | TimeSeriesDataModel.prototype.updateScope = function (value) { 76 | value = _.isArray(value) ? value[0] : value; 77 | 78 | this.items.push({ 79 | timestamp: parseInt(value.timestamp, 10), //TODO 80 | value: parseInt(value.value, 10) //TODO 81 | }); 82 | 83 | if (this.items.length > 100) { //TODO 84 | this.items.shift(); 85 | } 86 | 87 | var chart = { 88 | data: this.items, 89 | max: 30 90 | }; 91 | 92 | WebSocketDataModel.prototype.updateScope.call(this, chart); 93 | this.data = []; 94 | }; 95 | 96 | return TimeSeriesDataModel; 97 | }) 98 | .factory('PieChartDataModel', function (WebSocketDataModel) { 99 | function PieChartDataModel() { 100 | } 101 | 102 | PieChartDataModel.prototype = Object.create(WebSocketDataModel.prototype); 103 | 104 | PieChartDataModel.prototype.init = function () { 105 | WebSocketDataModel.prototype.init.call(this); 106 | this.data = []; 107 | }; 108 | 109 | PieChartDataModel.prototype.update = function (newTopic) { 110 | WebSocketDataModel.prototype.update.call(this, newTopic); 111 | }; 112 | 113 | PieChartDataModel.prototype.updateScope = function (value) { 114 | var sum = _.reduce(value, function (memo, item) { 115 | return memo + parseFloat(item.value); 116 | }, 0); 117 | 118 | var sectors = _.map(value, function (item) { 119 | return { 120 | key: item.label, 121 | y: item.value / sum 122 | }; 123 | }); 124 | 125 | sectors = _.sortBy(sectors, function (item) { 126 | return item.key; 127 | }); 128 | 129 | WebSocketDataModel.prototype.updateScope.call(this, sectors); 130 | }; 131 | 132 | return PieChartDataModel; 133 | }); 134 | -------------------------------------------------------------------------------- /src/modules.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 DataTorrent, Inc. ALL Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | angular.module('ui.widgets', ['datatorrent.mlhrTable', 'nvd3ChartDirectives']); 18 | angular.module('ui.websocket', ['ui.visibility', 'ui.notify']); 19 | angular.module('ui.models', ['ui.visibility', 'ui.websocket']); 20 | -------------------------------------------------------------------------------- /src/service/visibility.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 DataTorrent, Inc. ALL Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | angular.module('ui.visibility', []) 20 | .factory('Visibility', function ($window) { 21 | return $window.Visibility; 22 | }); -------------------------------------------------------------------------------- /src/service/websocket.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 DataTorrent, Inc. ALL Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | angular.module('ui.websocket') 20 | .factory('Visibility', function ($window) { 21 | return $window.Visibility; 22 | }) 23 | .provider('webSocket', function () { 24 | var visibilityTimeout = 20000; 25 | var maxConnectionAttempts = 5; 26 | var connectionAttemptInterval = 2000; 27 | var webSocketURL; 28 | var explicitConnect; 29 | var closeRequested; 30 | 31 | return { 32 | $get: function ($q, $rootScope, $timeout, notificationService, Visibility, $log, $window) { 33 | 34 | var socket; 35 | 36 | var webSocketService; 37 | 38 | var deferred = $q.defer(); 39 | 40 | var webSocketError = false; 41 | 42 | var connectionAttempts = 0; 43 | 44 | var onopen = function () { 45 | $log.info('WebSocket connection has been made. URL: ', webSocketURL); 46 | connectionAttempts = 0; 47 | deferred.resolve(); 48 | $rootScope.$apply(); 49 | }; 50 | 51 | var onclose = function() { 52 | // Reset the deferred 53 | deferred = $q.defer(); 54 | 55 | // Check if this close was requested 56 | if (!closeRequested) { 57 | 58 | // Check connectionAttempts count 59 | if (connectionAttempts < maxConnectionAttempts) { 60 | // Try to re-establish connection 61 | var url = this.url; 62 | connectionAttempts++; 63 | $timeout(function() { 64 | $log.info('Attempting to reconnect to websocket'); 65 | webSocketService.connect(url); 66 | }, connectionAttemptInterval); 67 | } 68 | 69 | else { 70 | $log.error('Could not re-establish the WebSocket connection.'); 71 | notificationService.notify({ 72 | type: 'error', 73 | title: 'Could not re-establish WebSocket Connection', 74 | text: 'The dashboard lost contact with your DataTorrent Gateway for over ' + 75 | Math.round((maxConnectionAttempts * connectionAttemptInterval)/1000) + 76 | ' seconds. Double-check your connection and try refreshing the page.' 77 | }); 78 | } 79 | 80 | } 81 | 82 | // Otherwise reset some flags 83 | else { 84 | connectionAttempts = 0; 85 | closeRequested = false; 86 | } 87 | }; 88 | 89 | // TODO: listeners for error and close, exposed on 90 | // service itself 91 | var onerror = function () { 92 | webSocketError = true; 93 | $log.error('WebSocket encountered an error'); 94 | }; 95 | 96 | var onmessage = function (event) { 97 | if (stopUpdates) { // stop updates if page is inactive 98 | return; 99 | } 100 | 101 | var message = JSON.parse(event.data); 102 | 103 | var topic = message.topic; 104 | 105 | if (topicMap.hasOwnProperty(topic)) { 106 | if ($window.WS_DEBUG) { 107 | if ($window.WS_DEBUG === true) { 108 | $log.debug('WebSocket ', topic, ' => ', message.data); 109 | } 110 | else { 111 | var search = new RegExp($window.WS_DEBUG + ''); 112 | if (search.test(topic)) { 113 | $log.debug('WebSocket ', topic, ' => ', message.data); 114 | } 115 | } 116 | } 117 | topicMap[topic].fire(message.data); 118 | } 119 | }; 120 | 121 | var topicMap = {}; // topic -> [callbacks] mapping 122 | 123 | var stopUpdates = false; 124 | 125 | 126 | if (Visibility.isSupported()) { 127 | var timeoutPromise; 128 | 129 | Visibility.change(angular.bind(this, function (e, state) { 130 | if (state === 'hidden') { 131 | timeoutPromise = $timeout(function () { 132 | stopUpdates = true; 133 | timeoutPromise = null; 134 | }, visibilityTimeout); 135 | } else { 136 | stopUpdates = false; 137 | 138 | if (timeoutPromise) { 139 | $timeout.cancel(timeoutPromise); 140 | } 141 | 142 | $log.debug('visible'); 143 | } 144 | })); 145 | } 146 | 147 | webSocketService = { 148 | send: function (message) { 149 | var msg = JSON.stringify(message); 150 | 151 | deferred.promise.then(function () { 152 | $log.debug('send ' + msg); 153 | socket.send(msg); 154 | }); 155 | }, 156 | 157 | subscribe: function (topic, callback, $scope) { 158 | var callbacks = topicMap[topic]; 159 | 160 | // If a jQuery.Callbacks object has not been created for this 161 | // topic, one should be created and a "subscribe" message 162 | // should be sent. 163 | if (!callbacks) { 164 | 165 | // send the subscribe message 166 | var message = { type: 'subscribe', topic: topic }; 167 | this.send(message); 168 | 169 | // create the Callbacks object 170 | callbacks = jQuery.Callbacks(); 171 | topicMap[topic] = callbacks; 172 | } 173 | 174 | // When scope is provided... 175 | if ($scope) { 176 | 177 | // ...it's $digest method should be called 178 | // after the callback has been triggered, so 179 | // we have to wrap the function. 180 | var wrappedCallback = function () { 181 | callback.apply({}, arguments); 182 | $scope.$digest(); 183 | }; 184 | callbacks.add(wrappedCallback); 185 | 186 | // We should also be listening for the destroy 187 | // event so we can automatically unsubscribe. 188 | $scope.$on('$destroy', angular.bind(this, function () { 189 | this.unsubscribe(topic, wrappedCallback); 190 | })); 191 | 192 | return wrappedCallback; 193 | } 194 | else { 195 | callbacks.add(callback); 196 | return callback; 197 | } 198 | }, 199 | // Unsubscribe callback is optional. If callback is omitted, all callbacks are removed. 200 | unsubscribe: function (topic, callback) { 201 | if (topicMap.hasOwnProperty(topic)) { 202 | var callbacks = topicMap[topic]; 203 | if (callback) { 204 | callbacks.remove(callback); 205 | } 206 | 207 | // If no callback is provided proceed to delete all existing callbacks 208 | // Or check callbacks.has() which will return false if there are no more handlers 209 | // registered in this callbacks collection. 210 | if (!callback || !callbacks.has()) { 211 | 212 | // Send the unsubscribe message first 213 | var message = { type: 'unsubscribe', topic: topic }; 214 | this.send(message); 215 | 216 | // Then remove the callbacks object for this topic 217 | delete topicMap[topic]; 218 | 219 | } 220 | } 221 | }, 222 | 223 | disconnect: function() { 224 | if (!socket) { 225 | return; 226 | } 227 | closeRequested = true; 228 | socket.close(); 229 | }, 230 | 231 | connect: function(url) { 232 | if (!url) { 233 | if (webSocketURL) { 234 | url = webSocketURL; 235 | } 236 | else { 237 | throw new TypeError('No WebSocket connection URL specified in connect method'); 238 | } 239 | } 240 | 241 | if (socket && socket.readyState === $window.WebSocket.OPEN) { 242 | $log.info('webSocket.connect called, but webSocket connection already established.'); 243 | return; 244 | } 245 | 246 | socket = new $window.WebSocket(url); 247 | // deferred = $q.defer(); 248 | socket.onopen = onopen; 249 | socket.onclose = onclose; 250 | socket.onerror = onerror; 251 | socket.onmessage = onmessage; 252 | 253 | // resubscribe to topics 254 | // send the subscribe message 255 | for (var topic in topicMap) { 256 | if (topicMap.hasOwnProperty(topic)) { 257 | var message = { type: 'subscribe', topic: topic }; 258 | this.send(message); 259 | } 260 | } 261 | } 262 | }; 263 | 264 | if (!explicitConnect) { 265 | webSocketService.connect(); 266 | } 267 | 268 | return webSocketService; 269 | }, 270 | 271 | setVisibilityTimeout: function (timeout) { 272 | visibilityTimeout = timeout; 273 | }, 274 | 275 | setWebSocketURL: function (wsURL) { 276 | webSocketURL = wsURL; 277 | }, 278 | 279 | setExplicitConnection: function(flag) { 280 | explicitConnect = flag; 281 | }, 282 | 283 | setMaxConnectionAttempts: function(max) { 284 | maxConnectionAttempts = max; 285 | }, 286 | 287 | setConnectionAttemptInterval: function(interval) { 288 | maxConnectionAttempts = interval; 289 | } 290 | }; 291 | }); 292 | -------------------------------------------------------------------------------- /src/vendor/gauge_vendor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copied from https://github.com/lithiumtech/angular_and_d3 3 | */ 4 | 5 | /* jshint ignore:start */ 6 | 7 | function Gauge(element, configuration) 8 | { 9 | this.element = element; 10 | 11 | var self = this; // for internal d3 functions 12 | 13 | this.configure = function(configuration) 14 | { 15 | this.config = configuration; 16 | 17 | this.config.size = this.config.size * 0.9; 18 | 19 | this.config.raduis = this.config.size * 0.97 / 2; 20 | this.config.cx = this.config.size / 2; 21 | this.config.cy = this.config.size / 2; 22 | 23 | this.config.min = undefined != configuration.min ? configuration.min : 0; 24 | this.config.max = undefined != configuration.max ? configuration.max : 100; 25 | this.config.range = this.config.max - this.config.min; 26 | 27 | this.config.majorTicks = configuration.majorTicks || 5; 28 | this.config.minorTicks = configuration.minorTicks || 2; 29 | 30 | this.config.greenColor = configuration.greenColor || "#109618"; 31 | this.config.yellowColor = configuration.yellowColor || "#FF9900"; 32 | this.config.redColor = configuration.redColor || "#DC3912"; 33 | 34 | this.config.transitionDuration = configuration.transitionDuration || 500; 35 | } 36 | 37 | this.render = function() 38 | { 39 | this.body = d3.select( this.element ) 40 | .append("svg:svg") 41 | .attr("class", "gauge") 42 | .attr("width", this.config.size) 43 | .attr("height", this.config.size); 44 | 45 | this.body.append("svg:circle") 46 | .attr("cx", this.config.cx) 47 | .attr("cy", this.config.cy) 48 | .attr("r", this.config.raduis) 49 | .style("fill", "#ccc") 50 | .style("stroke", "#000") 51 | .style("stroke-width", "0.5px"); 52 | 53 | this.body.append("svg:circle") 54 | .attr("cx", this.config.cx) 55 | .attr("cy", this.config.cy) 56 | .attr("r", 0.9 * this.config.raduis) 57 | .style("fill", "#fff") 58 | .style("stroke", "#e0e0e0") 59 | .style("stroke-width", "2px"); 60 | 61 | for (var index in this.config.greenZones) 62 | { 63 | this.drawBand(this.config.greenZones[index].from, this.config.greenZones[index].to, self.config.greenColor); 64 | } 65 | 66 | for (var index in this.config.yellowZones) 67 | { 68 | this.drawBand(this.config.yellowZones[index].from, this.config.yellowZones[index].to, self.config.yellowColor); 69 | } 70 | 71 | for (var index in this.config.redZones) 72 | { 73 | this.drawBand(this.config.redZones[index].from, this.config.redZones[index].to, self.config.redColor); 74 | } 75 | 76 | if (undefined != this.config.label) 77 | { 78 | var fontSize = Math.round(this.config.size / 9); 79 | this.body.append("svg:text") 80 | .attr("x", this.config.cx) 81 | .attr("y", this.config.cy / 2 + fontSize / 2) 82 | .attr("dy", fontSize / 2) 83 | .attr("text-anchor", "middle") 84 | .text(this.config.label) 85 | .style("font-size", fontSize + "px") 86 | .style("fill", "#333") 87 | .style("stroke-width", "0px"); 88 | } 89 | 90 | var fontSize = Math.round(this.config.size / 16); 91 | var majorDelta = this.config.range / (this.config.majorTicks - 1); 92 | for (var major = this.config.min; major <= this.config.max; major += majorDelta) 93 | { 94 | var minorDelta = majorDelta / this.config.minorTicks; 95 | for (var minor = major + minorDelta; minor < Math.min(major + majorDelta, this.config.max); minor += minorDelta) 96 | { 97 | var point1 = this.valueToPoint(minor, 0.75); 98 | var point2 = this.valueToPoint(minor, 0.85); 99 | 100 | this.body.append("svg:line") 101 | .attr("x1", point1.x) 102 | .attr("y1", point1.y) 103 | .attr("x2", point2.x) 104 | .attr("y2", point2.y) 105 | .style("stroke", "#666") 106 | .style("stroke-width", "1px"); 107 | } 108 | 109 | var point1 = this.valueToPoint(major, 0.7); 110 | var point2 = this.valueToPoint(major, 0.85); 111 | 112 | this.body.append("svg:line") 113 | .attr("x1", point1.x) 114 | .attr("y1", point1.y) 115 | .attr("x2", point2.x) 116 | .attr("y2", point2.y) 117 | .style("stroke", "#333") 118 | .style("stroke-width", "2px"); 119 | 120 | if (major == this.config.min || major == this.config.max) 121 | { 122 | var point = this.valueToPoint(major, 0.63); 123 | 124 | this.body.append("svg:text") 125 | .attr("x", point.x) 126 | .attr("y", point.y) 127 | .attr("dy", fontSize / 3) 128 | .attr("text-anchor", major == this.config.min ? "start" : "end") 129 | .text(major) 130 | .style("font-size", fontSize + "px") 131 | .style("fill", "#333") 132 | .style("stroke-width", "0px"); 133 | } 134 | } 135 | 136 | var pointerContainer = this.body.append("svg:g").attr("class", "pointerContainer"); 137 | 138 | var midValue = (this.config.min + this.config.max) / 2; 139 | 140 | var pointerPath = this.buildPointerPath(midValue); 141 | 142 | var pointerLine = d3.svg.line() 143 | .x(function(d) { return d.x }) 144 | .y(function(d) { return d.y }) 145 | .interpolate("basis"); 146 | 147 | pointerContainer.selectAll("path") 148 | .data([pointerPath]) 149 | .enter() 150 | .append("svg:path") 151 | .attr("d", pointerLine) 152 | .style("fill", "#dc3912") 153 | .style("stroke", "#c63310") 154 | .style("fill-opacity", 0.7) 155 | 156 | pointerContainer.append("svg:circle") 157 | .attr("cx", this.config.cx) 158 | .attr("cy", this.config.cy) 159 | .attr("r", 0.12 * this.config.raduis) 160 | .style("fill", "#4684EE") 161 | .style("stroke", "#666") 162 | .style("opacity", 1); 163 | 164 | var fontSize = Math.round(this.config.size / 10); 165 | pointerContainer.selectAll("text") 166 | .data([midValue]) 167 | .enter() 168 | .append("svg:text") 169 | .attr("x", this.config.cx) 170 | .attr("y", this.config.size - this.config.cy / 4 - fontSize) 171 | .attr("dy", fontSize / 2) 172 | .attr("text-anchor", "middle") 173 | .style("font-size", fontSize + "px") 174 | .style("fill", "#000") 175 | .style("stroke-width", "0px"); 176 | 177 | this.redraw(this.config.min, 0); 178 | } 179 | 180 | this.buildPointerPath = function(value) 181 | { 182 | var delta = this.config.range / 13; 183 | 184 | var head = valueToPoint(value, 0.85); 185 | var head1 = valueToPoint(value - delta, 0.12); 186 | var head2 = valueToPoint(value + delta, 0.12); 187 | 188 | var tailValue = value - (this.config.range * (1/(270/360)) / 2); 189 | var tail = valueToPoint(tailValue, 0.28); 190 | var tail1 = valueToPoint(tailValue - delta, 0.12); 191 | var tail2 = valueToPoint(tailValue + delta, 0.12); 192 | 193 | return [head, head1, tail2, tail, tail1, head2, head]; 194 | 195 | function valueToPoint(value, factor) 196 | { 197 | var point = self.valueToPoint(value, factor); 198 | point.x -= self.config.cx; 199 | point.y -= self.config.cy; 200 | return point; 201 | } 202 | } 203 | 204 | this.drawBand = function(start, end, color) 205 | { 206 | if (0 >= end - start) return; 207 | 208 | this.body.append("svg:path") 209 | .style("fill", color) 210 | .attr("d", d3.svg.arc() 211 | .startAngle(this.valueToRadians(start)) 212 | .endAngle(this.valueToRadians(end)) 213 | .innerRadius(0.65 * this.config.raduis) 214 | .outerRadius(0.85 * this.config.raduis)) 215 | .attr("transform", function() { return "translate(" + self.config.cx + ", " + self.config.cy + ") rotate(270)" }); 216 | } 217 | 218 | this.redraw = function(value, transitionDuration) 219 | { 220 | var pointerContainer = this.body.select(".pointerContainer"); 221 | 222 | pointerContainer.selectAll("text").text(Math.round(value)); 223 | 224 | var pointer = pointerContainer.selectAll("path"); 225 | pointer.transition() 226 | .duration(undefined != transitionDuration ? transitionDuration : this.config.transitionDuration) 227 | //.delay(0) 228 | //.ease("linear") 229 | //.attr("transform", function(d) 230 | .attrTween("transform", function() 231 | { 232 | var pointerValue = value; 233 | if (value > self.config.max) pointerValue = self.config.max + 0.02*self.config.range; 234 | else if (value < self.config.min) pointerValue = self.config.min - 0.02*self.config.range; 235 | var targetRotation = (self.valueToDegrees(pointerValue) - 90); 236 | var currentRotation = self._currentRotation || targetRotation; 237 | self._currentRotation = targetRotation; 238 | 239 | return function(step) 240 | { 241 | var rotation = currentRotation + (targetRotation-currentRotation)*step; 242 | return "translate(" + self.config.cx + ", " + self.config.cy + ") rotate(" + rotation + ")"; 243 | } 244 | }); 245 | } 246 | 247 | this.valueToDegrees = function(value) 248 | { 249 | // thanks @closealert 250 | //return value / this.config.range * 270 - 45; 251 | return value / this.config.range * 270 - (this.config.min / this.config.range * 270 + 45); 252 | } 253 | 254 | this.valueToRadians = function(value) 255 | { 256 | return this.valueToDegrees(value) * Math.PI / 180; 257 | } 258 | 259 | this.valueToPoint = function(value, factor) 260 | { 261 | return { x: this.config.cx - this.config.raduis * factor * Math.cos(this.valueToRadians(value)), 262 | y: this.config.cy - this.config.raduis * factor * Math.sin(this.valueToRadians(value)) }; 263 | } 264 | 265 | // initialization 266 | this.configure(configuration); 267 | } 268 | 269 | /* jshint ignore:end */ -------------------------------------------------------------------------------- /src/vendor/visibly.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * visibly - v0.6 Aug 2011 - Page Visibility API Polyfill 3 | * http://github.com/addyosmani 4 | * Copyright (c) 2011 Addy Osmani 5 | * Dual licensed under the MIT and GPL licenses. 6 | * 7 | * Methods supported: 8 | * visibly.onVisible(callback) 9 | * visibly.onHidden(callback) 10 | * visibly.hidden() 11 | * visibly.visibilityState() 12 | * visibly.visibilitychange(callback(state)); 13 | */ 14 | 15 | ;(function () { 16 | 17 | window.visibly = { 18 | q: document, 19 | p: undefined, 20 | prefixes: ['webkit', 'ms','o','moz','khtml'], 21 | props: ['VisibilityState', 'visibilitychange', 'Hidden'], 22 | m: ['focus', 'blur'], 23 | visibleCallbacks: [], 24 | hiddenCallbacks: [], 25 | genericCallbacks:[], 26 | _callbacks: [], 27 | cachedPrefix:"", 28 | fn:null, 29 | 30 | onVisible: function (_callback) { 31 | if(typeof _callback == 'function' ){ 32 | this.visibleCallbacks.push(_callback); 33 | } 34 | }, 35 | onHidden: function (_callback) { 36 | if(typeof _callback == 'function' ){ 37 | this.hiddenCallbacks.push(_callback); 38 | } 39 | }, 40 | getPrefix:function(){ 41 | if(!this.cachedPrefix){ 42 | var b; 43 | for(var l=0;b=this.prefixes[l++];){ 44 | if(b + this.props[2] in this.q){ 45 | this.cachedPrefix = b; 46 | return this.cachedPrefix; 47 | } 48 | } 49 | } 50 | }, 51 | 52 | visibilityState:function(){ 53 | return this._getProp(0); 54 | }, 55 | hidden:function(){ 56 | return this._getProp(2); 57 | }, 58 | visibilitychange:function(fn){ 59 | if(typeof fn == 'function' ){ 60 | this.genericCallbacks.push(fn); 61 | } 62 | 63 | var n = this.genericCallbacks.length; 64 | if(n){ 65 | if(this.cachedPrefix){ 66 | while(n--){ 67 | this.genericCallbacks[n].call(this, this.visibilityState()); 68 | } 69 | }else{ 70 | while(n--){ 71 | this.genericCallbacks[n].call(this, arguments[0]); 72 | } 73 | } 74 | } 75 | 76 | }, 77 | isSupported: function (index) { 78 | return ((this.cachedPrefix + this.props[2]) in this.q); 79 | }, 80 | _getProp:function(index){ 81 | return this.q[this.cachedPrefix + this.props[index]]; 82 | }, 83 | _execute: function (index) { 84 | if (index) { 85 | this._callbacks = (index == 1) ? this.visibleCallbacks : this.hiddenCallbacks; 86 | var n = this._callbacks.length; 87 | while(n--){ 88 | this._callbacks[n](); 89 | } 90 | } 91 | }, 92 | _visible: function () { 93 | window.visibly._execute(1); 94 | window.visibly.visibilitychange.call(window.visibly, 'visible'); 95 | }, 96 | _hidden: function () { 97 | window.visibly._execute(2); 98 | window.visibly.visibilitychange.call(window.visibly, 'hidden'); 99 | }, 100 | _nativeSwitch: function () { 101 | this[this._getProp(2) ? '_hidden' : '_visible'](); 102 | }, 103 | _listen: function () { 104 | try { /*if no native page visibility support found..*/ 105 | if (!(this.isSupported())) { 106 | if (this.q.addEventListener) { /*for browsers without focusin/out support eg. firefox, opera use focus/blur*/ 107 | window.addEventListener(this.m[0], this._visible, 1); 108 | window.addEventListener(this.m[1], this._hidden, 1); 109 | } else { /*IE <10s most reliable focus events are onfocusin/onfocusout*/ 110 | if (this.q.attachEvent) { 111 | this.q.attachEvent('onfocusin', this._visible); 112 | this.q.attachEvent('onfocusout', this._hidden); 113 | } 114 | } 115 | } else { /*switch support based on prefix detected earlier*/ 116 | this.q.addEventListener(this.cachedPrefix + this.props[1], function () { 117 | window.visibly._nativeSwitch.apply(window.visibly, arguments); 118 | }, 1); 119 | } 120 | } catch (e) {} 121 | }, 122 | init: function () { 123 | this.getPrefix(); 124 | this._listen(); 125 | } 126 | }; 127 | 128 | this.visibly.init(); 129 | })(); 130 | -------------------------------------------------------------------------------- /src/widgets/barChart/barChart.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 DataTorrent, Inc. ALL Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | angular.module('ui.widgets') 20 | .directive('wtBarChart', function ($filter) { 21 | return { 22 | restrict: 'A', 23 | replace: true, 24 | templateUrl: 'template/widgets/barChart/barChart.html', 25 | scope: { 26 | data: '=data' 27 | }, 28 | controller: function ($scope) { 29 | var filter = $filter('date'); 30 | 31 | $scope.xAxisTickFormatFunction = function () { 32 | return function(d) { 33 | return filter(d, 'HH:mm'); 34 | }; 35 | }; 36 | 37 | $scope.xFunction = function(){ 38 | return function(d) { 39 | return d.timestamp; 40 | }; 41 | }; 42 | $scope.yFunction = function(){ 43 | return function(d) { 44 | return d.value; 45 | }; 46 | }; 47 | }, 48 | link: function postLink(scope) { 49 | scope.$watch('data', function (data) { 50 | if (data && data[0] && data[0].values && (data[0].values.length > 1)) { 51 | var timeseries = _.sortBy(data[0].values, function (item) { 52 | return item.timestamp; 53 | }); 54 | 55 | var start = timeseries[0].timestamp; 56 | var end = timeseries[timeseries.length - 1].timestamp; 57 | scope.start = start; 58 | scope.end = end; 59 | } 60 | }); 61 | } 62 | }; 63 | }); -------------------------------------------------------------------------------- /src/widgets/gauge/gauge.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 DataTorrent, Inc. ALL Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | angular.module('ui.widgets') 20 | .directive('wtGauge', function () { 21 | return { 22 | replace: true, 23 | scope: { 24 | label: '@', 25 | min: '=', 26 | max: '=', 27 | value: '=' 28 | }, 29 | link: function (scope, element, attrs) { 30 | var config = { 31 | size: 200, 32 | label: attrs.label, 33 | min: undefined !== scope.min ? scope.min : 0, 34 | max: undefined !== scope.max ? scope.max : 100, 35 | minorTicks: 5 36 | }; 37 | 38 | var range = config.max - config.min; 39 | config.yellowZones = [ 40 | { from: config.min + range * 0.75, to: config.min + range * 0.9 } 41 | ]; 42 | config.redZones = [ 43 | { from: config.min + range * 0.9, to: config.max } 44 | ]; 45 | 46 | scope.gauge = new Gauge(element[0], config); 47 | scope.gauge.render(); 48 | 49 | function update(value) { 50 | var percentage; 51 | if (_.isString(value)) { 52 | percentage = parseFloat(value); 53 | } else if (_.isNumber(value)) { 54 | percentage = value; 55 | } 56 | 57 | if (!_.isUndefined(percentage)) { 58 | scope.gauge.redraw(percentage); 59 | } 60 | } 61 | 62 | update(0); 63 | 64 | scope.$watch('value', function (value) { 65 | if (scope.gauge) { 66 | update(value); 67 | } 68 | }); 69 | } 70 | }; 71 | }); -------------------------------------------------------------------------------- /src/widgets/historicalChart/historicalChart.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 DataTorrent, Inc. ALL Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | angular.module('ui.widgets') 20 | .directive('wtHistoricalChart', function () { 21 | return { 22 | restrict: 'A', 23 | replace: true, 24 | templateUrl: 'template/widgets/historicalChart/historicalChart.html', 25 | scope: { 26 | chart: '=' 27 | }, 28 | controller: function ($scope) { 29 | $scope.mode = 'MINUTES'; 30 | 31 | $scope.changeMode = function (mode) { 32 | $scope.mode = mode; 33 | $scope.$emit('modeChanged', mode); 34 | }; 35 | } 36 | }; 37 | }); -------------------------------------------------------------------------------- /src/widgets/lineChart/lineChart.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 DataTorrent, Inc. ALL Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | angular.module('ui.widgets') 20 | .directive('wtLineChart', function () { 21 | return { 22 | template: '
', 23 | scope: { 24 | chart: '=' 25 | }, 26 | replace: true, 27 | link: function postLink(scope, element) { 28 | var lineChart = new google.visualization.LineChart(element[0]); 29 | 30 | function draw(chart) { 31 | var data = chart.data; 32 | 33 | var table = new google.visualization.DataTable(); 34 | table.addColumn('datetime'); 35 | table.addColumn('number'); 36 | table.addRows(data.length); 37 | 38 | var view = new google.visualization.DataView(table); 39 | 40 | for (var i = 0; i < data.length; i++) { 41 | var item = data[i]; 42 | table.setCell(i, 0, new Date(item.timestamp)); 43 | var value = parseFloat(item.value); 44 | table.setCell(i, 1, value); 45 | } 46 | 47 | var chartOptions = { 48 | legend: 'none', 49 | vAxis: { minValue: 0, maxValue: 100 } 50 | //chartArea: { top: 20, left: 30, height: 240 } 51 | }; 52 | 53 | if (chart.max) { 54 | var lastTimestamp; 55 | 56 | if (data.length) { 57 | var last = data[data.length - 1]; 58 | lastTimestamp = last.timestamp; 59 | } else { 60 | lastTimestamp = Date.now(); 61 | } 62 | 63 | var max = new Date(lastTimestamp); 64 | var min = new Date(lastTimestamp - (chart.max - 1) * 1000); 65 | 66 | angular.extend(chartOptions, { 67 | hAxis: { viewWindow: { min: min, max: max }} 68 | }); 69 | } 70 | 71 | if (chart.chartOptions) { 72 | angular.extend(chartOptions, chart.chartOptions); 73 | } 74 | 75 | lineChart.draw(view, chartOptions); 76 | } 77 | 78 | scope.$watch('chart', function (chart) { 79 | if (!chart) { 80 | chart = { 81 | data: [], 82 | max: 30 83 | }; 84 | } 85 | 86 | if (chart.data) { 87 | draw(chart); 88 | } 89 | }); 90 | } 91 | }; 92 | }); -------------------------------------------------------------------------------- /src/widgets/metricsChart/metricsChart.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 DataTorrent, Inc. ALL Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | angular.module('ui.widgets') 20 | .directive('wtMetricsChart', function ($filter, MetricsChartHistory) { 21 | return { 22 | restrict: 'A', 23 | replace: true, 24 | templateUrl: 'template/widgets/metricsChart/metricsChart.html', 25 | scope: { 26 | data: '=?', 27 | metrics: '=', 28 | controller: '=' 29 | }, 30 | controller: function ($scope) { 31 | var filter = $filter('date'); 32 | var yAxisFilter = $filter('number'); 33 | 34 | $scope.xAxisTickFormatFunction = function () { 35 | return function (d) { 36 | return filter(d, 'mm:ss'); 37 | }; 38 | }; 39 | 40 | $scope.yAxisTickFormatFunction = function () { 41 | return function (d) { 42 | return yAxisFilter(d); 43 | }; 44 | }; 45 | 46 | $scope.xFunction = function () { 47 | return function (d) { 48 | return d.timestamp; 49 | }; 50 | }; 51 | $scope.yFunction = function () { 52 | return function (d) { 53 | return d.value; 54 | }; 55 | }; 56 | 57 | $scope.chartCallback = function () { // callback to access nvd3 chart 58 | //console.log('chartCallback'); 59 | //console.log(arguments); 60 | //console.log(chart.legend.dispatch.); 61 | //chart.legend.dispatch.on('legendClick', function(newState) { 62 | // console.log(newState); 63 | //}); 64 | }; 65 | 66 | $scope.maxTimeLimit = 300; 67 | 68 | $scope.options = [ 69 | { 70 | value: 30, 71 | label: 'last 30 seconds' 72 | }, 73 | { 74 | value: 60, 75 | label: 'last minute' 76 | }, 77 | { 78 | value: 120, 79 | label: 'last two minutes' 80 | }, 81 | { 82 | value: $scope.maxTimeLimit, 83 | label: 'last 5 minutes' 84 | } 85 | ]; 86 | $scope.timeFrame = $scope.options[0]; 87 | 88 | 89 | var chartHistory = null; 90 | if ($scope.controller) { 91 | chartHistory = new MetricsChartHistory($scope, $scope.metrics, $scope.maxTimeLimit, $scope.timeFrame.value); 92 | $scope.controller.addPoint = function (point) { 93 | chartHistory.addPoint(point); 94 | }; 95 | } 96 | 97 | $scope.timeFrameChanged = function (newTimeFrame) { 98 | if (chartHistory) { 99 | chartHistory.updateChart(Date.now(), newTimeFrame.value); 100 | } 101 | }; 102 | }, 103 | link: function postLink(scope) { 104 | scope.data = []; 105 | } 106 | }; 107 | }) 108 | .factory('MetricsChartHistory', function () { 109 | function MetricsChartHistory(scope, metrics, maxTimeLimit, timeLimit) { 110 | this.scope = scope; 111 | this.metrics = metrics; 112 | this.maxTimeLimit = maxTimeLimit; 113 | this.timeLimit = timeLimit; 114 | this.history = []; 115 | 116 | this.series = []; 117 | 118 | _.each(metrics, function (metric) { 119 | this.series.push({ 120 | key: metric.key, 121 | disabled: !metric.visible, 122 | color: metric.color 123 | }); 124 | }.bind(this)); 125 | } 126 | 127 | angular.extend(MetricsChartHistory.prototype, { 128 | updateHistory: function (now, point) { 129 | var historyStartTime = now - this.maxTimeLimit * 1000; 130 | 131 | var ind = _.findIndex(this.history, function (historyPoint) { 132 | return historyPoint.timestamp >= historyStartTime; 133 | }); 134 | if (ind > 1) { 135 | this.history = _.rest(this.history, ind - 1); 136 | } 137 | 138 | var historyPoint = { 139 | timestamp: now, 140 | data: point 141 | }; 142 | this.history.push(historyPoint); 143 | }, 144 | 145 | updateChart: function (now, timeLimit) { 146 | this.timeLimit = timeLimit; 147 | 148 | var startTime = now - 1000 * timeLimit; 149 | 150 | var history = _.filter(this.history, function (historyPoint) { //TODO optimize 151 | return historyPoint.timestamp >= startTime; 152 | }); 153 | 154 | _.each(this.metrics, function (metric, index) { 155 | var metricKey = metric.key; 156 | 157 | var values = _.map(history, function (historyPoint) { 158 | return { 159 | timestamp: historyPoint.timestamp, 160 | value: Math.round(parseInt(historyPoint.data[metricKey], 10)) 161 | }; 162 | }); 163 | 164 | this.series[index].values = values; 165 | }.bind(this)); 166 | 167 | /* 168 | //TODO this is workaround to have fixed x axis scale when no enough date is available 169 | chart.push({ 170 | key: 'Left Value', 171 | values: [ 172 | {timestamp: startTime, value: 0} 173 | ] 174 | }); 175 | */ 176 | 177 | /* 178 | var max = _.max(history, function (historyPoint) { //TODO optimize 179 | return historyPoint.stats.tuplesEmittedPSMA; //TODO 180 | }); 181 | 182 | chart.push({ 183 | key: 'Upper Value', 184 | values: [ 185 | {timestamp: now - 30 * 1000, value: Math.round(max.value * 1.2)} 186 | ] 187 | }); 188 | */ 189 | 190 | if (history.length > 1) { 191 | this.scope.data = _.clone(this.series); 192 | this.scope.start = Math.min(startTime, _.first(history).timestamp); 193 | this.scope.end = _.last(history).timestamp; 194 | } 195 | }, 196 | 197 | addPoint: function (point) { 198 | var now = Date.now(); 199 | this.updateHistory(now, point); 200 | 201 | this.updateChart(now, this.timeLimit); 202 | } 203 | }); 204 | 205 | return MetricsChartHistory; 206 | }); -------------------------------------------------------------------------------- /src/widgets/nvd3LineChart/nvd3LineChart.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 DataTorrent, Inc. ALL Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | angular.module('ui.widgets') 20 | .directive('wtNvd3LineChart', function ($filter) { 21 | return { 22 | restrict: 'A', 23 | replace: true, 24 | templateUrl: 'template/widgets/nvd3LineChart/nvd3LineChart.html', 25 | scope: { 26 | data: '=data', 27 | showLegend: '@', 28 | showTimeRange: '=?', 29 | timeAxisFormat: '=?' 30 | }, 31 | controller: function ($scope) { 32 | var filter = $filter('date'); 33 | var numberFilter = $filter('number'); 34 | 35 | $scope.xAxisTickFormatFunction = function () { 36 | return function (d) { 37 | return filter(d, $scope.timeAxisFormat); 38 | }; 39 | }; 40 | 41 | $scope.yAxisTickFormatFunction = function () { 42 | return function (d) { 43 | if (d > 999) { 44 | var value; 45 | var scale; 46 | if (d < 999999) { 47 | value = Math.round(d/1000); 48 | scale = 'k'; 49 | } else { 50 | value = Math.round(d/1000000); 51 | scale = 'm'; 52 | } 53 | return numberFilter(value) + scale; 54 | } else { 55 | return numberFilter(d); 56 | } 57 | }; 58 | }; 59 | 60 | $scope.xFunction = function () { 61 | return function (d) { 62 | return d.timestamp; 63 | }; 64 | }; 65 | $scope.yFunction = function () { 66 | return function (d) { 67 | return d.value; 68 | }; 69 | }; 70 | }, 71 | link: function postLink(scope, element, attrs) { 72 | if (!_.has(attrs, 'showTimeRange')) { 73 | scope.showTimeRange = true; 74 | } 75 | 76 | scope.timeAxisFormat = scope.timeAxisFormat || 'HH:mm'; 77 | 78 | scope.$watch('data', function (data) { 79 | if (data && data[0] && data[0].values && (data[0].values.length > 1)) { 80 | var timeseries = _.sortBy(data[0].values, function (item) { 81 | return item.timestamp; 82 | }); 83 | 84 | var start = timeseries[0].timestamp; 85 | var end = timeseries[timeseries.length - 1].timestamp; 86 | scope.start = start; 87 | scope.end = end; 88 | } 89 | }); 90 | } 91 | }; 92 | }); -------------------------------------------------------------------------------- /src/widgets/pieChart/pieChart.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 DataTorrent, Inc. ALL Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | angular.module('ui.widgets') 20 | .directive('wtPieChart', function () { 21 | return { 22 | restrict: 'A', 23 | replace: true, 24 | templateUrl: 'template/widgets/pieChart/pieChart.html', 25 | scope: { 26 | data: '=data' 27 | }, 28 | controller: function ($scope) { 29 | $scope.xFunction = function(){ 30 | return function(d) { 31 | return d.key; 32 | }; 33 | }; 34 | $scope.yFunction = function(){ 35 | return function(d) { 36 | return d.y; 37 | }; 38 | }; 39 | 40 | $scope.descriptionFunction = function(){ 41 | return function(d){ 42 | return d.key + ' ' + d.y; 43 | }; 44 | }; 45 | } 46 | }; 47 | }); -------------------------------------------------------------------------------- /src/widgets/random/random.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 DataTorrent, Inc. ALL Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | angular.module('ui.widgets') 20 | .directive('wtRandom', function ($interval) { 21 | return { 22 | restrict: 'A', 23 | replace: true, 24 | templateUrl: 'template/widgets/random/random.html', 25 | link: function postLink(scope) { 26 | function update() { 27 | scope.number = Math.floor(Math.random() * 100); 28 | } 29 | 30 | var promise = $interval(update, 500); 31 | 32 | scope.$on('$destroy', function () { 33 | $interval.cancel(promise); 34 | }); 35 | } 36 | }; 37 | }); -------------------------------------------------------------------------------- /src/widgets/scopeWatch/scopeWatch.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 DataTorrent, Inc. ALL Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | angular.module('ui.widgets') 20 | .directive('wtScopeWatch', function () { 21 | return { 22 | restrict: 'A', 23 | replace: true, 24 | templateUrl: 'template/widgets/scopeWatch/scopeWatch.html', 25 | scope: { 26 | scopeValue: '=value', 27 | valueClass: '@valueClass' 28 | } 29 | }; 30 | }); -------------------------------------------------------------------------------- /src/widgets/select/select.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 DataTorrent, Inc. ALL Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | angular.module('ui.widgets') 20 | .directive('wtSelect', function () { 21 | return { 22 | restrict: 'A', 23 | replace: true, 24 | templateUrl: 'template/widgets/select/select.html', 25 | scope: { 26 | label: '@', 27 | value: '=', 28 | options: '=' 29 | }, 30 | controller: function ($scope) { 31 | $scope.prevValue = function () { 32 | var index = $scope.options.indexOf($scope.value); 33 | var nextIndex = (index - 1 + $scope.options.length) % $scope.options.length; 34 | $scope.value = $scope.options[nextIndex]; 35 | }; 36 | 37 | $scope.nextValue = function () { 38 | var index = $scope.options.indexOf($scope.value); 39 | var nextIndex = (index + 1) % $scope.options.length; 40 | $scope.value = $scope.options[nextIndex]; 41 | }; 42 | } 43 | }; 44 | }); -------------------------------------------------------------------------------- /src/widgets/time/time.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 DataTorrent, Inc. ALL Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | angular.module('ui.widgets') 20 | .directive('wtTime', function ($interval) { 21 | return { 22 | restrict: 'A', 23 | replace: true, 24 | templateUrl: 'template/widgets/time/time.html', 25 | link: function (scope) { 26 | function update() { 27 | scope.time = new Date().toLocaleTimeString(); 28 | } 29 | 30 | var promise = $interval(update, 500); 31 | 32 | scope.$on('$destroy', function () { 33 | $interval.cancel(promise); 34 | }); 35 | } 36 | }; 37 | }); -------------------------------------------------------------------------------- /src/widgets/topN/topN.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 DataTorrent, Inc. ALL Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | angular.module('ui.widgets') 20 | .directive('wtTopN', function () { 21 | return { 22 | restrict: 'A', 23 | replace: true, 24 | templateUrl: 'template/widgets/topN/topN.html', 25 | scope: { 26 | data: '=' 27 | }, 28 | controller: function ($scope) { 29 | $scope.tableOptions = { 30 | initialSorts: [ 31 | { id: 'value', dir: '-' } 32 | ] 33 | }; 34 | $scope.columns = [ 35 | { id: 'name', key: 'name', label: 'Name' }, 36 | { id: 'value', key: 'value', label: 'Value', sort: 'number' } 37 | ]; 38 | }, 39 | link: function postLink(scope) { 40 | scope.$watch('data', function (data) { 41 | if (data) { 42 | scope.items = data; 43 | } 44 | }); 45 | } 46 | }; 47 | }); -------------------------------------------------------------------------------- /template/widgets/barChart/barChart.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{start|date:'HH:mm:ss'}} - {{end|date:'HH:mm:ss'}}  4 |
5 | 14 | 15 |
-------------------------------------------------------------------------------- /template/widgets/historicalChart/historicalChart.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 6 | 8 |
9 |
10 |
11 |
-------------------------------------------------------------------------------- /template/widgets/metricsChart/metricsChart.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{start|date:'HH:mm:ss'}} - {{end|date:'HH:mm:ss'}}  4 | 7 |
8 | 22 | 23 |
-------------------------------------------------------------------------------- /template/widgets/nvd3LineChart/nvd3LineChart.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{start|date:'HH:mm:ss'}} - {{end|date:'HH:mm:ss'}}  4 |
5 | 19 | 20 |
-------------------------------------------------------------------------------- /template/widgets/pieChart/pieChart.html: -------------------------------------------------------------------------------- 1 |
2 | 11 | 12 |
-------------------------------------------------------------------------------- /template/widgets/random/random.html: -------------------------------------------------------------------------------- 1 |
2 | Random Number 3 |
{{number}}
4 |
-------------------------------------------------------------------------------- /template/widgets/scopeWatch/scopeWatch.html: -------------------------------------------------------------------------------- 1 |
2 | Value 3 |
{{scopeValue || 'no data'}}
4 |
-------------------------------------------------------------------------------- /template/widgets/select/select.html: -------------------------------------------------------------------------------- 1 |
2 | {{label}} 4 | 7 | 10 |
-------------------------------------------------------------------------------- /template/widgets/time/time.html: -------------------------------------------------------------------------------- 1 |
2 | Time 3 |
{{time}}
4 |
-------------------------------------------------------------------------------- /template/widgets/topN/topN.html: -------------------------------------------------------------------------------- 1 |
2 | 6 | 7 |
-------------------------------------------------------------------------------- /test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": true, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "regexp": true, 16 | "undef": true, 17 | "unused": true, 18 | "strict": true, 19 | "trailing": true, 20 | "smarttabs": true, 21 | "globals": { 22 | "after": false, 23 | "afterEach": false, 24 | "angular": false, 25 | "before": false, 26 | "beforeEach": false, 27 | "browser": false, 28 | "describe": false, 29 | "expect": false, 30 | "inject": false, 31 | "it": false, 32 | "spyOn": false 33 | } 34 | } 35 | 36 | -------------------------------------------------------------------------------- /test/runner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | End2end Test Runner 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/spec/webSocket_test.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013 DataTorrent, Inc. ALL Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 'use strict'; 17 | 18 | describe('Service: webSocket', function () { 19 | 20 | var webSocket, webSocketObject, notificationService, $rootScope, $timeout, WebSocket, Visibility, visibilityIsSupported, $window, wsUrl; 21 | 22 | describe('when explicitConnection is disabled', function() { 23 | 24 | describe('when url is provided by calling provider.setWebSocketURL', function() { 25 | 26 | beforeEach(module('ui.websocket', function($provide, webSocketProvider) { 27 | 28 | WebSocket = function(url, protocol) { 29 | webSocketObject = this; 30 | wsUrl = url; 31 | }; 32 | 33 | Visibility = { 34 | change: jasmine.createSpy(), 35 | isSupported: function() { 36 | return visibilityIsSupported; 37 | } 38 | }; 39 | 40 | visibilityIsSupported = true; 41 | 42 | $provide.value('$window', $window = { 43 | Visibility: Visibility, 44 | WebSocket: WebSocket 45 | }); 46 | 47 | spyOn($window, 'WebSocket').andCallThrough(); 48 | 49 | webSocketProvider.setWebSocketURL('ws://testing.com'); 50 | 51 | })); 52 | 53 | beforeEach(inject(function (_webSocket_, _notificationService_, _$rootScope_) { 54 | webSocket = _webSocket_; 55 | notificationService = _notificationService_; 56 | $rootScope = _$rootScope_; 57 | })); 58 | 59 | it('should instantiate a WebSocket immediately with the url set with provider.setWebSocketURL', function() { 60 | expect($window.WebSocket).toHaveBeenCalledWith('ws://testing.com'); 61 | }); 62 | 63 | }); 64 | 65 | describe('when the url is not provided by calling provider.setWebSocketURL', function() { 66 | 67 | beforeEach(module('ui.websocket', function($provide, webSocketProvider) { 68 | 69 | WebSocket = function(url, protocol) { 70 | webSocketObject = this; 71 | wsUrl = url; 72 | }; 73 | 74 | Visibility = { 75 | change: jasmine.createSpy(), 76 | isSupported: function() { 77 | return visibilityIsSupported; 78 | } 79 | }; 80 | 81 | visibilityIsSupported = true; 82 | 83 | $provide.value('$window', $window = { 84 | Visibility: Visibility, 85 | WebSocket: WebSocket 86 | }); 87 | 88 | spyOn($window, 'WebSocket').andCallThrough(); 89 | 90 | })); 91 | 92 | it('should throw if a URL was not set with provider.setWebSocketURL', function() { 93 | expect(function() { 94 | inject(function(webSocket) { 95 | // should not get here 96 | }); 97 | }).toThrow(); 98 | }); 99 | 100 | }); 101 | 102 | }); 103 | 104 | describe('when explicitConnection is enabled', function() { 105 | 106 | describe('when url is provided by calling provider.setWebSocketURL', function() { 107 | 108 | beforeEach(module('ui.websocket', function($provide, webSocketProvider) { 109 | 110 | WebSocket = function(url, protocol) { 111 | webSocketObject = this; 112 | wsUrl = url; 113 | }; 114 | 115 | Visibility = { 116 | change: jasmine.createSpy(), 117 | isSupported: function() { 118 | return visibilityIsSupported; 119 | } 120 | }; 121 | 122 | visibilityIsSupported = true; 123 | 124 | $provide.value('$window', $window = { 125 | Visibility: Visibility, 126 | WebSocket: WebSocket 127 | }); 128 | 129 | spyOn($window, 'WebSocket').andCallThrough(); 130 | 131 | webSocketProvider.setExplicitConnection(true); 132 | 133 | webSocketProvider.setWebSocketURL('ws://testing.com'); 134 | 135 | })); 136 | 137 | beforeEach(inject(function (_webSocket_, _notificationService_, _$rootScope_) { 138 | webSocket = _webSocket_; 139 | notificationService = _notificationService_; 140 | $rootScope = _$rootScope_; 141 | })); 142 | 143 | it('should not call WebSocket immediately', function() { 144 | expect($window.WebSocket).not.toHaveBeenCalled(); 145 | }); 146 | 147 | it('should instantiate a WebSocket with the url set with provider.setWebSocketURL if no url arg is passed to it', function() { 148 | webSocket.connect(); 149 | expect($window.WebSocket).toHaveBeenCalledWith('ws://testing.com'); 150 | }); 151 | 152 | }); 153 | 154 | describe('when the url is not provided by calling provider.setWebSocketURL', function() { 155 | 156 | beforeEach(module('ui.websocket', function($provide, webSocketProvider) { 157 | 158 | WebSocket = function(url, protocol) { 159 | webSocketObject = this; 160 | wsUrl = url; 161 | }; 162 | 163 | Visibility = { 164 | change: jasmine.createSpy(), 165 | isSupported: function() { 166 | return visibilityIsSupported; 167 | } 168 | }; 169 | 170 | visibilityIsSupported = true; 171 | 172 | $provide.value('$window', $window = { 173 | Visibility: Visibility, 174 | WebSocket: WebSocket 175 | }); 176 | 177 | spyOn($window, 'WebSocket').andCallThrough(); 178 | 179 | webSocketProvider.setExplicitConnection(true); 180 | 181 | })); 182 | 183 | beforeEach(inject(function (_webSocket_, _notificationService_, _$rootScope_) { 184 | webSocket = _webSocket_; 185 | notificationService = _notificationService_; 186 | $rootScope = _$rootScope_; 187 | })); 188 | 189 | it('should not call WebSocket immediately', function() { 190 | expect($window.WebSocket).not.toHaveBeenCalled(); 191 | }); 192 | 193 | it('should instantiate a WebSocket with the provided url', function() { 194 | webSocket.connect('ws://my-other-url.com'); 195 | expect($window.WebSocket).toHaveBeenCalledWith('ws://my-other-url.com'); 196 | }); 197 | 198 | it('should throw if no url is passed and no url was set with provider.setWebSocketURL', function() { 199 | expect(function() { 200 | webSocket.connect(); 201 | }).toThrow(); 202 | }); 203 | 204 | }); 205 | 206 | }); 207 | 208 | describe('the send method', function() { 209 | 210 | var wsUrl; 211 | 212 | beforeEach(module('ui.websocket', function($provide, webSocketProvider) { 213 | 214 | WebSocket = function(url, protocol) { 215 | webSocketObject = this; 216 | wsUrl = url; 217 | this.send = jasmine.createSpy(); 218 | }; 219 | 220 | Visibility = { 221 | change: jasmine.createSpy(), 222 | isSupported: function() { 223 | return visibilityIsSupported; 224 | } 225 | }; 226 | 227 | visibilityIsSupported = true; 228 | 229 | $provide.value('$window', $window = { 230 | Visibility: Visibility, 231 | WebSocket: WebSocket 232 | }); 233 | 234 | spyOn($window, 'WebSocket').andCallThrough(); 235 | 236 | webSocketProvider.setWebSocketURL('ws://testing.com'); 237 | 238 | })); 239 | 240 | beforeEach(inject(function (_webSocket_, _notificationService_, _$rootScope_) { 241 | webSocket = _webSocket_; 242 | notificationService = _notificationService_; 243 | $rootScope = _$rootScope_; 244 | })); 245 | 246 | it('should send message when WebSocket connection is opened', inject(function () { 247 | expect(webSocketObject.onopen).toBeDefined(); 248 | 249 | webSocket.send({}); 250 | 251 | expect(webSocketObject.send).not.toHaveBeenCalled(); // no connection yet 252 | 253 | webSocketObject.onopen(); 254 | 255 | expect(webSocketObject.send).toHaveBeenCalled(); 256 | })); 257 | 258 | it('should notify subscribers', function () { 259 | expect(webSocketObject.onmessage).toBeDefined(); 260 | 261 | var listener1 = jasmine.createSpy(); 262 | var listener2 = jasmine.createSpy(); 263 | 264 | webSocket.subscribe('test', listener1); 265 | webSocket.subscribe('test', listener2); 266 | 267 | var message1 = { topic: 'test', data: { value: 100 } }; 268 | webSocketObject.onmessage({ data: JSON.stringify(message1) }); 269 | 270 | expect(listener1).toHaveBeenCalledWith({ value: 100 }); 271 | expect(listener2).toHaveBeenCalledWith({ value: 100 }); 272 | 273 | var message2 = { topic: 'test', data: { value: 50 } }; 274 | webSocketObject.onmessage({ data: JSON.stringify(message2) }); 275 | 276 | expect(listener1).toHaveBeenCalledWith({ value: 50 }); 277 | expect(listener2).toHaveBeenCalledWith({ value: 50 }); 278 | }); 279 | 280 | it('should send a subscribe message', function() { 281 | var listener1 = jasmine.createSpy(); 282 | spyOn(webSocket, 'send'); 283 | webSocket.subscribe('testing', listener1); 284 | expect(webSocket.send).toHaveBeenCalledWith({ type: 'subscribe', topic: 'testing' }); 285 | }); 286 | 287 | it('should unsubscribe single callback', function () { 288 | expect(webSocketObject.onmessage).toBeDefined(); 289 | 290 | var listener1 = jasmine.createSpy(); 291 | var listener2 = jasmine.createSpy(); 292 | 293 | webSocket.subscribe('test', listener1); 294 | webSocket.subscribe('test', listener2); 295 | 296 | var message = { topic: 'test', data: {} }; 297 | var event = { data: JSON.stringify(message) }; 298 | webSocketObject.onmessage(event); 299 | 300 | expect(listener1).toHaveBeenCalled(); 301 | expect(listener2).toHaveBeenCalled(); 302 | 303 | webSocket.unsubscribe('test', listener1); 304 | 305 | webSocketObject.onmessage(event); 306 | 307 | expect(listener1.callCount).toEqual(1); 308 | expect(listener2.callCount).toEqual(2); 309 | }); 310 | 311 | it('should unsubscribe all listeners when second argument is omitted', function () { 312 | expect(webSocketObject.onmessage).toBeDefined(); 313 | 314 | var listener1 = jasmine.createSpy(); 315 | var listener2 = jasmine.createSpy(); 316 | 317 | webSocket.subscribe('test', listener1); 318 | webSocket.subscribe('test', listener2); 319 | 320 | var message = { topic: 'test', data: {} }; 321 | var event = { data: JSON.stringify(message) }; 322 | webSocketObject.onmessage(event); 323 | 324 | expect(listener1).toHaveBeenCalled(); 325 | expect(listener2).toHaveBeenCalled(); 326 | 327 | webSocket.unsubscribe('test'); 328 | 329 | webSocketObject.onmessage(event); 330 | 331 | expect(listener1.callCount).toEqual(1); 332 | expect(listener2.callCount).toEqual(1); 333 | }); 334 | 335 | 336 | it('should call unsubscribe on scope destroy', function () { 337 | var listener = jasmine.createSpy(); 338 | var scope = $rootScope.$new(); 339 | 340 | spyOn(webSocket, 'unsubscribe'); 341 | 342 | webSocket.subscribe('test', listener, scope); 343 | 344 | expect(webSocket.unsubscribe).not.toHaveBeenCalled(); 345 | 346 | scope.$destroy(); 347 | 348 | expect(webSocket.unsubscribe).toHaveBeenCalled(); 349 | 350 | }); 351 | 352 | it('should send an unsubscribe message when there are no more listeners for a given topic', function() { 353 | 354 | var listener1 = jasmine.createSpy(); 355 | spyOn(webSocket, 'send'); 356 | webSocket.subscribe('testing', listener1); 357 | expect(webSocket.send).toHaveBeenCalledWith({ type: 'subscribe', topic: 'testing' }); 358 | webSocket.unsubscribe('testing', listener1); 359 | expect(webSocket.send).toHaveBeenCalledWith({ type: 'unsubscribe', topic: 'testing' }); 360 | }); 361 | 362 | it('should send a subscribe message again if a topic has previously been unsubscribed to', function() { 363 | var listener1 = jasmine.createSpy(); 364 | spyOn(webSocket, 'send'); 365 | webSocket.subscribe('testing', listener1); 366 | expect(webSocket.send).toHaveBeenCalledWith({ type: 'subscribe', topic: 'testing' }); 367 | webSocket.unsubscribe('testing', listener1); 368 | expect(webSocket.send).toHaveBeenCalledWith({ type: 'unsubscribe', topic: 'testing' }); 369 | webSocket.subscribe('testing', listener1); 370 | expect(webSocket.send.calls[2].args[0]).toEqual({ type: 'subscribe', topic: 'testing' }); 371 | }); 372 | 373 | }); 374 | 375 | describe('the connect method', function() { 376 | 377 | beforeEach(module('ui.websocket', function($provide, webSocketProvider) { 378 | WebSocket = function(url, protocol) { 379 | webSocketObject = this; 380 | wsUrl = url; 381 | this.send = jasmine.createSpy(); 382 | this.close = function() { 383 | this.onclose(); 384 | }; 385 | }; 386 | WebSocket.OPEN = 1; 387 | 388 | Visibility = { 389 | change: jasmine.createSpy(), 390 | isSupported: function() { 391 | return visibilityIsSupported; 392 | } 393 | }; 394 | 395 | visibilityIsSupported = true; 396 | 397 | $provide.value('$window', $window = { 398 | Visibility: Visibility, 399 | WebSocket: WebSocket 400 | }); 401 | webSocketProvider.setExplicitConnection(true); 402 | webSocketProvider.setWebSocketURL('ws://testing.com'); 403 | 404 | })); 405 | 406 | beforeEach(inject(function (_webSocket_, _notificationService_, _$rootScope_, _$timeout_) { 407 | webSocket = _webSocket_; 408 | notificationService = _notificationService_; 409 | $rootScope = _$rootScope_; 410 | $timeout = _$timeout_; 411 | webSocket.connect(); 412 | 413 | webSocketObject.onopen(); 414 | 415 | spyOn(webSocketObject, 'close').andCallThrough(); 416 | // spyOn(webSocket, 'connect').andCallThrough(); 417 | })); 418 | 419 | it('should not throw', function() { 420 | expect(function() { 421 | webSocket.connect(); 422 | }).not.toThrow(); 423 | }); 424 | 425 | it('should do nothing if an open WebSocket instance is already there', function() { 426 | var initObj = webSocketObject; 427 | initObj.readyState = WebSocket.OPEN; 428 | webSocket.connect(); 429 | expect(initObj === webSocketObject).toEqual(true); 430 | }); 431 | 432 | }); 433 | 434 | describe('the disconnect method', function() { 435 | 436 | beforeEach(module('ui.websocket', function($provide, webSocketProvider) { 437 | 438 | WebSocket = function(url, protocol) { 439 | webSocketObject = this; 440 | wsUrl = url; 441 | this.send = jasmine.createSpy(); 442 | this.close = function() { 443 | this.onclose(); 444 | }; 445 | }; 446 | 447 | Visibility = { 448 | change: jasmine.createSpy(), 449 | isSupported: function() { 450 | return visibilityIsSupported; 451 | } 452 | }; 453 | 454 | visibilityIsSupported = true; 455 | 456 | $provide.value('$window', $window = { 457 | Visibility: Visibility, 458 | WebSocket: WebSocket 459 | }); 460 | 461 | spyOn($window, 'WebSocket').andCallThrough(); 462 | webSocketProvider.setExplicitConnection(true); 463 | webSocketProvider.setWebSocketURL('ws://testing.com'); 464 | 465 | })); 466 | 467 | describe('when the webSocket has been connected', function() { 468 | 469 | beforeEach(inject(function (_webSocket_, _notificationService_, _$rootScope_, _$timeout_) { 470 | webSocket = _webSocket_; 471 | notificationService = _notificationService_; 472 | $rootScope = _$rootScope_; 473 | $timeout = _$timeout_; 474 | webSocket.connect(); 475 | 476 | webSocketObject.onopen(); 477 | 478 | spyOn(webSocketObject, 'close').andCallThrough(); 479 | spyOn(webSocket, 'connect'); 480 | })); 481 | 482 | it('should call the close method of the WebSocket', function() { 483 | webSocket.disconnect(); 484 | expect(webSocketObject.close).toHaveBeenCalled(); 485 | }); 486 | 487 | it('should not try to re-establish connection after connectionAttemptInterval', function() { 488 | webSocket.disconnect(); 489 | $timeout.verifyNoPendingTasks(); 490 | }); 491 | 492 | }); 493 | 494 | describe('when the webSocket has not connected', function() { 495 | 496 | beforeEach(inject(function (_webSocket_, _notificationService_, _$rootScope_, _$timeout_) { 497 | webSocket = _webSocket_; 498 | notificationService = _notificationService_; 499 | $rootScope = _$rootScope_; 500 | $timeout = _$timeout_; 501 | spyOn(webSocket, 'connect'); 502 | })); 503 | 504 | it('should not throw', function() { 505 | expect(function() { 506 | webSocket.disconnect(); 507 | }).not.toThrow(); 508 | }); 509 | 510 | }); 511 | 512 | }); 513 | 514 | describe('when the connection gets unexpectedly closed', function() { 515 | 516 | beforeEach(module('ui.websocket', function($provide, webSocketProvider) { 517 | 518 | WebSocket = function(url, protocol) { 519 | webSocketObject = this; 520 | wsUrl = url; 521 | this.send = jasmine.createSpy(); 522 | this.close = function() { 523 | this.onclose(); 524 | }; 525 | }; 526 | 527 | Visibility = { 528 | change: jasmine.createSpy(), 529 | isSupported: function() { 530 | return visibilityIsSupported; 531 | } 532 | }; 533 | 534 | visibilityIsSupported = true; 535 | 536 | $provide.value('$window', $window = { 537 | Visibility: Visibility, 538 | WebSocket: WebSocket 539 | }); 540 | 541 | spyOn($window, 'WebSocket').andCallThrough(); 542 | 543 | webSocketProvider.setWebSocketURL('ws://testing.com'); 544 | webSocketProvider.setConnectionAttemptInterval(1000); 545 | webSocketProvider.setMaxConnectionAttempts(3); 546 | 547 | })); 548 | 549 | beforeEach(inject(function (_webSocket_, _notificationService_, _$rootScope_, _$timeout_) { 550 | webSocket = _webSocket_; 551 | notificationService = _notificationService_; 552 | $rootScope = _$rootScope_; 553 | $timeout = _$timeout_; 554 | 555 | webSocketObject.onopen(); 556 | 557 | spyOn(webSocketObject, 'close').andCallThrough(); 558 | spyOn(webSocket, 'connect').andCallThrough(); 559 | })); 560 | 561 | it('should try to re-establish connection', function() { 562 | webSocketObject.onclose(); 563 | $timeout.flush(); 564 | expect(webSocket.connect).toHaveBeenCalled(); 565 | }); 566 | 567 | it('should try maxConnectionAttempts times', function() { 568 | webSocketObject.onclose(); 569 | $timeout.flush(); 570 | webSocketObject.onclose(); 571 | $timeout.flush(); 572 | webSocketObject.onclose(); 573 | $timeout.flush(); 574 | webSocketObject.onclose(); 575 | $timeout.verifyNoPendingTasks(); 576 | expect(webSocket.connect.calls.length).toEqual(3); 577 | }); 578 | 579 | it('should start queuing up send messages again, then send those messages once connection has been re-established', function() { 580 | 581 | }); 582 | 583 | }); 584 | 585 | }); 586 | -------------------------------------------------------------------------------- /test/spec/widgets/main.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 DataTorrent, Inc. ALL Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | describe('main test', function () { 20 | var $compile, $rootScope; 21 | 22 | beforeEach(module('ui.widgets')); 23 | beforeEach(inject(function(_$compile_, _$rootScope_) { 24 | $compile = _$compile_; 25 | $rootScope = _$rootScope_; 26 | $rootScope.myData = []; 27 | })); 28 | 29 | it('should have topN directive', function() { 30 | var element = angular.element('
'); 31 | $compile(element)($rootScope); 32 | $rootScope.$digest(); 33 | expect(element.hasClass('top-n')).toBe(true); 34 | }); 35 | }); --------------------------------------------------------------------------------