├── .bowerrc ├── .editorconfig ├── .gitignore ├── .jshintrc ├── .travis.yml ├── CONTRIBUTING.md ├── Dockerfile ├── Gruntfile.js ├── LICENSE ├── README.md ├── bower.json ├── circle.yml ├── dist ├── malhar-angular-dashboard.css └── malhar-angular-dashboard.js ├── docs ├── AngularJSDashboard.png └── scope.png ├── e2e ├── main.po.js └── main.spec.js ├── favicon.ico ├── gulp ├── build-demo.js ├── build.js ├── e2e-tests.js ├── inject.js ├── proxy.js ├── server.js ├── styles.js ├── unit-tests.js └── watch.js ├── gulpfile.js ├── karma.conf.js ├── package.json ├── protractor.conf.js ├── src ├── 404.html ├── app │ ├── cartDataModel.js │ ├── cartDataModel.spec.js │ ├── customWidgetSettings.js │ ├── customWidgetSettings.spec.js │ ├── dataModel.js │ ├── dataModel.spec.js │ ├── demo.js │ ├── demo.less │ ├── demo.spec.js │ ├── directives.js │ ├── directives.spec.js │ ├── dynamicData.js │ ├── dynamicData.spec.js │ ├── dynamicOptions.js │ ├── dynamicOptions.spec.js │ ├── explicitSave.js │ ├── explicitSave.spec.js │ ├── index.js │ ├── index.less │ ├── layouts.js │ ├── layouts.spec.js │ ├── resize.js │ ├── resize.spec.js │ ├── template │ │ ├── cartDetail.html │ │ ├── cartSummary.html │ │ ├── configurableWidgetModalOptions.html │ │ ├── customSettingsTemplate.html │ │ ├── dynamicData.html │ │ ├── dynamicOptions.html │ │ ├── dynamicOptionsContainer.html │ │ ├── fluid.html │ │ ├── layouts.html │ │ ├── peopleList.html │ │ ├── peopleThumbnail.html │ │ ├── resizable.html │ │ ├── view.html │ │ └── widgetSpecificSettings.html │ └── vendor.less ├── components │ ├── directives │ │ ├── dashboard │ │ │ ├── WidgetSettingsCtrl.js │ │ │ ├── WidgetSettingsCtrl.spec.js │ │ │ ├── altDashboard.html │ │ │ ├── dashboard.html │ │ │ ├── dashboard.js │ │ │ ├── dashboard.less │ │ │ ├── dashboard.spec.js │ │ │ └── widget-settings-template.html │ │ ├── dashboardLayouts │ │ │ ├── SaveChangesModal.html │ │ │ ├── SaveChangesModalCtrl.js │ │ │ ├── SaveChangesModalCtrl.spec.js │ │ │ ├── dashboardLayouts.html │ │ │ ├── dashboardLayouts.js │ │ │ └── dashboardLayouts.spec.js │ │ └── widget │ │ │ ├── DashboardWidgetCtrl.js │ │ │ ├── DashboardWidgetCtrl.spec.js │ │ │ ├── widget.js │ │ │ └── widget.spec.js │ └── models │ │ ├── DashboardState.js │ │ ├── DashboardState.spec.js │ │ ├── LayoutStorage.js │ │ ├── LayoutStorage.spec.js │ │ ├── WidgetDataModel.js │ │ ├── WidgetDataModel.spec.js │ │ ├── WidgetDefCollection.js │ │ ├── WidgetDefCollection.spec.js │ │ ├── WidgetModel.js │ │ └── WidgetModel.spec.js ├── favicon.ico ├── index.html └── person.png └── test ├── .jshintrc ├── runner.html └── spec ├── SaveChangesModalCtrl.spec.js ├── dashboardState.js ├── widgetDataModel.js └── widgetSettingsCtrl.spec.js /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | bower_components/ 3 | .sass-cache/ 4 | .tmp/ 5 | dist/ 6 | /demo/ 7 | /coverage -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "esnext": true, 4 | "bitwise": true, 5 | "camelcase": true, 6 | "curly": true, 7 | "eqeqeq": true, 8 | "immed": true, 9 | "indent": 2, 10 | "latedef": true, 11 | "newcap": true, 12 | "noarg": true, 13 | "quotmark": "single", 14 | "regexp": true, 15 | "undef": true, 16 | "unused": true, 17 | "strict": true, 18 | "trailing": true, 19 | "smarttabs": true, 20 | "white": true, 21 | "validthis": true, 22 | "globals": { 23 | "angular": false, 24 | "inject": false, 25 | "describe": false, 26 | "it": false, 27 | "before": false, 28 | "beforeEach": false, 29 | "after": false, 30 | "afterEach": false, 31 | "expect": false 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '4.3.0' 4 | before_script: 5 | - 'npm install -g bower gulp' 6 | - 'bower install' 7 | script: 8 | - 'gulp' -------------------------------------------------------------------------------- /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 | ``` 28 | 29 | Development 30 | ----------- 31 | To start development: 32 | 33 | 1. Fork this repository and clone the fork onto your development machine: 34 | ``` 35 | $ git clone git://github.com/[USERNAME]/malhar-angular-dashboard.git 36 | ``` 37 | 38 | 2. Install dependencies 39 | ``` 40 | $ npm install . 41 | $ bower install 42 | ``` 43 | 44 | 3. Create a branch for your new feature or bug fix 45 | ``` 46 | $ git checkout -b my_feature_branch 47 | ``` 48 | 49 | 4. Run grunt tasks 50 | When you have finished your bug fix or feature, be sure to always run `grunt`, which will run all necessary tasks, including code linting, testing, and generating distribution files. 51 | ``` 52 | $ grunt 53 | ``` 54 | 55 | 5. Commit changes, push to your fork 56 | ``` 57 | $ git add [CHANGED FILES] 58 | $ git commit -m 'descriptive commit message' 59 | $ git push origin [BRANCH_NAME] 60 | ``` 61 | 62 | 6. Issue a pull request 63 | Using the Github website, issue a pull request. We will review it and either merge your changes or explain why we didn't. 64 | 65 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:14.04 2 | RUN apt-get update && apt-get install git wget -y && wget -qO- https://deb.nodesource.com/setup_4.x | sudo bash - 3 | RUN apt-get install nodejs -y && npm install -g bower gulp && npm install gulp 4 | RUN mkdir -p /usr/src/app 5 | EXPOSE 3000 6 | WORKDIR /usr/src/app 7 | ADD . /usr/src/app 8 | RUN bower install --allow-root && npm install 9 | RUN gulp 10 | -------------------------------------------------------------------------------- /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.dashboard' 12 | }, 13 | src: ['template/*.html'], 14 | dest: 'template/dashboard.js' 15 | } 16 | }, 17 | karma: { 18 | unit: { 19 | configFile: 'karma.conf.js', 20 | singleRun: true 21 | }, 22 | auto: { 23 | configFile: 'karma.conf.js' 24 | } 25 | }, 26 | concat: { 27 | dist: { 28 | src: [ 29 | 'src/directives/dashboard.js', 30 | 'src/directives/*.js', 31 | 'src/models/*.js', 32 | 'src/controllers/*.js', 33 | 'template/dashboard.js' 34 | ], 35 | dest: 'dist/angular-ui-dashboard.js' 36 | } 37 | }, 38 | watch: { 39 | files: [ 40 | 'src/**/*.*', 41 | 'template/*.html' 42 | ], 43 | tasks: ['ngtemplates', 'concat', 'copy:dist'], 44 | livereload: { 45 | options: { 46 | livereload: '<%= connect.options.livereload %>' 47 | }, 48 | files: [ 49 | 'demo/{,*/}*.html', 50 | 'demo/{,*/}*.css', 51 | 'demo/{,*/}*.js', 52 | 'dist/*.css', 53 | 'dist/*.js' 54 | ] 55 | } 56 | }, 57 | jshint: { 58 | options: { 59 | jshintrc: '.jshintrc' 60 | }, 61 | all: [ 62 | 'Gruntfile.js', 63 | 'src/{,*/}*.js' 64 | ] 65 | }, 66 | copy: { 67 | dist: { 68 | files: [{ 69 | expand: true, 70 | flatten: true, 71 | src: ['src/angular-ui-dashboard.css'], 72 | dest: 'dist' 73 | }] 74 | } 75 | }, 76 | clean: { 77 | dist: { 78 | files: [{ 79 | src: [ 80 | 'dist/*' 81 | ] 82 | }] 83 | }, 84 | templates: { 85 | src: ['<%= ngtemplates.dashboard.dest %>'] 86 | } 87 | }, 88 | connect: { 89 | options: { 90 | port:9000, 91 | // Change this to '0.0.0.0' to access the server from outside. 92 | hostname: 'localhost', 93 | livereload: 35729 94 | }, 95 | livereload: { 96 | options: { 97 | open: true, 98 | base: [ 99 | '.', 100 | 'demo', 101 | 'dist' 102 | ] 103 | } 104 | } 105 | } 106 | }); 107 | 108 | grunt.registerTask('test', [ 109 | 'jshint', 110 | 'ngtemplates', 111 | 'karma:unit' 112 | ]); 113 | 114 | grunt.registerTask('test_auto', [ 115 | 'jshint', 116 | 'ngtemplates', 117 | 'karma:auto' 118 | ]); 119 | 120 | grunt.registerTask('demo', [ 121 | 'connect:livereload', 122 | 'watch' 123 | ]); 124 | 125 | grunt.registerTask('default', [ 126 | 'clean:dist', 127 | 'jshint', 128 | 'ngtemplates', 129 | 'karma:unit', 130 | 'concat', 131 | 'copy:dist', 132 | 'clean:templates' 133 | ]); 134 | }; 135 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "malhar-angular-dashboard", 3 | "version": "2.0.2", 4 | "ignore": [ 5 | "docs", 6 | "e2e", 7 | "gulp", 8 | "src", 9 | "test" 10 | ], 11 | "dependencies": { 12 | "angular": "~1.4", 13 | "angular-bootstrap": ">=1.0.0", 14 | "angular-sanitize": "~1.5.8", 15 | "angular-ui-sortable": "~0.13.4", 16 | "bootstrap": "~3.3.6", 17 | "lodash": "~4.5.1" 18 | }, 19 | "devDependencies": { 20 | "angular-mocks": "~1.4", 21 | "angular-route": "~1.3", 22 | "angular-markdown-directive": "~0.3.1" 23 | }, 24 | "main": [ 25 | "dist/malhar-angular-dashboard.css", 26 | "dist/malhar-angular-dashboard.js" 27 | ], 28 | "overrides": { 29 | "showdown": { 30 | "main": "src/showdown.js" 31 | } 32 | }, 33 | "resolutions": { 34 | "angular": "~1.4" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | 2 | dependencies: 3 | pre: 4 | - wget -qO- https://deb.nodesource.com/setup_4.x | sudo bash - 5 | - sudo apt-get install nodejs 6 | - npm install -g bower gulp 7 | - bower install 8 | 9 | post: 10 | - gulp 11 | 12 | test: 13 | override: 14 | - npm test 15 | -------------------------------------------------------------------------------- /dist/malhar-angular-dashboard.css: -------------------------------------------------------------------------------- 1 | .dashboard-widget-area { 2 | margin: 10px 0 30px; 3 | min-height: 200px; 4 | } 5 | .widget-container { 6 | float: left; 7 | display: inline-block; 8 | width: 33%; 9 | padding-bottom: 1em; 10 | } 11 | .widget { 12 | margin: 0 1em 0 0; 13 | background-color: white; 14 | border: 2px solid #444; 15 | border-radius: 5px; 16 | position: relative; 17 | height: 100%; 18 | } 19 | .widget-header { 20 | display: flex; 21 | overflow: hidden; 22 | } 23 | .widget-header .panel-title { 24 | flex: 1 1 auto; 25 | overflow: hidden; 26 | padding-right: 4px; 27 | text-overflow: ellipsis; 28 | white-space: nowrap; 29 | } 30 | .widget-header .buttons { 31 | flex: 0 0 auto; 32 | } 33 | .widget-header .label { 34 | display: inline-block; 35 | vertical-align: middle; 36 | } 37 | .widget-header .glyphicon { 38 | cursor: pointer; 39 | float: right; 40 | opacity: 0.5; 41 | margin-left: 5px; 42 | } 43 | .widget-header .glyphicon:hover { 44 | opacity: 1; 45 | } 46 | .widget-header .widget-title { 47 | vertical-align: middle; 48 | } 49 | .widget-header form.widget-title { 50 | display: inline; 51 | } 52 | .widget-header form.widget-title input.form-control { 53 | width: auto; 54 | display: inline-block; 55 | } 56 | .widget-content { 57 | overflow: hidden; 58 | } 59 | .widget .widget-w-resizer { 60 | background-color: transparent; 61 | display: flex; 62 | flex-direction: column; 63 | height: 100%; 64 | position: absolute; 65 | left: -2px; 66 | top: 0px; 67 | width: 5px; 68 | } 69 | .widget .widget-w-resizer .nw-resizer { 70 | background-color: transparent; 71 | cursor: nwse-resize; 72 | flex: 0 0 auto; 73 | height: 15px; 74 | } 75 | .widget .widget-w-resizer .w-resizer { 76 | background-color: transparent; 77 | cursor: ew-resize; 78 | flex: 1 1 auto; 79 | } 80 | .widget .widget-w-resizer .sw-resizer { 81 | background-color: transparent; 82 | cursor: nesw-resize; 83 | flex: 0 0 auto; 84 | height: 15px; 85 | } 86 | .widget .widget-e-resizer { 87 | background-color: transparent; 88 | display: flex; 89 | flex-direction: column; 90 | height: 100%; 91 | position: absolute; 92 | right: -2px; 93 | top: 0px; 94 | width: 5px; 95 | } 96 | .widget .widget-e-resizer .ne-resizer { 97 | background-color: transparent; 98 | cursor: nesw-resize; 99 | flex: 0 0 auto; 100 | height: 15px; 101 | } 102 | .widget .widget-e-resizer .e-resizer { 103 | background-color: transparent; 104 | cursor: ew-resize; 105 | flex: 1 1 auto; 106 | } 107 | .widget .widget-e-resizer .se-resizer { 108 | background-color: transparent; 109 | cursor: nwse-resize; 110 | flex: 0 0 auto; 111 | height: 15px; 112 | } 113 | .widget .widget-n-resizer { 114 | background-color: transparent; 115 | display: flex; 116 | height: 5px; 117 | width: 100%; 118 | left: 0; 119 | position: absolute; 120 | top: -2px; 121 | } 122 | .widget .widget-n-resizer .nw-resizer { 123 | background-color: transparent; 124 | cursor: nwse-resize; 125 | flex: 0 0 auto; 126 | width: 18px; 127 | } 128 | .widget .widget-n-resizer .n-resizer { 129 | background-color: transparent; 130 | cursor: ns-resize; 131 | flex: 1 1 auto; 132 | } 133 | .widget .widget-n-resizer .ne-resizer { 134 | background-color: transparent; 135 | cursor: nesw-resize; 136 | flex: 0 0 auto; 137 | width: 18px; 138 | } 139 | .widget .widget-s-resizer { 140 | background-color: transparent; 141 | bottom: -2px; 142 | display: flex; 143 | height: 5px; 144 | position: absolute; 145 | width: 100%; 146 | left: 0; 147 | } 148 | .widget .widget-s-resizer .sw-resizer { 149 | background-color: transparent; 150 | cursor: nesw-resize; 151 | flex: 0 0 auto; 152 | width: 18px; 153 | } 154 | .widget .widget-s-resizer .s-resizer { 155 | background-color: transparent; 156 | cursor: ns-resize; 157 | flex: 1 1 auto; 158 | } 159 | .widget .widget-s-resizer .se-resizer { 160 | background-color: transparent; 161 | cursor: nwse-resize; 162 | flex: 0 0 auto; 163 | width: 18px; 164 | } 165 | .widget .widget-resizer-marquee { 166 | xborder: 2px dashed #09305f; 167 | border: 2px dotted #79a0e0; 168 | position: absolute; 169 | top: -2px; 170 | left: -2px; 171 | z-index: 999999; 172 | } 173 | .widget .widget-resizer-marquee.n, 174 | .widget .widget-resizer-marquee.s { 175 | cursor: ns-resize; 176 | } 177 | .widget .widget-resizer-marquee.w, 178 | .widget .widget-resizer-marquee.e { 179 | cursor: ew-resize; 180 | } 181 | .widget .widget-resizer-marquee.nw, 182 | .widget .widget-resizer-marquee.se { 183 | cursor: nwse-resize; 184 | } 185 | .widget .widget-resizer-marquee.sw, 186 | .widget .widget-resizer-marquee.ne { 187 | cursor: nesw-resize; 188 | } 189 | .remove-layout-icon { 190 | vertical-align: text-top; 191 | cursor: pointer; 192 | opacity: 0.3; 193 | } 194 | .remove-layout-icon:hover { 195 | opacity: 1; 196 | } 197 | .layout-title { 198 | display: inline-block; 199 | } 200 | -------------------------------------------------------------------------------- /docs/AngularJSDashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dtpublic/malhar-angular-dashboard/774e69e0b31ce344605e2aae5695c2d7ae5b90c4/docs/AngularJSDashboard.png -------------------------------------------------------------------------------- /docs/scope.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dtpublic/malhar-angular-dashboard/774e69e0b31ce344605e2aae5695c2d7ae5b90c4/docs/scope.png -------------------------------------------------------------------------------- /e2e/main.po.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file uses the Page Object pattern to define the main page for tests 3 | * https://docs.google.com/presentation/d/1B6manhG0zEXkC-H-tPo2vwU06JhL8w9-XCF9oehXzAQ 4 | */ 5 | 6 | 'use strict'; 7 | 8 | var MainPage = function() { 9 | this.jumbEl = element(by.css('.jumbotron')); 10 | this.h1El = this.jumbEl.element(by.css('h1')); 11 | this.imgEl = this.jumbEl.element(by.css('img')); 12 | this.thumbnailEls = element(by.css('body')).all(by.repeater('awesomeThing in awesomeThings')); 13 | }; 14 | 15 | module.exports = new MainPage(); 16 | -------------------------------------------------------------------------------- /e2e/main.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('The main view', function () { 4 | var page; 5 | 6 | beforeEach(function () { 7 | browser.get('http://localhost:3000/index.html'); 8 | page = require('./main.po'); 9 | }); 10 | 11 | it('should include jumbotron with correct data', function() { 12 | expect(page.h1El.getText()).toBe('\'Allo, \'Allo!'); 13 | expect(page.imgEl.getAttribute('src')).toMatch(/assets\/images\/yeoman.png$/); 14 | expect(page.imgEl.getAttribute('alt')).toBe('I\'m Yeoman'); 15 | }); 16 | 17 | it('list more than 5 awesome things', function () { 18 | expect(page.thumbnailEls.count()).toBeGreaterThan(5); 19 | }); 20 | 21 | }); 22 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dtpublic/malhar-angular-dashboard/774e69e0b31ce344605e2aae5695c2d7ae5b90c4/favicon.ico -------------------------------------------------------------------------------- /gulp/build-demo.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | 5 | var paths = gulp.paths; 6 | 7 | var $ = require('gulp-load-plugins')({ 8 | pattern: ['gulp-*', 'main-bower-files', 'uglify-save-license', 'del'] 9 | }); 10 | 11 | gulp.task('demo:partials', function () { 12 | return gulp.src([ 13 | paths.src + '/{app,components}/**/*.html', 14 | paths.tmp + '/{app,components}/**/*.html' 15 | ]) 16 | .pipe($.angularTemplatecache('templateCacheHtml.js', { 17 | module: 'app' 18 | })) 19 | .pipe(gulp.dest(paths.tmp + '/partials/')); 20 | }); 21 | 22 | gulp.task('demo:html', ['inject', 'demo:partials'], function () { 23 | var partialsInjectFile = gulp.src(paths.tmp + '/partials/templateCacheHtml.js', { read: false }); 24 | var partialsInjectOptions = { 25 | starttag: '', 26 | ignorePath: paths.tmp + '/partials', 27 | addRootSlash: false 28 | }; 29 | 30 | var htmlFilter = $.filter('*.html'); 31 | var jsFilter = $.filter('**/*.js'); 32 | var cssFilter = $.filter('**/*.css'); 33 | var assets; 34 | 35 | return gulp.src(paths.tmp + '/serve/*.html') 36 | .pipe($.inject(partialsInjectFile, partialsInjectOptions)) 37 | .pipe(assets = $.useref.assets()) 38 | .pipe($.rev()) 39 | .pipe(jsFilter) 40 | .pipe($.ngAnnotate()) 41 | .pipe($.uglify({preserveComments: $.uglifySaveLicense})) 42 | .pipe(jsFilter.restore()) 43 | .pipe(cssFilter) 44 | .pipe($.replace('../bootstrap/fonts', 'fonts')) 45 | .pipe($.csso()) 46 | .pipe(cssFilter.restore()) 47 | .pipe(assets.restore()) 48 | .pipe($.useref()) 49 | .pipe($.revReplace()) 50 | .pipe(htmlFilter) 51 | .pipe($.minifyHtml({ 52 | empty: true, 53 | spare: true, 54 | quotes: true 55 | })) 56 | .pipe(htmlFilter.restore()) 57 | .pipe(gulp.dest(paths.demo + '/')) 58 | .pipe($.size({ title: paths.demo + '/', showFiles: true })); 59 | }); 60 | 61 | gulp.task('demo:images', function () { 62 | return gulp.src(paths.src + '/assets/images/**/*') 63 | .pipe(gulp.dest(paths.demo + '/assets/images/')); 64 | }); 65 | 66 | gulp.task('demo:fonts', function () { 67 | return gulp.src($.mainBowerFiles()) 68 | .pipe($.filter('**/*.{eot,svg,ttf,woff}')) 69 | .pipe($.flatten()) 70 | .pipe(gulp.dest(paths.demo + '/fonts/')); 71 | }); 72 | 73 | gulp.task('demo:misc', function () { 74 | return gulp.src(paths.src + '/**/*.ico') 75 | .pipe(gulp.dest(paths.demo + '/')); 76 | }); 77 | 78 | gulp.task('demo:clean', function (done) { 79 | $.del([paths.demo + '/', paths.tmp + '/'], done); 80 | }); 81 | 82 | gulp.task('build:demo', ['demo:html', 'demo:images', 'demo:fonts', 'demo:misc']); 83 | -------------------------------------------------------------------------------- /gulp/build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | 5 | var paths = gulp.paths; 6 | 7 | var $ = require('gulp-load-plugins')({ 8 | pattern: ['gulp-*', 'main-bower-files', 'uglify-save-license', 'del'] 9 | }); 10 | 11 | gulp.task('partials', function () { 12 | return gulp.src([ 13 | paths.src + '/components/**/*.html' 14 | ]) 15 | .pipe($.angularTemplatecache('templateCacheHtml.js', { 16 | module: 'ui.dashboard', 17 | root: 'components' 18 | })) 19 | .pipe(gulp.dest(paths.tmp + '/partials/')); 20 | }); 21 | 22 | gulp.task('clean', function (done) { 23 | $.del([paths.dist + '/', paths.tmp + '/'], done); 24 | }); 25 | 26 | gulp.task('build:js', ['partials'], function() { 27 | return gulp.src([ 28 | paths.src + '/components/**/!(*.spec|*_e2e)+(.js)', 29 | paths.tmp + '/partials/templateCacheHtml.js' 30 | ]) 31 | .pipe($.angularFilesort()) 32 | .pipe($.concat('malhar-angular-dashboard.js')) 33 | .pipe($.ngAnnotate()) 34 | .pipe(gulp.dest(paths.dist)) 35 | 36 | }); 37 | 38 | gulp.task('build:css', function() { 39 | return gulp.src([ 40 | paths.src + '/components/**/*.less' 41 | ]) 42 | .pipe($.concat('malhar-angular-dashboard.less')) 43 | .pipe($.less()) 44 | .pipe(gulp.dest(paths.dist)); 45 | }); 46 | 47 | gulp.task('build', ['build:js', 'build:css']); 48 | -------------------------------------------------------------------------------- /gulp/e2e-tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | 5 | var $ = require('gulp-load-plugins')(); 6 | 7 | var browserSync = require('browser-sync'); 8 | 9 | var paths = gulp.paths; 10 | 11 | // Downloads the selenium webdriver 12 | gulp.task('webdriver-update', $.protractor.webdriver_update); 13 | 14 | gulp.task('webdriver-standalone', $.protractor.webdriver_standalone); 15 | 16 | function runProtractor (done) { 17 | 18 | gulp.src(paths.e2e + '/**/*.js') 19 | .pipe($.protractor.protractor({ 20 | configFile: 'protractor.conf.js', 21 | })) 22 | .on('error', function (err) { 23 | // Make sure failed tests cause gulp to exit non-zero 24 | throw err; 25 | }) 26 | .on('end', function () { 27 | // Close browser sync server 28 | browserSync.exit(); 29 | done(); 30 | }); 31 | } 32 | 33 | gulp.task('protractor', ['protractor:src']); 34 | gulp.task('protractor:src', ['serve:e2e', 'webdriver-update'], runProtractor); 35 | gulp.task('protractor:dist', ['serve:e2e-dist', 'webdriver-update'], runProtractor); 36 | -------------------------------------------------------------------------------- /gulp/inject.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | 5 | var paths = gulp.paths; 6 | 7 | var $ = require('gulp-load-plugins')(); 8 | 9 | var wiredep = require('wiredep').stream; 10 | 11 | gulp.task('inject', ['styles'], function () { 12 | 13 | var injectStyles = gulp.src([ 14 | paths.tmp + '/serve/{app,components}/**/*.css', 15 | '!' + paths.tmp + '/serve/app/vendor.css' 16 | ], { read: false }); 17 | 18 | var injectScripts = gulp.src([ 19 | paths.src + '/{app,components}/**/*.js', 20 | paths.tmp + '/partials/templateCacheHtml.js', 21 | '!' + paths.src + '/{app,components}/**/*.spec.js', 22 | '!' + paths.src + '/{app,components}/**/*.mock.js' 23 | ]).pipe($.angularFilesort()); 24 | 25 | var injectOptions = { 26 | ignorePath: [paths.src, paths.tmp + '/serve', paths.tmp + '/partials'], 27 | addRootSlash: false 28 | }; 29 | 30 | var wiredepOptions = { 31 | devDependencies: true, 32 | directory: 'bower_components', 33 | exclude: [/bootstrap\.css/, /bootstrap\.css/, /foundation\.css/] 34 | }; 35 | 36 | return gulp.src(paths.src + '/index.html') 37 | .pipe($.inject(injectStyles, injectOptions)) 38 | .pipe($.inject(injectScripts, injectOptions)) 39 | .pipe(wiredep(wiredepOptions)) 40 | .pipe(gulp.dest(paths.tmp + '/serve')); 41 | 42 | }); 43 | -------------------------------------------------------------------------------- /gulp/proxy.js: -------------------------------------------------------------------------------- 1 | /*jshint unused:false */ 2 | 3 | /*************** 4 | 5 | This file allow to configure a proxy system plugged into BrowserSync 6 | in order to redirect backend requests while still serving and watching 7 | files from the web project 8 | 9 | IMPORTANT: The proxy is disabled by default. 10 | 11 | If you want to enable it, watch at the configuration options and finally 12 | change the `module.exports` at the end of the file 13 | 14 | ***************/ 15 | 16 | 'use strict'; 17 | 18 | var httpProxy = require('http-proxy'); 19 | var chalk = require('chalk'); 20 | 21 | /* 22 | * Location of your backend server 23 | */ 24 | var proxyTarget = 'http://server/context/'; 25 | 26 | var proxy = httpProxy.createProxyServer({ 27 | target: proxyTarget 28 | }); 29 | 30 | proxy.on('error', function(error, req, res) { 31 | res.writeHead(500, { 32 | 'Content-Type': 'text/plain' 33 | }); 34 | 35 | console.error(chalk.red('[Proxy]'), error); 36 | }); 37 | 38 | /* 39 | * The proxy middleware is an Express middleware added to BrowserSync to 40 | * handle backend request and proxy them to your backend. 41 | */ 42 | function proxyMiddleware(req, res, next) { 43 | /* 44 | * This test is the switch of each request to determine if the request is 45 | * for a static file to be handled by BrowserSync or a backend request to proxy. 46 | * 47 | * The existing test is a standard check on the files extensions but it may fail 48 | * for your needs. If you can, you could also check on a context in the url which 49 | * may be more reliable but can't be generic. 50 | */ 51 | if (/\.(html|css|js|png|jpg|jpeg|gif|ico|xml|rss|txt|eot|svg|ttf|woff|cur)(\?((r|v|rel|rev)=[\-\.\w]*)?)?$/.test(req.url)) { 52 | next(); 53 | } else { 54 | proxy.web(req, res); 55 | } 56 | } 57 | 58 | /* 59 | * This is where you activate or not your proxy. 60 | * 61 | * The first line activate if and the second one ignored it 62 | */ 63 | 64 | //module.exports = [proxyMiddleware]; 65 | module.exports = []; 66 | -------------------------------------------------------------------------------- /gulp/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | 5 | var paths = gulp.paths; 6 | 7 | var util = require('util'); 8 | 9 | var browserSync = require('browser-sync'); 10 | 11 | var middleware = require('./proxy'); 12 | 13 | function browserSyncInit(baseDir, files, browser) { 14 | browser = browser === undefined ? 'default' : browser; 15 | 16 | var routes = null; 17 | if(baseDir === paths.src || (util.isArray(baseDir) && baseDir.indexOf(paths.src) !== -1)) { 18 | routes = { 19 | '/bower_components': 'bower_components' 20 | }; 21 | } 22 | 23 | browserSync.instance = browserSync.init(files, { 24 | startPath: '/', 25 | server: { 26 | baseDir: baseDir, 27 | middleware: middleware, 28 | routes: routes 29 | }, 30 | browser: browser 31 | }); 32 | } 33 | 34 | gulp.task('serve', ['watch'], function () { 35 | browserSyncInit([ 36 | paths.tmp + '/serve', 37 | paths.src, 38 | paths.bower + '/bootstrap', 39 | paths.tmp + '/partials/' 40 | ], [ 41 | paths.tmp + '/serve/{app,components}/**/*.css', 42 | paths.src + '/{app,components}/**/*.js', 43 | paths.src + 'src/assets/images/**/*', 44 | paths.tmp + '/serve/*.html', 45 | paths.tmp + '/serve/{app,components}/**/*.html', 46 | paths.src + '/{app,components}/**/*.html' 47 | ]); 48 | }); 49 | 50 | gulp.task('serve:dist', ['build:demo'], function () { 51 | browserSyncInit(paths.demo); 52 | }); 53 | 54 | gulp.task('serve:e2e', ['inject'], function () { 55 | browserSyncInit([paths.tmp + '/serve', paths.src], null, []); 56 | }); 57 | 58 | gulp.task('serve:e2e-dist', ['build:demo'], function () { 59 | browserSyncInit(paths.demo, null, []); 60 | }); 61 | -------------------------------------------------------------------------------- /gulp/styles.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | 5 | var paths = gulp.paths; 6 | 7 | var $ = require('gulp-load-plugins')(); 8 | 9 | gulp.task('styles', function () { 10 | 11 | var lessOptions = { 12 | paths: [ 13 | 'bower_components', 14 | paths.src + '/app', 15 | paths.src + '/components' 16 | ] 17 | }; 18 | 19 | var injectFiles = gulp.src([ 20 | paths.src + '/{app,components}/**/*.less', 21 | '!' + paths.src + '/app/index.less', 22 | '!' + paths.src + '/app/vendor.less' 23 | ], { read: false }); 24 | 25 | var injectOptions = { 26 | transform: function(filePath) { 27 | filePath = filePath.replace(paths.src + '/app/', ''); 28 | filePath = filePath.replace(paths.src + '/components/', '../components/'); 29 | return '@import \'' + filePath + '\';'; 30 | }, 31 | starttag: '// injector', 32 | endtag: '// endinjector', 33 | addRootSlash: false 34 | }; 35 | 36 | var indexFilter = $.filter('index.less'); 37 | 38 | return gulp.src([ 39 | paths.src + '/app/index.less', 40 | paths.src + '/app/vendor.less' 41 | ]) 42 | .pipe(indexFilter) 43 | .pipe($.inject(injectFiles, injectOptions)) 44 | .pipe(indexFilter.restore()) 45 | .pipe($.less()) 46 | 47 | .pipe($.autoprefixer()) 48 | .on('error', function handleError(err) { 49 | console.error(err.toString()); 50 | this.emit('end'); 51 | }) 52 | .pipe(gulp.dest(paths.tmp + '/serve/app/')); 53 | }); 54 | -------------------------------------------------------------------------------- /gulp/unit-tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | 5 | var $ = require('gulp-load-plugins')(); 6 | 7 | var merge = require('merge-stream'); 8 | var wiredep = require('wiredep'); 9 | 10 | var paths = gulp.paths; 11 | 12 | function runTests (singleRun, done) { 13 | var bowerDeps = wiredep({ 14 | directory: 'bower_components', 15 | exclude: ['bootstrap-sass-official'], 16 | dependencies: true, 17 | devDependencies: true 18 | }); 19 | var testFiles = gulp.src(bowerDeps.js); 20 | var srcFiles = gulp.src([ 21 | paths.src + '/{app,components}/**/*.js', 22 | paths.tmp + '/partials/templateCacheHtml.js' 23 | ]).pipe($.angularFilesort()); 24 | 25 | return merge(testFiles, srcFiles) 26 | .pipe($.karma({ 27 | configFile: 'karma.conf.js', 28 | action: (singleRun)? 'run': 'watch' 29 | })) 30 | .on('error', function (err) { 31 | // Make sure failed tests cause gulp to exit non-zero 32 | throw err; 33 | }); 34 | } 35 | 36 | gulp.task('test', ['partials'], function () { return runTests(true /* singleRun */) }); 37 | gulp.task('test:auto', ['partials'], function () { return runTests(false /* singleRun */) }); -------------------------------------------------------------------------------- /gulp/watch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | 5 | var paths = gulp.paths; 6 | 7 | gulp.task('watch', ['inject'], function () { 8 | var globs = [ 9 | paths.src + '/{app,components}/**/*.html', 10 | paths.tmp + '/{app,components}/**/*.html' 11 | ]; 12 | 13 | gulp.watch(globs, ['demo:partials']); 14 | 15 | gulp.watch([ 16 | paths.src + '/{app,components}/**/*.less', 17 | paths.src + '/{app,components}/**/*.js', 18 | paths.tmp + '/partials/**/*.js', 19 | 'bower.json' 20 | ], ['inject']); 21 | }); 22 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | 5 | gulp.paths = { 6 | src: 'src', 7 | dist: 'dist', 8 | demo: 'demo', 9 | tmp: '.tmp', 10 | e2e: 'e2e', 11 | bower: 'bower_components' 12 | }; 13 | 14 | require('require-dir')('./gulp'); 15 | 16 | gulp.task('default', ['clean','test'], function () { 17 | gulp.start('build'); 18 | }); 19 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(config) { 4 | 5 | config.set({ 6 | autoWatch : false, 7 | 8 | frameworks: ['jasmine'], 9 | 10 | browsers : ['PhantomJS'], 11 | 12 | plugins : [ 13 | 'karma-phantomjs-launcher', 14 | 'karma-jasmine', 15 | 'karma-coverage' 16 | ], 17 | 18 | reporters: ['dots', 'coverage'], 19 | 20 | coverageReporter: { 21 | type : 'html', 22 | // where to store the report 23 | dir : 'coverage/' 24 | }, 25 | 26 | preprocessors: { 27 | 'src/**/!(*spec)+(.js)': ['coverage'] 28 | } 29 | }); 30 | 31 | }; 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "malhar-angular-dashboard", 3 | "version": "2.0.2", 4 | "author": "https://github.com/DataTorrent/malhar-angular-dashboard/graphs/contributors", 5 | "homepage": "https://github.com/DataTorrent/malhar-angular-dashboard", 6 | "license": "Apache License, v2.0", 7 | "dependencies": {}, 8 | "devDependencies": { 9 | "browser-sync": "~1.7.1", 10 | "chalk": "~0.5.1", 11 | "del": "~0.1.3", 12 | "gulp": "3.9.1", 13 | "gulp-angular-filesort": "~1.0.4", 14 | "gulp-angular-templatecache": "~1.4.2", 15 | "gulp-autoprefixer": "~2.0.0", 16 | "gulp-concat": "^2.4.3", 17 | "gulp-consolidate": "~0.1.2", 18 | "gulp-csso": "~0.2.9", 19 | "gulp-filter": "~1.0.2", 20 | "gulp-flatten": "~0.0.4", 21 | "gulp-inject": "~1.0.2", 22 | "gulp-jshint": "~1.9.0", 23 | "gulp-karma": "~0.0.4", 24 | "gulp-less": "~1.3.6", 25 | "gulp-load-plugins": "~0.7.1", 26 | "gulp-minify-html": "~0.1.7", 27 | "gulp-ng-annotate": "~0.5.3", 28 | "gulp-protractor": "~0.0.11", 29 | "gulp-rename": "~1.2.0", 30 | "gulp-replace": "~0.5.0", 31 | "gulp-rev": "~2.0.1", 32 | "gulp-rev-replace": "~0.3.1", 33 | "gulp-size": "~1.1.0", 34 | "gulp-uglify": "~1.0.1", 35 | "gulp-useref": "~1.0.2", 36 | "http-proxy": "~1.7.0", 37 | "jshint-stylish": "~1.0.0", 38 | "karma": "~0.13.22", 39 | "karma-coverage": "", 40 | "karma-jasmine": "~0.3.1", 41 | "karma-ng-html2js-preprocessor": "~1.0.0", 42 | "karma-phantomjs-launcher": "~0.2.0", 43 | "main-bower-files": "~2.4.0", 44 | "merge-stream": "^0.1.7", 45 | "protractor": "~1.4.0", 46 | "require-dir": "~0.1.0", 47 | "uglify-save-license": "~0.4.1", 48 | "wiredep": "~2.2.0" 49 | }, 50 | "engines": { 51 | "node": ">=0.10.0" 52 | }, 53 | "main": [ 54 | "dist/malhar-angular-dashboard.css", 55 | "dist/malhar-angular-dashboard.js" 56 | ], 57 | "scripts": { 58 | "test": "gulp test" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /protractor.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var paths = require('./.yo-rc.json')['generator-gulp-angular'].props.paths; 4 | 5 | // An example configuration file. 6 | exports.config = { 7 | // The address of a running selenium server. 8 | //seleniumAddress: 'http://localhost:4444/wd/hub', 9 | //seleniumServerJar: deprecated, this should be set on node_modules/protractor/config.json 10 | 11 | // Capabilities to be passed to the webdriver instance. 12 | capabilities: { 13 | 'browserName': 'chrome' 14 | }, 15 | 16 | // Spec patterns are relative to the current working directly when 17 | // protractor is called. 18 | specs: [paths.e2e + '/**/*.js'], 19 | 20 | // Options to be passed to Jasmine-node. 21 | jasmineNodeOpts: { 22 | showColors: true, 23 | defaultTimeoutInterval: 30000 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /src/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Page Not Found :( 6 | 141 | 142 | 143 |
144 |

Not found :(

145 |

Sorry, but the page you were trying to view does not exist.

146 |

It looks like this was the result of either:

147 | 151 | 154 | 155 |
156 | 157 | 158 | -------------------------------------------------------------------------------- /src/app/cartDataModel.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 | .factory('CartDataModel', function() { 21 | function CartDataModel() { 22 | this.items = []; // array to store all items in cart 23 | this.total = 0; // total price of all items in cart 24 | this.qty = 0; // total count of all items in cart 25 | this.expItem = {}; // most expensive item in cart based on unit price 26 | this.cheapItem = {}; // cheapest item in cart based on unit price 27 | } 28 | 29 | angular.extend(CartDataModel.prototype, { 30 | addItem: function(item) { 31 | var index = _.findIndex(this.items, function(i) { 32 | return i.name === item.name; 33 | }); 34 | if (index > -1) { 35 | // item already in cart, increase qty and adjust unit price 36 | this.items[index].qty += item.qty; 37 | this.items[index].total += item.qty * item.price; 38 | 39 | // need to adjust unit price 40 | this.items[index].price = Math.round(this.items[index].total / this.items[index].qty * 100) / 100; 41 | } 42 | else { 43 | // add new item 44 | item.total = Math.round(item.qty * item.price * 100) / 100; 45 | this.items.push(item); 46 | } 47 | this.processItems(); 48 | }, 49 | removeItem: function(item) { 50 | var index = _.findIndex(this.items, function(i) { 51 | return i.name === item.name; 52 | }); 53 | if (index > -1) { 54 | this.items.splice(index, 1); 55 | this.processItems(); 56 | } 57 | }, 58 | processItems: function() { 59 | // recalculate qty and total and determine most expensive and cheapest items 60 | var this_ = this; 61 | this_.total = 0; 62 | this_.qty = 0; 63 | this_.expItem = { 64 | price: -Infinity 65 | }; 66 | this_.cheapItem = { 67 | price: Infinity 68 | }; 69 | _.each(this.items, function(item) { 70 | this_.total += item.total; 71 | this_.qty += item.qty; 72 | if (item.price > this_.expItem.price) { 73 | this_.expItem = item; 74 | } 75 | if (item.price < this_.cheapItem.price) { 76 | this_.cheapItem = item; 77 | } 78 | }); 79 | 80 | // handle Javascript rounding issue 81 | this_.total = Math.round(this_.total * 100) / 100; 82 | 83 | } 84 | }); 85 | 86 | return CartDataModel; 87 | }); -------------------------------------------------------------------------------- /src/app/cartDataModel.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 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 | 18 | 'use strict'; 19 | 20 | describe('Factory: CartDataModel', function () { 21 | 22 | var CartDataModel, model; 23 | 24 | // load the service's module 25 | beforeEach(module('app')); 26 | 27 | // instantiate service 28 | beforeEach(inject(function (_CartDataModel_) { 29 | CartDataModel = _CartDataModel_; 30 | })); 31 | 32 | describe('the constructor', function() { 33 | beforeEach(function() { 34 | model = new CartDataModel(); 35 | }); 36 | 37 | it('should create cart with empty properties', function() { 38 | expect(model.items).toEqual([]); 39 | expect(model.total).toEqual(0); 40 | expect(model.qty).toEqual(0); 41 | expect(model.expItem).toEqual({}); 42 | expect(model.cheapItem).toEqual({}); 43 | }); 44 | }); 45 | 46 | describe('the methods with items', function() { 47 | beforeEach(function() { 48 | model = new CartDataModel(); 49 | 50 | // let's populate cart with one item 51 | model.addItem({name: 'Apple', qty: 2, price: .55}); 52 | model.addItem({name: 'Pear', qty: 5, price: .75}); 53 | model.addItem({name: 'Banana', qty: 3, price: .35}); 54 | }); 55 | 56 | describe('the addItem method', function() { 57 | it('should add new item', function() { 58 | model.addItem({ 59 | name: 'Orange', 60 | qty: 2, 61 | price: .75 62 | }); 63 | 64 | expect(model.items.length).toEqual(4); 65 | expect(model.items[3].name).toEqual('Orange'); 66 | expect(model.items[3].qty).toEqual(2); 67 | expect(model.items[3].price).toEqual(.75); 68 | }); 69 | 70 | it('shoudl update existing item', function() { 71 | model.addItem({ 72 | name: 'Pear', 73 | qty: 2, 74 | price: .67 75 | }); 76 | 77 | expect(model.items.length).toEqual(3); 78 | expect(model.items[1].name).toEqual('Pear'); 79 | expect(model.items[1].qty).toEqual(7); 80 | expect(model.items[1].price).toEqual(.73); 81 | }); 82 | 83 | }); 84 | 85 | describe('the removeItem method', function() { 86 | it('should remove one item', function() { 87 | model.removeItem({name: 'Pear'}); 88 | 89 | expect(model.items.length).toEqual(2); 90 | expect(model.items[0].name).toEqual('Apple'); 91 | expect(model.items[1].name).toEqual('Banana'); 92 | }); 93 | 94 | it('should not remove anything', function() { 95 | model.removeItem({name: 'Name_not_in_cart'}); 96 | 97 | expect(model.items.length).toEqual(3); 98 | expect(model.items[0].name).toEqual('Apple'); 99 | expect(model.items[1].name).toEqual('Pear'); 100 | expect(model.items[2].name).toEqual('Banana'); 101 | }); 102 | 103 | }); 104 | 105 | describe('the processItems method', function() { 106 | it('should calculate total', function() { 107 | expect(model.total).toEqual(5.90); 108 | }); 109 | 110 | it('should calculate qty', function() { 111 | expect(model.qty).toEqual(10); 112 | }); 113 | 114 | it('should set most expensive item', function() { 115 | expect(model.expItem.name).toEqual('Pear'); 116 | expect(model.expItem.qty).toEqual(5); 117 | expect(model.expItem.price).toEqual(.75); 118 | }); 119 | 120 | it('should set cheapest item', function() { 121 | expect(model.cheapItem.name).toEqual('Banana'); 122 | expect(model.cheapItem.qty).toEqual(3); 123 | expect(model.cheapItem.price).toEqual(.35); 124 | }); 125 | }); 126 | 127 | }); 128 | 129 | }); -------------------------------------------------------------------------------- /src/app/customWidgetSettings.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 | .controller('CustomSettingsDemoCtrl', function($scope, $window, widgetDefinitions, defaultWidgets, RandomDataModel) { 21 | 22 | // Add an additional widget with setting overrides 23 | var definitions = [{ 24 | name: 'congfigurable widget', 25 | directive: 'wt-scope-watch', 26 | dataAttrName: 'value', 27 | dataModelType: RandomDataModel, 28 | dataModelOptions: { 29 | limit: 10 30 | }, 31 | settingsModalOptions: { 32 | partialTemplateUrl: 'app/template/configurableWidgetModalOptions.html' 33 | }, 34 | onSettingsClose: function (result, widget) { 35 | if (widget.dataModel && widget.dataModel.updateLimit) { 36 | widget.dataModel.updateLimit(result.dataModelOptions.limit); 37 | } 38 | } 39 | }, { 40 | name: 'override modal widget', 41 | directive: 'wt-scope-watch', 42 | dataAttrName: 'value', 43 | dataModelType: RandomDataModel, 44 | settingsModalOptions: { 45 | templateUrl: 'app/template/WidgetSpecificSettings.html', 46 | controller: 'WidgetSpecificSettingsCtrl', 47 | backdrop: false 48 | }, 49 | onSettingsClose: function(result, widget) { 50 | jQuery.extend(true, widget, result); 51 | }, 52 | onSettingsDismiss: function(reason, scope) { 53 | // console.log('Widget-specific settings dismissed', reason); 54 | } 55 | }]; 56 | 57 | var defaultWidgets = [ 58 | { name: 'congfigurable widget' }, 59 | { name: 'override modal widget' } 60 | ]; 61 | 62 | $scope.dashboardOptions = { 63 | widgetButtons: true, 64 | widgetDefinitions: definitions, 65 | defaultWidgets: defaultWidgets, 66 | storage: $window.localStorage, 67 | storageId: 'custom-settings', 68 | 69 | /* 70 | // Overrides default $uibModal options. 71 | // This can also be set on individual 72 | // widget definition objects (see above). 73 | settingsModalOptions: { 74 | // This will completely override the modal template for all widgets. 75 | // You also have the option to add to the default modal template with settingsModalOptions.partialTemplateUrl (see "configurable widget" above) 76 | templateUrl: 'template/customSettingsTemplate.html' 77 | // We could pass a custom controller name here to be used 78 | // with the widget settings dialog, but for this demo we 79 | // will just keep the default. 80 | // 81 | // controller: 'CustomSettingsModalCtrl' 82 | // 83 | // Other options passed to $uibModal.open can be put here, 84 | // eg: 85 | // 86 | // backdrop: false, 87 | // keyboard: false 88 | // 89 | // @see http://angular-ui.github.io/bootstrap/#/modal <-- heads up: routing on their site was broken as of this writing 90 | }, 91 | */ 92 | 93 | // Called when a widget settings dialog is closed 94 | // by the "ok" method (i.e., the promise is resolved 95 | // and not rejected). This can also be set on individual 96 | // widgets (see above). 97 | onSettingsClose: function(result, widget, scope) { 98 | // console.log('Settings result: ', result); 99 | // console.log('Widget: ', widget); 100 | // console.log('Dashboard scope: ', scope); 101 | jQuery.extend(true, widget, result); 102 | }, 103 | 104 | // Called when a widget settings dialog is closed 105 | // by the "cancel" method (i.e., the promise is rejected 106 | // and not resolved). This can also be set on individual 107 | // widgets (see above). 108 | onSettingsDismiss: function(reason, scope) { 109 | // console.log('Settings have been dismissed: ', reason); 110 | // console.log('Dashboard scope: ', scope); 111 | } 112 | }; 113 | 114 | $scope.prependWidget = function() { 115 | $scope.dashboardOptions.prependWidget({ name: 'congfigurable widget', title: 'Prepend Widget'}); 116 | }; 117 | }) 118 | .controller('WidgetSpecificSettingsCtrl', function ($scope, $uibModalInstance, widget) { 119 | // add widget to scope 120 | $scope.widget = widget; 121 | 122 | // set up result object 123 | $scope.result = jQuery.extend(true, {}, widget); 124 | 125 | $scope.ok = function () { 126 | // console.log('calling ok from widget-specific settings controller!'); 127 | $uibModalInstance.close($scope.result); 128 | }; 129 | 130 | $scope.cancel = function () { 131 | // console.log('calling cancel from widget-specific settings controller!'); 132 | $uibModalInstance.dismiss('cancel'); 133 | }; 134 | }) 135 | -------------------------------------------------------------------------------- /src/app/customWidgetSettings.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: CustomSettingsDemoCtrl', function() { 4 | 5 | var $scope, injections, WidgetDataModel, RandomDataModel; 6 | 7 | beforeEach(module('app')); 8 | 9 | beforeEach(inject(function($rootScope, $controller, $window, _WidgetDataModel_, _RandomDataModel_){ 10 | WidgetDataModel = _WidgetDataModel_; 11 | RandomDataModel = _RandomDataModel_; 12 | 13 | $scope = $rootScope.$new(); 14 | 15 | injections = { 16 | $scope: $scope, 17 | $window: $window, 18 | RandomDataModel: RandomDataModel 19 | }; 20 | $controller('CustomSettingsDemoCtrl', injections); 21 | })); 22 | 23 | describe('the controller properties', function() { 24 | 25 | it('should have properties in scope', function() { 26 | expect($scope.dashboardOptions.widgetButtons).toEqual(true); 27 | expect($scope.dashboardOptions.widgetDefinitions.length).toEqual(2); 28 | expect($scope.dashboardOptions.widgetDefinitions[0].name).toEqual('congfigurable widget'); 29 | expect($scope.dashboardOptions.widgetDefinitions[0].directive).toEqual('wt-scope-watch'); 30 | expect($scope.dashboardOptions.widgetDefinitions[0].dataAttrName).toEqual('value'); 31 | expect($scope.dashboardOptions.widgetDefinitions[0].dataModelType).toBeDefined(); 32 | expect($scope.dashboardOptions.widgetDefinitions[0].dataModelOptions.limit).toEqual(10); 33 | expect($scope.dashboardOptions.widgetDefinitions[0].settingsModalOptions.partialTemplateUrl).toEqual('app/template/configurableWidgetModalOptions.html'); 34 | expect(typeof $scope.dashboardOptions.widgetDefinitions[0].onSettingsClose).toEqual('function'); 35 | 36 | expect($scope.dashboardOptions.widgetDefinitions[1].name).toEqual('override modal widget'); 37 | expect($scope.dashboardOptions.widgetDefinitions[1].directive).toEqual('wt-scope-watch'); 38 | expect($scope.dashboardOptions.widgetDefinitions[1].dataAttrName).toEqual('value'); 39 | expect($scope.dashboardOptions.widgetDefinitions[1].dataModelType).toBeDefined(); 40 | expect($scope.dashboardOptions.widgetDefinitions[1].settingsModalOptions.templateUrl).toEqual('app/template/WidgetSpecificSettings.html'); 41 | expect($scope.dashboardOptions.widgetDefinitions[1].settingsModalOptions.controller).toEqual('WidgetSpecificSettingsCtrl'); 42 | expect($scope.dashboardOptions.widgetDefinitions[1].settingsModalOptions.backdrop).toBe(false); 43 | expect(typeof $scope.dashboardOptions.widgetDefinitions[1].onSettingsClose).toEqual('function'); 44 | expect(typeof $scope.dashboardOptions.widgetDefinitions[1].onSettingsDismiss).toEqual('function'); 45 | }); 46 | 47 | }); 48 | 49 | describe('the definition onSettingsClose function', function() { 50 | 51 | it('should update the limit', function() { 52 | var widget = $scope.dashboardOptions.widgetDefinitions[0]; 53 | widget.dataModel = new RandomDataModel(); 54 | widget.dataModel.setup(widget, $scope); 55 | widget.dataModel.init(); 56 | var result = { 57 | dataModelOptions: { 58 | limit: 20 59 | } 60 | }; 61 | widget.onSettingsClose(result, widget); 62 | expect(widget.dataModelOptions.limit).toEqual(20); 63 | }); 64 | 65 | it('should not update the limit', function() { 66 | var widget = $scope.dashboardOptions.widgetDefinitions[0]; 67 | var result = { 68 | dataModelOptions: { 69 | limit: 20 70 | } 71 | }; 72 | widget.onSettingsClose(result, widget); 73 | expect(widget.dataModelOptions.limit).toEqual(10); 74 | }); 75 | 76 | it('should update the title', function() { 77 | var widget = $scope.dashboardOptions.widgetDefinitions[1]; 78 | widget.dataModel = new RandomDataModel(); 79 | widget.dataModel.setup(widget, $scope); 80 | widget.dataModel.init(); 81 | var result = { 82 | title: 'new widget title' 83 | }; 84 | widget.onSettingsClose(result, widget); 85 | expect(widget.title).toEqual('new widget title'); 86 | }); 87 | 88 | it('should call the settings dismiss', function() { 89 | expect(function() { 90 | $scope.dashboardOptions.widgetDefinitions[1].onSettingsDismiss(); 91 | }).not.toThrow(); 92 | }); 93 | 94 | }); 95 | 96 | describe('the dashboardOptions onSettingsClose function', function() { 97 | 98 | it('should update the title', function() { 99 | var widget = $scope.dashboardOptions.widgetDefinitions[1]; 100 | widget.dataModel = new RandomDataModel(); 101 | widget.dataModel.setup(widget, $scope); 102 | widget.dataModel.init(); 103 | var result = { 104 | title: 'new widget title' 105 | }; 106 | $scope.dashboardOptions.onSettingsClose(result, widget); 107 | expect(widget.title).toEqual('new widget title'); 108 | }); 109 | 110 | it('should call the settings dismiss', function() { 111 | expect(function() { 112 | $scope.dashboardOptions.onSettingsDismiss(); 113 | }).not.toThrow(); 114 | }); 115 | 116 | }); 117 | 118 | }); 119 | 120 | describe('Controller: WidgetSpecificSettingsCtrl', function() { 121 | 122 | var $scope, uibModalInstance, widget = {}; 123 | 124 | beforeEach(module('app')); 125 | 126 | beforeEach(inject(function($rootScope, $controller) { 127 | $scope = $rootScope.$new(); 128 | 129 | uibModalInstance = { 130 | close: function() { 131 | 132 | }, 133 | dismiss: function() { 134 | 135 | } 136 | }; 137 | spyOn(uibModalInstance, 'close'); 138 | spyOn(uibModalInstance, 'dismiss'); 139 | 140 | // let's mock widget 141 | $scope.widget = { 142 | includeUrl: 'app/template/peopleList.html' 143 | }; 144 | 145 | $controller('WidgetSpecificSettingsCtrl', { 146 | $scope: $scope, 147 | $uibModalInstance: uibModalInstance, 148 | widget: widget 149 | }); 150 | })); 151 | 152 | describe('the controller properties', function() { 153 | 154 | it('should have widget in scope', function() { 155 | expect($scope.widget).toBeDefined(); 156 | expect($scope.result).toBeDefined(); 157 | }); 158 | 159 | }); 160 | 161 | describe('the ok function', function() { 162 | 163 | it('should call modal close', function() { 164 | $scope.ok(); 165 | expect(uibModalInstance.close).toHaveBeenCalled(); 166 | }); 167 | 168 | }); 169 | 170 | describe('the dismiss function', function() { 171 | 172 | it('should call modal dismiss', function() { 173 | $scope.cancel(); 174 | expect(uibModalInstance.dismiss).toHaveBeenCalled(); 175 | }); 176 | 177 | }); 178 | 179 | }); -------------------------------------------------------------------------------- /src/app/dataModel.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 | .factory('RandomDataModel', function ($interval, WidgetDataModel) { 21 | function RandomDataModel() { 22 | } 23 | 24 | RandomDataModel.prototype = Object.create(WidgetDataModel.prototype); 25 | RandomDataModel.prototype.constructor = WidgetDataModel; 26 | 27 | angular.extend(RandomDataModel.prototype, { 28 | init: function () { 29 | var dataModelOptions = this.dataModelOptions; 30 | this.limit = (dataModelOptions && dataModelOptions.limit) ? dataModelOptions.limit : 100; 31 | 32 | this.updateScope('-'); 33 | this.startInterval(); 34 | }, 35 | 36 | startInterval: function () { 37 | $interval.cancel(this.intervalPromise); 38 | 39 | this.intervalPromise = $interval(function () { 40 | var value = Math.floor(Math.random() * this.limit); 41 | this.updateScope(value); 42 | }.bind(this), 500); 43 | }, 44 | 45 | updateLimit: function (limit) { 46 | this.dataModelOptions = this.dataModelOptions ? this.dataModelOptions : {}; 47 | this.dataModelOptions.limit = limit; 48 | this.limit = limit; 49 | }, 50 | 51 | destroy: function () { 52 | WidgetDataModel.prototype.destroy.call(this); 53 | $interval.cancel(this.intervalPromise); 54 | } 55 | }); 56 | 57 | return RandomDataModel; 58 | }); -------------------------------------------------------------------------------- /src/app/dataModel.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 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 | 18 | 'use strict'; 19 | 20 | describe('Factory: RandomDataModel', function () { 21 | 22 | var $interval, RandomDataModel, model; 23 | 24 | // load the service's module 25 | beforeEach(module('app')); 26 | 27 | // instantiate service 28 | beforeEach(inject(function (_$interval_, _RandomDataModel_) { 29 | $interval = _$interval_; 30 | RandomDataModel = _RandomDataModel_; 31 | })); 32 | 33 | beforeEach(function() { 34 | model = new RandomDataModel(); 35 | model.setup({}, {}); 36 | spyOn(model, 'updateScope').and.callThrough(); 37 | spyOn(model, 'startInterval').and.callThrough(); 38 | spyOn($interval, 'cancel').and.callThrough(); 39 | }); 40 | 41 | describe('the constructor', function() { 42 | 43 | it('should have model functions', function() { 44 | expect(typeof model.constructor).toEqual('function'); 45 | expect(typeof model.destroy).toEqual('function'); 46 | expect(typeof model.init).toEqual('function'); 47 | expect(typeof model.startInterval).toEqual('function'); 48 | expect(typeof model.updateLimit).toEqual('function'); 49 | }); 50 | 51 | }); 52 | 53 | describe('the init function', function() { 54 | 55 | it('should use default as limit', function() { 56 | model.init(); 57 | expect(model.limit).toEqual(100); 58 | expect(model.updateScope).toHaveBeenCalled(); 59 | expect(model.startInterval).toHaveBeenCalled(); 60 | }); 61 | 62 | it('should set limit', function() { 63 | model.dataModelOptions = { 64 | limit: 30 65 | }; 66 | model.init(); 67 | expect(model.limit).toEqual(30); 68 | }); 69 | 70 | }); 71 | 72 | describe('the startInterval function', function() { 73 | 74 | it('should generate a random value', function() { 75 | model.startInterval(); 76 | $interval.flush(500); 77 | expect(model.updateScope).toHaveBeenCalled(); 78 | }); 79 | 80 | }); 81 | 82 | describe('the updateLimit function', function() { 83 | 84 | it('should set the limit', function() { 85 | model.init(); 86 | model.updateLimit(50); 87 | expect(model.dataModelOptions).toEqual({ limit: 50 }); 88 | expect(model.limit).toEqual(50); 89 | }); 90 | 91 | it('should use existing dataModelOptions', function() { 92 | model.init(); 93 | model.dataModelOptions = { 94 | extra: 'this is an extra property in the dataModelOptions' 95 | }; 96 | model.updateLimit(80); 97 | expect(model.dataModelOptions.extra).toEqual('this is an extra property in the dataModelOptions'); 98 | expect(model.dataModelOptions.limit).toEqual(80); 99 | expect(model.limit).toEqual(80); 100 | }); 101 | 102 | }); 103 | 104 | describe('the destroy function', function() { 105 | 106 | it('should call $interval.cancel', function() { 107 | model.destroy(); 108 | expect($interval.cancel).toHaveBeenCalled(); 109 | }); 110 | 111 | }); 112 | 113 | }); -------------------------------------------------------------------------------- /src/app/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 | 'btford.markdown' 23 | ]) 24 | .config(function ($routeProvider) { 25 | $routeProvider 26 | .when('/', { 27 | templateUrl: 'app/template/view.html', 28 | controller: 'DemoCtrl', 29 | title: 'simple', 30 | description: 'This is the simplest demo.' 31 | }) 32 | .when('/resize', { 33 | templateUrl: 'app/template/view.html', 34 | controller: 'ResizeDemoCtrl', 35 | title: 'resize', 36 | description: 'This demo showcases widget resizing.' 37 | }) 38 | .when('/custom-settings', { 39 | templateUrl: 'app/template/view.html', 40 | controller: 'CustomSettingsDemoCtrl', 41 | title: 'custom widget settings', 42 | description: 'This demo showcases overriding the widget settings dialog/modal ' + 43 | 'for the entire dashboard and for a specific widget. Click on the cog of each ' + 44 | 'widget to see the custom modal. \n"configurable widget" has "limit" option in the modal ' + 45 | 'that controls RandomDataModel.' 46 | }) 47 | .when('/explicit-saving', { 48 | templateUrl: 'app/template/view.html', 49 | controller: 'ExplicitSaveDemoCtrl', 50 | title: 'explicit saving', 51 | description: 'This demo showcases an option to only save the dashboard state '+ 52 | 'explicitly, e.g. by user input. Notice the "all saved" button in the controls ' + 53 | 'updates as you make saveable changes.' 54 | }) 55 | .when('/layouts', { 56 | templateUrl: 'app/template/layouts.html', 57 | controller: 'LayoutsDemoCtrl', 58 | title: 'dashboard layouts', 59 | description: 'This demo showcases the ability to have "dashboard layouts", ' + 60 | 'meaning the ability to have multiple arbitrary configurations of widgets. For more ' + 61 | 'information, take a look at [issue #31](https://github.com/DataTorrent/malhar-angular-dashboard/issues/31)' 62 | }) 63 | .when('/layouts/explicit-saving', { 64 | templateUrl: 'app/template/layouts.html', 65 | controller: 'LayoutsDemoExplicitSaveCtrl', 66 | title: 'layouts explicit saving', 67 | description: 'This demo showcases dashboard layouts with explicit saving enabled.' 68 | }) 69 | .when('/dynamic-options', { 70 | templateUrl: 'app/template/dynamicOptions.html', 71 | controller: 'DynamicOptionsCtrl', 72 | title: 'dynamic options', 73 | description: 'This demo showcases loading dashboard options dynamically.' 74 | }) 75 | .when('/dynamic-data', { 76 | templateUrl: 'app/template/dynamicData.html', 77 | controller: 'DynamicDataCtrl', 78 | title: 'dynamic data', 79 | description: 'This demo showcases loading the widgets and refreshing the contents as the source data is updated.' 80 | }) 81 | .otherwise({ 82 | redirectTo: '/' 83 | }); 84 | }) 85 | .controller('NavBarCtrl', function($scope, $route) { 86 | $scope.$route = $route; 87 | }) 88 | .factory('widgetDefinitions', function(RandomDataModel) { 89 | return [ 90 | { 91 | name: 'random', 92 | directive: 'wt-scope-watch', 93 | attrs: { 94 | value: 'randomValue' 95 | } 96 | }, 97 | { 98 | name: 'time', 99 | directive: 'wt-time' 100 | }, 101 | { 102 | name: 'datamodel', 103 | directive: 'wt-scope-watch', 104 | dataAttrName: 'value', 105 | dataModelType: RandomDataModel 106 | }, 107 | { 108 | name: 'resizable', 109 | templateUrl: 'app/template/resizable.html', 110 | attrs: { 111 | class: 'demo-widget-resizable' 112 | } 113 | }, 114 | { 115 | name: 'fluid', 116 | directive: 'wt-fluid', 117 | size: { 118 | width: '50%', 119 | height: '250px' 120 | } 121 | } 122 | ]; 123 | }) 124 | .value('defaultWidgets', [ 125 | { name: 'random' }, 126 | { name: 'time' }, 127 | { name: 'datamodel' }, 128 | { 129 | name: 'random', 130 | style: { 131 | width: '50%', 132 | minWidth: '39%' 133 | } 134 | }, 135 | { 136 | name: 'time', 137 | style: { 138 | width: '50%' 139 | } 140 | } 141 | ]) 142 | .controller('DemoCtrl', function ($scope, $interval, $window, widgetDefinitions, defaultWidgets) { 143 | 144 | $scope.dashboardOptions = { 145 | widgetButtons: true, 146 | widgetDefinitions: widgetDefinitions, 147 | defaultWidgets: defaultWidgets, 148 | storage: $window.localStorage, 149 | storageId: 'demo_simple' 150 | }; 151 | $scope.randomValue = Math.random(); 152 | $interval(function () { 153 | $scope.randomValue = Math.random(); 154 | }, 500); 155 | 156 | $scope.prependWidget = function() { 157 | $scope.dashboardOptions.prependWidget({ name: 'random', title: 'Prepend Widget'}); 158 | }; 159 | 160 | }); 161 | 162 | -------------------------------------------------------------------------------- /src/app/demo.less: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 15px; 3 | padding-top: 50px; 4 | } 5 | a { 6 | cursor: pointer; 7 | } 8 | .layout-tabs { 9 | margin-bottom: 10px; 10 | } 11 | 12 | .demo-widget-fluid { 13 | border: 1px solid blue; 14 | height: 100%; 15 | } 16 | 17 | .demo-widget-fluid > div { 18 | border: 1px solid red; 19 | position: relative; 20 | top: 50%; 21 | -webkit-transform: translateY(-50%); 22 | -ms-transform: translateY(-50%); 23 | transform: translateY(-50%); 24 | } -------------------------------------------------------------------------------- /src/app/demo.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: NavBarCtrl', function() { 4 | 5 | var $scope, injections; 6 | 7 | beforeEach(module('app')); 8 | 9 | beforeEach(inject(function($rootScope, $controller, $route){ 10 | $scope = $rootScope.$new(); 11 | 12 | injections = { 13 | $scope: $scope, 14 | $route: $route 15 | }; 16 | $controller('NavBarCtrl', injections); 17 | })); 18 | 19 | describe('the controller properties', function() { 20 | 21 | it('should have $route in scope', function() { 22 | expect($scope.$route).toBeDefined(); 23 | }); 24 | 25 | }); 26 | 27 | }); 28 | 29 | describe('Factory: widgetDefinitions', function() { 30 | 31 | // load the service's module 32 | beforeEach(module('app')); 33 | 34 | // instantiate service 35 | var widgetDefinitions; 36 | beforeEach(inject(function (_widgetDefinitions_) { 37 | widgetDefinitions = _widgetDefinitions_; 38 | })); 39 | 40 | describe('the widgetDefinitions', function() { 41 | 42 | it('should be an array', function() { 43 | expect(_.isArray(widgetDefinitions)).toBe(true); 44 | expect(widgetDefinitions.length).toEqual(5); 45 | expect(widgetDefinitions[0].name).toEqual('random'); 46 | expect(widgetDefinitions[0].directive).toEqual('wt-scope-watch'); 47 | expect(widgetDefinitions[0].attrs).toEqual({value: 'randomValue'}); 48 | expect(widgetDefinitions[1].name).toEqual('time'); 49 | expect(widgetDefinitions[1].directive).toEqual('wt-time'); 50 | expect(widgetDefinitions[2].name).toEqual('datamodel'); 51 | expect(widgetDefinitions[2].directive).toEqual('wt-scope-watch'); 52 | expect(widgetDefinitions[2].dataAttrName).toEqual('value'); 53 | expect(typeof widgetDefinitions[2].dataModelType).toEqual('function') 54 | expect(widgetDefinitions[3].name).toEqual('resizable'); 55 | expect(widgetDefinitions[3].templateUrl).toEqual('app/template/resizable.html'); 56 | expect(widgetDefinitions[3].attrs).toEqual({class: 'demo-widget-resizable'}); 57 | expect(widgetDefinitions[4].name).toEqual('fluid'); 58 | expect(widgetDefinitions[4].directive).toEqual('wt-fluid'); 59 | expect(widgetDefinitions[4].size).toEqual({width: '50%', height: '250px'}); 60 | }); 61 | 62 | }); 63 | 64 | }); 65 | 66 | describe('Value: defaultWidgets', function() { 67 | 68 | beforeEach(module('app')); 69 | 70 | var defaultWidgets; 71 | beforeEach(inject(function(_defaultWidgets_) { 72 | defaultWidgets = _defaultWidgets_; 73 | })); 74 | 75 | describe('the defaultWidgets', function() { 76 | 77 | it('should be an array', function() { 78 | expect(_.isArray(defaultWidgets)).toBe(true); 79 | expect(defaultWidgets.length).toEqual(5); 80 | expect(defaultWidgets[0].name).toEqual('random'); 81 | expect(defaultWidgets[1].name).toEqual('time'); 82 | expect(defaultWidgets[2].name).toEqual('datamodel'); 83 | expect(defaultWidgets[3].name).toEqual('random'); 84 | expect(defaultWidgets[3].style).toEqual({width: '50%', minWidth: '39%'}); 85 | expect(defaultWidgets[4].name).toEqual('time'); 86 | expect(defaultWidgets[4].style).toEqual({width: '50%'}); 87 | }); 88 | 89 | }); 90 | 91 | }); 92 | 93 | describe('Controller: DemoCtrl', function() { 94 | 95 | var $scope, injections; 96 | 97 | beforeEach(module('app')); 98 | 99 | beforeEach(inject(function($rootScope, $controller, $window, $interval){ 100 | $scope = $rootScope.$new(); 101 | 102 | injections = { 103 | $scope: $scope, 104 | $window: $window, 105 | $interval: $interval 106 | }; 107 | $controller('DemoCtrl', injections); 108 | })); 109 | 110 | describe('the controller properties', function() { 111 | 112 | it('should dashboardOptions in scope', function() { 113 | expect($scope.randomValue).toBeDefined(); 114 | expect($scope.dashboardOptions.widgetButtons).toBe(true); 115 | expect($scope.dashboardOptions.widgetDefinitions).toBeDefined(); 116 | expect($scope.dashboardOptions.defaultWidgets).toBeDefined(); 117 | expect($scope.dashboardOptions.storage).toBeDefined(); 118 | expect($scope.dashboardOptions.storageId).toEqual('demo_simple'); 119 | }); 120 | 121 | it('should change randomValue', function() { 122 | var savedValue = $scope.randomValue; 123 | injections.$interval.flush(500); 124 | expect($scope.randomValue).not.toEqual(savedValue); 125 | 126 | }); 127 | 128 | }); 129 | 130 | }); -------------------------------------------------------------------------------- /src/app/directives.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 | .directive('wtTime', function ($interval) { 21 | return { 22 | restrict: 'A', 23 | scope: true, 24 | replace: true, 25 | template: '
Time
{{time}}
', 26 | link: function (scope) { 27 | function update() { 28 | scope.time = new Date().toLocaleTimeString(); 29 | } 30 | 31 | update(); 32 | 33 | var promise = $interval(update, 500); 34 | 35 | scope.$on('$destroy', function () { 36 | $interval.cancel(promise); 37 | }); 38 | } 39 | }; 40 | }) 41 | .directive('wtScopeWatch', function () { 42 | return { 43 | restrict: 'A', 44 | replace: true, 45 | template: '
Value
{{value}}
', 46 | scope: { 47 | value: '=value' 48 | } 49 | }; 50 | }) 51 | .directive('wtFluid', function () { 52 | return { 53 | restrict: 'A', 54 | replace: true, 55 | templateUrl: 'app/template/fluid.html', 56 | scope: true, 57 | controller: function ($scope) { 58 | $scope.$on('widgetResized', function (event, size) { 59 | $scope.width = size.width || $scope.width; 60 | $scope.height = size.height || $scope.height; 61 | }); 62 | } 63 | }; 64 | }); -------------------------------------------------------------------------------- /src/app/directives.spec.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('Directive: wtTime', function () { 19 | 20 | var $compile, $rootScope, $interval, element, isoScope; 21 | 22 | // load the directive's module 23 | beforeEach(module('app')); 24 | 25 | beforeEach(inject(function (_$compile_, _$rootScope_, _$interval_) { 26 | // Cache these for reuse 27 | $compile = _$compile_; 28 | $rootScope = _$rootScope_; 29 | $interval = _$interval_; 30 | })); 31 | 32 | beforeEach(function() { 33 | // Set up the outer scope 34 | var scope = $rootScope.$new(); 35 | var markup = '
'; 36 | 37 | // Define and compile the element 38 | element = angular.element(markup); 39 | element = $compile(element)(scope); 40 | scope.$digest(); 41 | isoScope = element.scope(); 42 | 43 | spyOn($interval, 'cancel').and.callThrough(); 44 | }); 45 | 46 | it('should create div with time string', function() { 47 | expect(element.text()).toEqual('Time' + isoScope.time); 48 | }); 49 | 50 | it('should update time as promised', function() { 51 | isoScope.time = 'some other text'; 52 | $interval.flush(500); 53 | expect(isoScope.time).not.toEqual('some other text'); 54 | }); 55 | 56 | it('should call $interval.cancel', function() { 57 | isoScope.$destroy(); 58 | expect($interval.cancel).toHaveBeenCalled(); 59 | }); 60 | 61 | }); 62 | 63 | describe('Directive: wtScopeWatch', function () { 64 | 65 | var $compile, $rootScope, $interval, element, isoScope; 66 | 67 | // load the directive's module 68 | beforeEach(module('app')); 69 | 70 | beforeEach(inject(function (_$compile_, _$rootScope_) { 71 | 72 | // Cache these for reuse 73 | $compile = _$compile_; 74 | $rootScope = _$rootScope_; 75 | })); 76 | 77 | beforeEach(function() { 78 | // Set up the outer scope 79 | var scope = $rootScope.$new(); 80 | var markup = '
'; 81 | 82 | scope.someValue = 'some randome text'; 83 | 84 | // Define and compile the element 85 | element = angular.element(markup); 86 | element = $compile(element)(scope); 87 | scope.$digest(); 88 | isoScope = element.scope(); 89 | }); 90 | 91 | it('should bind value to div', function() { 92 | expect(element.text()).toEqual('Valuesome randome text'); 93 | }); 94 | 95 | }); 96 | 97 | describe('Directive: wtFluid', function () { 98 | 99 | var $compile, $rootScope, $templateCache, $interval, element, isoScope; 100 | 101 | beforeEach(module('app')); 102 | 103 | beforeEach(inject(function (_$compile_, _$rootScope_, _$templateCache_) { 104 | // Cache these for reuse 105 | $compile = _$compile_; 106 | $rootScope = _$rootScope_; 107 | $templateCache = _$templateCache_; 108 | })); 109 | 110 | beforeEach(function() { 111 | // let's add the fluid.html to the temlateCache 112 | $templateCache.put("app/template/fluid.html","
\n
\n

Widget takes 100% height (blue border).

\n

Resize the widget vertically to see that this text (red border) stays middle aligned.

\n

New width: {{width}}

\n

New height: {{height}}

\n
\n
"); 113 | 114 | // Set up the outer scope 115 | var scope = $rootScope.$new(); 116 | var markup = '
'; 117 | 118 | scope.width = 10; 119 | scope.height = 20; 120 | 121 | // Define and compile the element 122 | element = angular.element(markup); 123 | element = $compile(element)(scope); 124 | scope.$digest(); 125 | isoScope = element.scope(); 126 | }); 127 | 128 | it('should render html with divs', function() { 129 | expect(element.find('p.ng-binding').length).toEqual(2); 130 | }); 131 | 132 | it('should update size', function() { 133 | var size = { 134 | width: 50, 135 | height: 60 136 | }; 137 | isoScope.$emit('widgetResized', size); 138 | expect(isoScope.width).toEqual(50); 139 | expect(isoScope.height).toEqual(60); 140 | }); 141 | 142 | it('should use scope size', function() { 143 | isoScope.$emit('widgetResized', {}); 144 | expect(isoScope.width).toEqual(10); 145 | expect(isoScope.height).toEqual(20); 146 | }); 147 | 148 | }); -------------------------------------------------------------------------------- /src/app/dynamicData.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 | .controller('DynamicDataCtrl', function ($scope, $window, widgetDefinitions, defaultWidgets, CartDataModel) { 21 | 22 | $scope.cart = new CartDataModel(); 23 | $scope.item = { 24 | name: '', 25 | qty: 0, 26 | price: 0 27 | }; 28 | 29 | var definitions = [{ 30 | name: 'cartDetail', 31 | title: 'cart detail', 32 | templateUrl: 'app/template/cartDetail.html', 33 | size: { width: '800px', minWidth: '600px', }, 34 | cart: $scope.cart 35 | }, { 36 | name: 'cartSummary', 37 | title: 'cart summary', 38 | templateUrl: 'app/template/cartSummary.html', 39 | size: { width: '400px', minWidth: '400px', }, 40 | cart: $scope.cart 41 | }]; 42 | 43 | var defaultWidgets = [ 44 | { name: 'cartDetail' }, 45 | { name: 'cartSummary' } 46 | ]; 47 | 48 | $scope.dashboardOptions = { 49 | hideToolbar: true, 50 | widgetDefinitions: definitions, 51 | defaultWidgets: defaultWidgets, 52 | storage: $window.localStorage, 53 | storageId: 'demo_dynamic-data' 54 | }; 55 | 56 | $scope.addItem = function() { 57 | if (!_.isEmpty($scope.item.name) && 58 | $scope.item.qty !== undefined && $scope.item.qty > 0 && 59 | $scope.item.price !== undefined && $scope.item.price > 0) { 60 | // only add item to cart if form is valid 61 | $scope.cart.addItem($scope.item); 62 | $scope.item = { 63 | name: '', 64 | qty: 0, 65 | price: 0 66 | }; 67 | } 68 | }; 69 | 70 | $scope.autoFillCart = function() { 71 | var list = [ 'Apple', 'Banana', 'Coke', 'Milk', 'Pear', 'Water' ]; 72 | for(var i = 0; i < list.length; i++) { 73 | $scope.cart.addItem({ 74 | name: list[i], 75 | qty: _.random(1, 10), 76 | price: _.round(_.random(1, 10, true), 2) 77 | }); 78 | } 79 | }; 80 | }) 81 | .controller('CartCtrl', function ($scope) { 82 | $scope.removeItem = function(item) { 83 | $scope.cart.removeItem(item); 84 | }; 85 | }); -------------------------------------------------------------------------------- /src/app/dynamicData.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: DynamicDataCtrl', function() { 4 | 5 | var $scope, $element; 6 | 7 | beforeEach(module('app')); 8 | 9 | beforeEach(inject(function($rootScope, $controller, $window){ 10 | $scope = $rootScope.$new(); 11 | 12 | $controller('DynamicDataCtrl', { 13 | $scope: $scope, 14 | $window: $window 15 | }); 16 | })); 17 | 18 | describe('the controller properties', function() { 19 | 20 | it('should have properties in scope', function() { 21 | expect($scope.cart.items.length).toEqual(0); 22 | expect($scope.item.name).toEqual(''); 23 | expect($scope.item.qty).toEqual(0); 24 | expect($scope.item.price).toEqual(0); 25 | expect($scope.dashboardOptions.hideToolbar).toEqual(true); 26 | expect($scope.dashboardOptions.widgetDefinitions.length).toEqual(2); 27 | expect($scope.dashboardOptions.widgetDefinitions[0].name).toEqual('cartDetail'); 28 | expect($scope.dashboardOptions.widgetDefinitions[0].title).toEqual('cart detail'); 29 | expect($scope.dashboardOptions.widgetDefinitions[0].templateUrl).toEqual('app/template/cartDetail.html'); 30 | expect($scope.dashboardOptions.widgetDefinitions[0].size).toBeDefined(); 31 | expect($scope.dashboardOptions.widgetDefinitions[0].cart).toEqual($scope.cart); 32 | expect($scope.dashboardOptions.widgetDefinitions[1].name).toEqual('cartSummary'); 33 | expect($scope.dashboardOptions.widgetDefinitions[1].title).toEqual('cart summary'); 34 | expect($scope.dashboardOptions.widgetDefinitions[1].templateUrl).toEqual('app/template/cartSummary.html'); 35 | expect($scope.dashboardOptions.widgetDefinitions[1].size).toBeDefined(); 36 | expect($scope.dashboardOptions.widgetDefinitions[1].cart).toEqual($scope.cart); 37 | expect($scope.dashboardOptions.defaultWidgets).toBeDefined(); 38 | expect($scope.dashboardOptions.storage).toBeDefined(); 39 | expect($scope.dashboardOptions.storageId).toEqual('demo_dynamic-data'); 40 | }); 41 | 42 | }); 43 | 44 | describe('the addItem method', function() { 45 | 46 | it('should add item to cart', function() { 47 | // simulate user filling the inputs and click Add 48 | $scope.item.name = 'Apple'; 49 | $scope.item.qty = 2; 50 | $scope.item.price = .75; 51 | $scope.addItem(); 52 | expect($scope.cart.items.length).toEqual(1); 53 | expect($scope.item.name).toEqual(''); 54 | expect($scope.item.qty).toEqual(0); 55 | expect($scope.item.price).toEqual(0); 56 | }); 57 | 58 | it('should not add item with blank name', function() { 59 | $scope.item.qty = 2; 60 | $scope.item.price = .75; 61 | $scope.addItem(); 62 | expect($scope.cart.items.length).toEqual(0); 63 | }); 64 | 65 | it('should not add item with qty 0', function() { 66 | $scope.item.name = 'Apple'; 67 | $scope.item.price = .75; 68 | $scope.addItem(); 69 | expect($scope.cart.items.length).toEqual(0); 70 | }); 71 | 72 | it('should not add item with price 0', function() { 73 | $scope.item.name = 'Apple'; 74 | $scope.item.qty = 2; 75 | $scope.addItem(); 76 | expect($scope.cart.items.length).toEqual(0); 77 | }); 78 | 79 | }); 80 | 81 | describe('the autoFillCart method', function() { 82 | 83 | it('should fill cart with sample items', function() { 84 | $scope.autoFillCart(); 85 | expect($scope.cart.items.length).toEqual(6); 86 | }); 87 | 88 | }); 89 | 90 | }); 91 | 92 | 93 | describe('Controller: CartCtrl', function() { 94 | 95 | var $scope, $element, CartDataModel; 96 | 97 | beforeEach(module('app')); 98 | 99 | beforeEach(inject(function($rootScope, $controller, _CartDataModel_) { 100 | CartDataModel = _CartDataModel_; 101 | 102 | $scope = $rootScope.$new(); 103 | 104 | $scope.cart = new CartDataModel(); 105 | 106 | $scope.cart.addItem({ name: 'Apple', qty: 2, price: .55 }); 107 | $scope.cart.addItem({ name: 'Banana', qty: 5, price: .75 }); 108 | $scope.cart.addItem({ name: 'Orange', qty: 3, price: .35 }); 109 | 110 | $controller('CartCtrl', { $scope: $scope }); 111 | })); 112 | 113 | describe('the controller properties', function() { 114 | 115 | it('should have cart in scope', function() { 116 | expect($scope.cart.items.length).toEqual(3); 117 | }); 118 | 119 | }); 120 | 121 | describe('the removeItem method', function() { 122 | 123 | it('should remove one item', function() { 124 | $scope.removeItem($scope.cart.items[1]); 125 | expect($scope.cart.items.length).toEqual(2); 126 | expect($scope.cart.items[0].name).toEqual('Apple'); 127 | expect($scope.cart.items[1].name).toEqual('Orange'); 128 | }); 129 | 130 | }); 131 | 132 | }); -------------------------------------------------------------------------------- /src/app/dynamicOptions.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 | .controller('DynamicOptionsCtrl', function ($scope, $timeout, $window, widgetDefinitions, defaultWidgets) { 21 | var definitions = [{ 22 | name: 'peopleList', 23 | title: 'people list', 24 | templateUrl: 'app/template/dynamicOptionsContainer.html', 25 | size: { width: '800px', minWidth: '600px', }, 26 | includeUrl: 'app/template/peopleList.html' 27 | }, { 28 | name: 'peopleThumbnail', 29 | title: 'people thumbnail', 30 | templateUrl: 'app/template/dynamicOptionsContainer.html', 31 | size: { width: '1000px', minWidth: '800px', }, 32 | includeUrl: 'app/template/peopleThumbnail.html' 33 | }]; 34 | 35 | $scope.style = 'peopleList'; 36 | 37 | var defaultWidgets = [ 38 | { name: $scope.style } 39 | ]; 40 | 41 | $scope.dashboardOptions = { 42 | hideToolbar: true, 43 | widgetDefinitions: definitions, 44 | defaultWidgets: defaultWidgets, 45 | storage: $window.localStorage, 46 | storageId: 'demo_dynamic-options_' + Date.now() 47 | }; 48 | 49 | $scope.toggleWidget = function() { 50 | $scope.style = ($scope.style === 'peopleList' ? 'peopleThumbnail' : 'peopleList'); 51 | 52 | var obj = _.cloneDeep($scope.dashboardOptions); 53 | obj.defaultWidgets[0].name = $scope.style; 54 | obj.storageId = 'demo_dynamic-options_' + Date.now(); 55 | $timeout(function() { 56 | delete $scope.dashboardOptions; 57 | }, 0); 58 | $timeout(function() { 59 | $scope.dashboardOptions = obj; 60 | }, 0); 61 | }; 62 | }) 63 | .controller('PeopleCtrl', function ($scope) { 64 | $scope.toggleTemplate = function() { 65 | $scope.widget.includeUrl = ($scope.widget.includeUrl === 'app/template/peopleList.html' ? 'app/template/peopleThumbnail.html' : 'app/template/peopleList.html'); 66 | }; 67 | 68 | $scope.removePerson = function(person) { 69 | var index = _.findIndex($scope.people, function(p) { 70 | return p.$$hashKey === person.$$hashKey; 71 | }); 72 | if (index > -1) { 73 | $scope.people.splice(index, 1); 74 | } 75 | }; 76 | 77 | function generatePeople() { 78 | var firstNames = [ 'James', 'Christopher', 'Ronald', 'Mary', 'Lisa', 'Michelle', 'John', 'Daniel', 'Anthony', 'Patricia', 'Nancy', 'Laura' ], 79 | lastNames = [ 'Smith', 'Anderson', 'Clark', 'Wright', 'Mitchell', 'Johnson', 'Thomas', 'Rodriguez', 'Lopez', 'Perez' ], 80 | ary = [], f, l; 81 | 82 | while(ary.length < 10) { 83 | f = Math.floor(Math.random() * (firstNames.length)); 84 | l = Math.floor(Math.random() * (lastNames.length)); 85 | ary.push({ 86 | name: firstNames[f] + ' ' + lastNames[l], 87 | email: lastNames[l].toLowerCase() + '@company.com', 88 | phone: Math.random().toString().slice(2, 12).match(/^(\d{3})(\d{3})(\d{4})/).slice(1).join('-') 89 | }); 90 | firstNames.splice(f, 1); 91 | lastNames.splice(l, 1); 92 | } 93 | 94 | return ary; 95 | } 96 | 97 | $scope.people = generatePeople(); 98 | }); -------------------------------------------------------------------------------- /src/app/dynamicOptions.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: DynamicOptionsCtrl', function() { 4 | 5 | var $scope, injections; 6 | 7 | beforeEach(module('app')); 8 | 9 | beforeEach(inject(function($rootScope, $controller, $window, $timeout){ 10 | $scope = $rootScope.$new(); 11 | 12 | // let's mock the $timeout so we can spy on it 13 | // however, we still want to retain the original $timeout functionality 14 | // to test for time sensitive operations 15 | function timeout(fn, delay) { 16 | $timeout(fn, delay); 17 | }; 18 | timeout.flush = function(ms) { 19 | $timeout.flush(ms); 20 | }; 21 | 22 | injections = { 23 | $scope: $scope, 24 | $timeout: timeout, 25 | $window: $window 26 | }; 27 | spyOn(injections, '$timeout').and.callThrough(); 28 | $controller('DynamicOptionsCtrl', injections); 29 | })); 30 | 31 | describe('the controller properties', function() { 32 | 33 | it('should have properties in scope', function() { 34 | expect($scope.style).toEqual('peopleList'); 35 | expect($scope.dashboardOptions.hideToolbar).toEqual(true); 36 | expect($scope.dashboardOptions.widgetDefinitions.length).toEqual(2); 37 | expect($scope.dashboardOptions.widgetDefinitions[0].name).toEqual('peopleList'); 38 | expect($scope.dashboardOptions.widgetDefinitions[0].title).toEqual('people list'); 39 | expect($scope.dashboardOptions.widgetDefinitions[0].templateUrl).toEqual('app/template/dynamicOptionsContainer.html'); 40 | expect($scope.dashboardOptions.widgetDefinitions[0].size).toBeDefined(); 41 | expect($scope.dashboardOptions.widgetDefinitions[0].includeUrl).toEqual('app/template/peopleList.html'); 42 | expect($scope.dashboardOptions.widgetDefinitions[1].name).toEqual('peopleThumbnail'); 43 | expect($scope.dashboardOptions.widgetDefinitions[1].title).toEqual('people thumbnail'); 44 | expect($scope.dashboardOptions.widgetDefinitions[1].templateUrl).toEqual('app/template/dynamicOptionsContainer.html'); 45 | expect($scope.dashboardOptions.widgetDefinitions[1].size).toBeDefined(); 46 | expect($scope.dashboardOptions.widgetDefinitions[1].includeUrl).toEqual('app/template/peopleThumbnail.html'); 47 | expect($scope.dashboardOptions.defaultWidgets).toBeDefined(); 48 | expect($scope.dashboardOptions.storage).toBeDefined(); 49 | expect($scope.dashboardOptions.storageId.indexOf('demo_dynamic-options') === 0).toEqual(true); 50 | }); 51 | 52 | }); 53 | 54 | describe('the toggleWidget method', function() { 55 | 56 | it('should change style', function() { 57 | $scope.toggleWidget(); 58 | expect($scope.style).toEqual('peopleThumbnail'); 59 | 60 | $scope.toggleWidget(); 61 | expect($scope.style).toEqual('peopleList'); 62 | }); 63 | 64 | it('should change dashboardOptions reference', function() { 65 | // object for later comparison 66 | var savedDashboardOptions = $scope.dashboardOptions; 67 | $scope.toggleWidget(); 68 | injections.$timeout.flush(); 69 | expect(injections.$timeout).toHaveBeenCalled(); 70 | // we want to compare the object reference 71 | expect(savedDashboardOptions === $scope.dashboardOptions).toBeFalsy(); 72 | }); 73 | 74 | }); 75 | 76 | }); 77 | 78 | describe('Controller: PeopleCtrl', function() { 79 | 80 | var $scope; 81 | 82 | beforeEach(module('app')); 83 | 84 | beforeEach(inject(function($rootScope, $controller) { 85 | $scope = $rootScope.$new(); 86 | 87 | // let's mock widget 88 | $scope.widget = { 89 | includeUrl: 'app/template/peopleList.html' 90 | }; 91 | 92 | $controller('PeopleCtrl', { $scope: $scope }); 93 | })); 94 | 95 | describe('the controller properties', function() { 96 | 97 | it('should have people in scope', function() { 98 | expect($scope.people.length).toEqual(10); 99 | }); 100 | 101 | }); 102 | 103 | describe('the toggleTemplate method', function() { 104 | 105 | it('should change the includeUrl', function() { 106 | $scope.toggleTemplate(); 107 | expect($scope.widget.includeUrl).toEqual('app/template/peopleThumbnail.html'); 108 | 109 | $scope.toggleTemplate(); 110 | expect($scope.widget.includeUrl).toEqual('app/template/peopleList.html'); 111 | }); 112 | 113 | }); 114 | 115 | describe('theremovePerson method', function() { 116 | 117 | it('should remove one person', function() { 118 | $scope.removePerson($scope.people[2]); 119 | expect($scope.people.length).toEqual(9); 120 | }); 121 | 122 | it('should not remove anyone', function() { 123 | $scope.removePerson({ $$hashKey: 'xxx' }); 124 | expect($scope.people.length).toEqual(10); 125 | }); 126 | 127 | }); 128 | 129 | }); -------------------------------------------------------------------------------- /src/app/explicitSave.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 | .controller('ExplicitSaveDemoCtrl', function ($scope, $interval, $window, widgetDefinitions, defaultWidgets) { 21 | $scope.dashboardOptions = { 22 | widgetButtons: true, 23 | widgetDefinitions: widgetDefinitions, 24 | defaultWidgets: defaultWidgets, 25 | storage: $window.localStorage, 26 | storageId: 'explicitSave', 27 | explicitSave: true 28 | }; 29 | $scope.randomValue = Math.random(); 30 | $interval(function () { 31 | $scope.randomValue = Math.random(); 32 | }, 500); 33 | 34 | $scope.prependWidget = function() { 35 | $scope.dashboardOptions.prependWidget({ name: 'random', title: 'Prepend Widget'}); 36 | }; 37 | }); -------------------------------------------------------------------------------- /src/app/explicitSave.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: ExplicitSaveDemoCtrl', function() { 4 | 5 | var $scope, injections; 6 | 7 | beforeEach(module('app')); 8 | 9 | beforeEach(inject(function($rootScope, $controller, $window, $interval){ 10 | $scope = $rootScope.$new(); 11 | 12 | injections = { 13 | $scope: $scope, 14 | $window: $window, 15 | $interval: $interval 16 | }; 17 | $controller('ExplicitSaveDemoCtrl', injections); 18 | })); 19 | 20 | describe('the controller properties', function() { 21 | 22 | it('should have properties in scope', function() { 23 | expect($scope.randomValue).toBeDefined(); 24 | expect($scope.dashboardOptions.widgetButtons).toBe(true); 25 | expect($scope.dashboardOptions.widgetDefinitions).toBeDefined(); 26 | expect($scope.dashboardOptions.defaultWidgets).toBeDefined(); 27 | expect($scope.dashboardOptions.storage).toBeDefined(); 28 | expect($scope.dashboardOptions.storageId).toEqual('explicitSave'); 29 | expect($scope.dashboardOptions.explicitSave).toBe(true); 30 | }); 31 | 32 | it('should change randomValue', function() { 33 | var savedValue = $scope.randomValue; 34 | injections.$interval.flush(500); 35 | expect($scope.randomValue).not.toEqual(savedValue); 36 | }); 37 | 38 | }); 39 | 40 | }); -------------------------------------------------------------------------------- /src/app/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | angular.module('dashboard', ['ui.bootstrap']); 4 | -------------------------------------------------------------------------------- /src/app/index.less: -------------------------------------------------------------------------------- 1 | .browsehappy { 2 | margin: 0.2em 0; 3 | background: #ccc; 4 | color: #000; 5 | padding: 0.2em 0; 6 | } 7 | 8 | .thumbnail { 9 | height: 200px; 10 | 11 | img.pull-right { 12 | width: 50px; 13 | } 14 | } 15 | 16 | table.people { 17 | width: 100%; 18 | 19 | td { 20 | border: lightgray 1px solid; 21 | padding: 4px; 22 | } 23 | 24 | td:last-child { 25 | text-align: center; 26 | width: 60px; 27 | } 28 | } 29 | 30 | div.people { 31 | border: lightgray 1px solid; 32 | border-radius: 6px; 33 | display: inline-block; 34 | margin: 6px; 35 | padding: 4px 6px; 36 | width: 180px; 37 | } 38 | 39 | .empty { 40 | border: none; 41 | color: grey; 42 | text-align: left; 43 | } 44 | 45 | 46 | table.cart-detail { 47 | width: 100%; 48 | 49 | th:first-child, td:first-child { 50 | text-align: left; 51 | } 52 | 53 | th { 54 | text-align: right; 55 | } 56 | 57 | td { 58 | border: lightgray 1px solid; 59 | padding: 4px; 60 | text-align: right; 61 | } 62 | 63 | td:last-child { 64 | text-align: center; 65 | width: 50px; 66 | } 67 | 68 | td.empty { 69 | .empty 70 | } 71 | } 72 | 73 | table.cart-summary { 74 | td { 75 | padding: 4px; 76 | } 77 | td:first-child { 78 | padding-right: 8px; 79 | text-align: right; 80 | } 81 | 82 | td.empty { 83 | .empty 84 | } 85 | } 86 | 87 | // injector 88 | // endinjector -------------------------------------------------------------------------------- /src/app/layouts.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 | .controller('LayoutsDemoCtrl', function($scope, widgetDefinitions, defaultWidgets, LayoutStorage, $interval) { 21 | $scope.layoutOptions = { 22 | storageId: 'demo-layouts', 23 | storage: localStorage, 24 | storageHash: 'fs4df4d51', 25 | widgetDefinitions: widgetDefinitions, 26 | defaultWidgets: defaultWidgets, 27 | lockDefaultLayouts: true, 28 | defaultLayouts: [ 29 | { title: 'Layout 1', active: true , defaultWidgets: defaultWidgets }, 30 | { title: 'Layout 2', active: false, defaultWidgets: defaultWidgets }, 31 | { title: 'Layout 3', active: false, defaultWidgets: defaultWidgets, locked: false } 32 | ] 33 | }; 34 | $scope.randomValue = Math.random(); 35 | $interval(function () { 36 | $scope.randomValue = Math.random(); 37 | }, 500); 38 | 39 | $scope.prependWidget = function() { 40 | $scope.layoutOptions.prependWidget({name: 'random', title: 'Prepend Widget'}); 41 | }; 42 | }) 43 | .controller('LayoutsDemoExplicitSaveCtrl', function($scope, widgetDefinitions, defaultWidgets, LayoutStorage, $interval) { 44 | $scope.layoutOptions = { 45 | storageId: 'demo-layouts-explicit-save', 46 | storage: localStorage, 47 | storageHash: 'fs4df4d51', 48 | widgetDefinitions: widgetDefinitions, 49 | defaultWidgets: defaultWidgets, 50 | explicitSave: true, 51 | defaultLayouts: [ 52 | { title: 'Layout 1', active: true , defaultWidgets: defaultWidgets }, 53 | { title: 'Layout 2', active: false, defaultWidgets: defaultWidgets }, 54 | { title: 'Layout 3', active: false, defaultWidgets: defaultWidgets } 55 | ] 56 | }; 57 | $scope.randomValue = Math.random(); 58 | $interval(function () { 59 | $scope.randomValue = Math.random(); 60 | }, 500); 61 | 62 | $scope.prependWidget = function() { 63 | $scope.layoutOptions.prependWidget({name: 'random', title: 'Prepend Widget'}); 64 | }; 65 | }); -------------------------------------------------------------------------------- /src/app/layouts.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: LayoutsDemoCtrl', function() { 4 | 5 | var $scope, injections; 6 | 7 | beforeEach(module('app')); 8 | 9 | beforeEach(inject(function($rootScope, $controller, $window, $interval){ 10 | $scope = $rootScope.$new(); 11 | 12 | injections = { 13 | $scope: $scope, 14 | $interval: $interval 15 | }; 16 | $controller('LayoutsDemoCtrl', injections); 17 | })); 18 | 19 | describe('the controller properties', function() { 20 | 21 | it('should have properties in scope', function() { 22 | expect($scope.randomValue).toBeDefined(); 23 | expect($scope.layoutOptions.storageId).toEqual('demo-layouts'); 24 | expect($scope.layoutOptions.storage).toBeDefined(); 25 | expect($scope.layoutOptions.storageHash).toEqual('fs4df4d51'); 26 | expect($scope.layoutOptions.widgetDefinitions).toBeDefined(); 27 | expect($scope.layoutOptions.defaultWidgets).toBeDefined(); 28 | expect($scope.layoutOptions.lockDefaultLayouts).toBe(true); 29 | expect($scope.layoutOptions.defaultLayouts.length).toEqual(3); 30 | expect($scope.layoutOptions.defaultLayouts[0].title).toEqual('Layout 1'); 31 | expect($scope.layoutOptions.defaultLayouts[0].active).toBe(true); 32 | expect($scope.layoutOptions.defaultLayouts[0].defaultWidgets).toBeDefined(); 33 | expect($scope.layoutOptions.defaultLayouts[1].title).toEqual('Layout 2'); 34 | expect($scope.layoutOptions.defaultLayouts[1].active).toBe(false); 35 | expect($scope.layoutOptions.defaultLayouts[1].defaultWidgets).toBeDefined(); 36 | expect($scope.layoutOptions.defaultLayouts[2].title).toEqual('Layout 3'); 37 | expect($scope.layoutOptions.defaultLayouts[2].active).toBe(false); 38 | expect($scope.layoutOptions.defaultLayouts[2].defaultWidgets).toBeDefined(); 39 | expect($scope.layoutOptions.defaultLayouts[2].locked).toBe(false); 40 | }); 41 | 42 | it('should change randomValue', function() { 43 | var savedValue = $scope.randomValue; 44 | injections.$interval.flush(500); 45 | expect($scope.randomValue).not.toEqual(savedValue); 46 | }); 47 | 48 | }); 49 | 50 | }); 51 | 52 | describe('Controller: LayoutsDemoExplicitSaveCtrl', function() { 53 | 54 | var $scope, $interval; 55 | 56 | beforeEach(module('app')); 57 | 58 | beforeEach(inject(function($rootScope, $controller, _$interval_) { 59 | $interval = _$interval_; 60 | 61 | $scope = $rootScope.$new(); 62 | 63 | $controller('LayoutsDemoExplicitSaveCtrl', { 64 | $scope: $scope, 65 | $interval: $interval 66 | }); 67 | })); 68 | 69 | describe('the controller properties', function() { 70 | 71 | it('should have properties in scope', function() { 72 | expect($scope.randomValue).toBeDefined(); 73 | expect($scope.layoutOptions.storageId).toEqual('demo-layouts-explicit-save'); 74 | expect($scope.layoutOptions.storage).toBeDefined(); 75 | expect($scope.layoutOptions.storageHash).toEqual('fs4df4d51'); 76 | expect($scope.layoutOptions.widgetDefinitions).toBeDefined(); 77 | expect($scope.layoutOptions.defaultWidgets).toBeDefined(); 78 | expect($scope.layoutOptions.explicitSave).toBe(true); 79 | expect($scope.layoutOptions.defaultLayouts.length).toEqual(3); 80 | expect($scope.layoutOptions.defaultLayouts[0].title).toEqual('Layout 1'); 81 | expect($scope.layoutOptions.defaultLayouts[0].active).toBe(true); 82 | expect($scope.layoutOptions.defaultLayouts[0].defaultWidgets).toBeDefined(); 83 | expect($scope.layoutOptions.defaultLayouts[1].title).toEqual('Layout 2'); 84 | expect($scope.layoutOptions.defaultLayouts[1].active).toBe(false); 85 | expect($scope.layoutOptions.defaultLayouts[1].defaultWidgets).toBeDefined(); 86 | expect($scope.layoutOptions.defaultLayouts[2].title).toEqual('Layout 3'); 87 | expect($scope.layoutOptions.defaultLayouts[2].active).toBe(false); 88 | expect($scope.layoutOptions.defaultLayouts[2].defaultWidgets).toBeDefined(); 89 | }); 90 | 91 | it('should change randomValue', function() { 92 | var savedValue = $scope.randomValue; 93 | $interval.flush(500); 94 | expect($scope.randomValue).not.toEqual(savedValue); 95 | }); 96 | 97 | }); 98 | 99 | }); -------------------------------------------------------------------------------- /src/app/resize.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 | .controller('ResizeDemoCtrl', function ($scope, $interval, $window, widgetDefinitions, defaultWidgets) { 21 | defaultWidgets = [ 22 | { name: 'fluid', resizeTimeout: 0 }, 23 | { name: 'resizable', resizeTimeout: 0 }, 24 | { name: 'random', style: { width: '50%' }, resizeTimeout: 0 }, 25 | { name: 'time', style: { width: '50%' }, resizeTimeout: 0 }, 26 | { name: 'resizable', title: 'resizable (width: 50%, minWidth: 40%)', size: { width: '50%', minWidth: '40%' }, resizeTimeout: 0 }, 27 | { name: 'resizable', title: 'resizable (width: 50%, minWidth: 900px)', size: { width: '50%', minWidth: '900px' }, resizeTimeout: 0 }, 28 | { name: 'resizable', title: 'resizable (width: 500px, minWidth: 70%)', size: { width: '500px', minWidth: '70%' }, resizeTimeout: 0 }, 29 | { name: 'resizable', title: 'resizable (width: 500px, minWidth: 400px, minHeight: 100px)', size: { width: '200px', height: '50px', minWidth: '400px', minHeight: '100px' }, resizeTimeout: 0 }, 30 | { name: 'resizable', title: 'resizable (height = 25% of width)', size: { width: '50%', height: '50px', minWidth: '400px', minHeight: '100px', heightToWidthRatio: .25 }, resizeTimeout: 0 } 31 | ]; 32 | 33 | $scope.dashboardOptions = { 34 | widgetButtons: true, 35 | widgetDefinitions: widgetDefinitions, 36 | defaultWidgets: defaultWidgets, 37 | storage: $window.localStorage, 38 | storageId: 'demo_resize' 39 | }; 40 | $scope.randomValue = Math.random(); 41 | $interval(function () { 42 | $scope.randomValue = Math.random(); 43 | }, 500); 44 | 45 | $scope.prependWidget = function() { 46 | $scope.dashboardOptions.prependWidget({ name: 'random', title: 'Prepend Widget'}); 47 | }; 48 | }) 49 | .controller('ResizableCtrl', function ($scope) { 50 | $scope.$on('widgetResized', function (event, size) { 51 | $scope.width = size.width || $scope.width; 52 | $scope.height = size.height || $scope.height; 53 | }); 54 | }); -------------------------------------------------------------------------------- /src/app/resize.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 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 | 18 | 'use strict'; 19 | 20 | describe('Controller: ResizeDemoCtrl', function () { 21 | var scope, widgetDefinitions, interval; 22 | 23 | // load the directive's module 24 | beforeEach(module('app')); 25 | 26 | // Mock package 27 | beforeEach(inject(function ($compile, $rootScope, $controller, $interval) { 28 | // Set up the controller scope 29 | scope = $rootScope.$new(); 30 | interval = $interval; 31 | 32 | widgetDefinitions = [{ 33 | name: 'random', 34 | directive: 'wt-scope-watch', 35 | attrs: { 36 | value: 'randomValue' 37 | } 38 | }]; 39 | 40 | $controller('ResizeDemoCtrl', { 41 | $scope: scope, 42 | widgetDefinitions: widgetDefinitions, 43 | defaultWidgets: ['random'] 44 | }); 45 | 46 | // Define and compile the element 47 | var element = angular.element('
'); 48 | $compile(element)(scope); 49 | scope.$digest(); 50 | })); 51 | 52 | it('should have dashboardOptions object defined', function() { 53 | expect(scope.dashboardOptions.widgetButtons).toEqual(true); 54 | expect(scope.dashboardOptions.widgetDefinitions).toEqual(widgetDefinitions); 55 | expect(scope.dashboardOptions.defaultWidgets.length).toEqual(9); 56 | expect(scope.dashboardOptions.storage).toBeDefined(); 57 | expect(scope.dashboardOptions.storageId).toEqual('demo_resize'); 58 | 59 | var randomValue = scope.randomValue; 60 | interval.flush(500); 61 | expect(scope.randomValue).not.toEqual(randomValue); 62 | }); 63 | }); 64 | 65 | 66 | describe('Controller: ResizableCtrl', function () { 67 | var scope; 68 | 69 | // load the directive's module 70 | beforeEach(module('app')); 71 | 72 | // Mock package 73 | beforeEach(inject(function ($compile, $rootScope, $controller) { 74 | // Set up the controller scope 75 | scope = $rootScope.$new(); 76 | 77 | scope.width = 50; 78 | scope.height = 60; 79 | 80 | $controller('ResizableCtrl', { 81 | $scope: scope 82 | }); 83 | 84 | // Define and compile the element 85 | var element = angular.element('
'); 86 | $compile(element)(scope); 87 | scope.$digest(); 88 | })); 89 | 90 | it('should use defined with and height', function() { 91 | scope.$broadcast('widgetResized', { }); 92 | expect(scope.width).toEqual(50); 93 | expect(scope.height).toEqual(60); 94 | }); 95 | 96 | it('should set width and height to values in event', function() { 97 | scope.$broadcast('widgetResized', { width: 200, height: 300 }); 98 | expect(scope.width).toEqual(200); 99 | expect(scope.height).toEqual(300); 100 | }); 101 | }); -------------------------------------------------------------------------------- /src/app/template/cartDetail.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
NameQtyUnitTotal
{{::item.name}}{{item.qty}}{{item.price | currency}}{{item.total | currency}}
The cart is empty
-------------------------------------------------------------------------------- /src/app/template/cartSummary.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
Cart Total:{{cart.total | currency}}
Total Qty:{{cart.qty}}
Most Expensive:{{cart.expItem.qty}} {{cart.expItem.name}} @{{cart.expItem.price | currency}}
Cheapest:{{cart.cheapItem.qty}} {{cart.cheapItem.name}} @{{cart.cheapItem.price | currency}}
The cart is empty
24 | -------------------------------------------------------------------------------- /src/app/template/configurableWidgetModalOptions.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 |
-------------------------------------------------------------------------------- /src/app/template/customSettingsTemplate.html: -------------------------------------------------------------------------------- 1 | 5 | 16 | -------------------------------------------------------------------------------- /src/app/template/dynamicData.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
Add item to cart:
5 | 6 | 7 | 8 | 9 | 10 |
11 |
12 |
13 |
-------------------------------------------------------------------------------- /src/app/template/dynamicOptions.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | Change widget to: 4 |
5 | 6 | 7 |
8 |
9 | This methodology will destroy the existing widget and creates a new widget with new scope and data. 10 | Notice the names change as you toggle between the List and Thumbnail buttons. 11 |
12 |
13 |
14 |
-------------------------------------------------------------------------------- /src/app/template/dynamicOptionsContainer.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | Change includeUrl to: 4 |
5 | 6 | 7 |
8 |
9 | This methodology does not create a new widget and therefore does not create a new scope and the existing data remains unchanged. 10 | Notice the names do not change as you toggle between the List and Thumbnail buttons. 11 |
12 |
13 |
14 |
-------------------------------------------------------------------------------- /src/app/template/fluid.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Widget takes 100% height (blue border).

4 |

Resize the widget vertically to see that this text (red border) stays middle aligned.

5 |

New width: {{width}}

6 |

New height: {{height}}

7 |
8 |
-------------------------------------------------------------------------------- /src/app/template/layouts.html: -------------------------------------------------------------------------------- 1 |

2 | Click here to add new "random" widget to beginning of dashboard. This demonstrates the prependWidget function. See issue #141. 3 |

4 |
-------------------------------------------------------------------------------- /src/app/template/peopleList.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
{{::person.name}}{{::person.email}}{{::person.phone}}
-------------------------------------------------------------------------------- /src/app/template/peopleThumbnail.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |

{{::person.name}}
{{::person.email}}
{{::person.phone}}

4 |
5 | -------------------------------------------------------------------------------- /src/app/template/resizable.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
New width: {{width}}
4 |
New height: {{height}}
5 |
6 |
-------------------------------------------------------------------------------- /src/app/template/view.html: -------------------------------------------------------------------------------- 1 |

2 | Click here to add new widget to beginning of dashboard. This demonstrates the prependWidget function. See issue #141. 3 |

4 | 5 |
6 |
7 |
8 |
9 |
-------------------------------------------------------------------------------- /src/app/template/widgetSpecificSettings.html: -------------------------------------------------------------------------------- 1 | 5 | 16 | -------------------------------------------------------------------------------- /src/app/vendor.less: -------------------------------------------------------------------------------- 1 | @import '../../bower_components/bootstrap/less/bootstrap.less'; 2 | 3 | @icon-font-path: '/fonts/'; 4 | -------------------------------------------------------------------------------- /src/components/directives/dashboard/WidgetSettingsCtrl.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.dashboard') 20 | .controller('WidgetSettingsCtrl', ['$scope', '$uibModalInstance', 'widget', function ($scope, $uibModalInstance, widget) { 21 | // add widget to scope 22 | $scope.widget = widget; 23 | 24 | // set up result object 25 | $scope.result = jQuery.extend(true, {}, widget); 26 | 27 | $scope.ok = function () { 28 | $uibModalInstance.close($scope.result); 29 | }; 30 | 31 | $scope.cancel = function () { 32 | $uibModalInstance.dismiss('cancel'); 33 | }; 34 | }]); -------------------------------------------------------------------------------- /src/components/directives/dashboard/WidgetSettingsCtrl.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: WidgetSettingsCtrl', function() { 4 | 5 | var $scope, $uibModalInstance, widget; 6 | 7 | beforeEach(module('ui.dashboard')); 8 | 9 | beforeEach(inject(function($rootScope, $controller){ 10 | $scope = $rootScope.$new(); 11 | 12 | // let's mock $uibModalInstance 13 | $uibModalInstance = { 14 | close: function() {}, 15 | dismiss: function() {} 16 | }; 17 | spyOn($uibModalInstance, 'close'); 18 | spyOn($uibModalInstance, 'dismiss'); 19 | 20 | // let's mock a widget 21 | widget = { 22 | name: 'my-widget', 23 | title: 'My mock widget' 24 | }; 25 | 26 | $controller('WidgetSettingsCtrl', { 27 | $scope: $scope, 28 | $uibModalInstance: $uibModalInstance, 29 | widget: widget 30 | }); 31 | })); 32 | 33 | describe('the controller properties', function() { 34 | 35 | it('should have result in scope', function() { 36 | expect($scope.result.name).toEqual('my-widget'); 37 | expect($scope.result.title).toEqual('My mock widget'); 38 | }); 39 | 40 | it('should call the close function', function() { 41 | $scope.ok(); 42 | expect($uibModalInstance.close).toHaveBeenCalled(); 43 | }) 44 | 45 | it('should call the dismiss function', function() { 46 | $scope.cancel(); 47 | expect($uibModalInstance.dismiss).toHaveBeenCalled(); 48 | }) 49 | 50 | }); 51 | 52 | }); -------------------------------------------------------------------------------- /src/components/directives/dashboard/altDashboard.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 8 | 13 | 14 |
15 | 16 |
17 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 |
31 |
32 |
33 |
34 |

35 | {{widget.title}} 36 |
37 | 38 |
39 | {{widget.name}} 40 | 41 | 42 |

43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | -------------------------------------------------------------------------------- /src/components/directives/dashboard/dashboard.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 8 | 13 | 14 |
15 |
16 | 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 |
30 |
31 |
32 |
33 |

34 | {{widget.title}} 35 |
36 | 37 |
38 | {{widget.name}} 39 |

40 |
41 | 42 | 43 | 44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
-------------------------------------------------------------------------------- /src/components/directives/dashboard/dashboard.less: -------------------------------------------------------------------------------- 1 | .dashboard-widget-area { 2 | margin: 10px 0 30px; 3 | min-height: 200px; 4 | } 5 | 6 | .widget-container { 7 | float:left; 8 | display: inline-block; 9 | width: 33%; 10 | padding-bottom: 1em; 11 | } 12 | 13 | .widget { 14 | margin: 0 1em 0 0; 15 | background-color: white; 16 | border: 2px solid #444; 17 | border-radius: 5px; 18 | position: relative; 19 | height: 100%; 20 | } 21 | .widget-header { 22 | display: flex; 23 | overflow: hidden; 24 | 25 | .panel-title { 26 | flex: 1 1 auto; 27 | overflow: hidden; 28 | padding-right: 4px; 29 | text-overflow: ellipsis; 30 | white-space: nowrap; 31 | } 32 | 33 | .buttons { 34 | flex: 0 0 auto; 35 | } 36 | } 37 | .widget-header .label { 38 | display: inline-block; 39 | vertical-align: middle; 40 | } 41 | .widget-header .glyphicon { 42 | cursor: pointer; 43 | float: right; 44 | opacity: 0.5; 45 | margin-left: 5px; 46 | } 47 | .widget-header .glyphicon:hover { 48 | opacity: 1; 49 | } 50 | .widget-header .widget-title { 51 | vertical-align: middle; 52 | } 53 | .widget-header form.widget-title { 54 | display: inline; 55 | } 56 | 57 | .widget-header form.widget-title input.form-control { 58 | width: auto; 59 | display: inline-block; 60 | } 61 | 62 | .widget-content { 63 | overflow: hidden; 64 | } 65 | 66 | .widget .widget-w-resizer { 67 | background-color: transparent; 68 | display: flex; 69 | flex-direction: column; 70 | height:100%; 71 | position: absolute; 72 | left: -2px; 73 | top: 0px; 74 | width: 5px; 75 | 76 | .nw-resizer { 77 | background-color: transparent; 78 | cursor: nwse-resize; 79 | flex: 0 0 auto; 80 | height: 15px; 81 | } 82 | 83 | 84 | .w-resizer { 85 | background-color: transparent; 86 | cursor: ew-resize; 87 | flex: 1 1 auto; 88 | } 89 | 90 | .sw-resizer { 91 | background-color: transparent; 92 | cursor: nesw-resize; 93 | flex: 0 0 auto; 94 | height: 15px; 95 | } 96 | } 97 | 98 | .widget .widget-e-resizer { 99 | background-color: transparent; 100 | display: flex; 101 | flex-direction: column; 102 | height:100%; 103 | position: absolute; 104 | right: -2px; 105 | top: 0px; 106 | width: 5px; 107 | 108 | .ne-resizer { 109 | background-color: transparent; 110 | cursor: nesw-resize; 111 | flex: 0 0 auto; 112 | height: 15px; 113 | } 114 | 115 | 116 | .e-resizer { 117 | background-color: transparent; 118 | cursor: ew-resize; 119 | flex: 1 1 auto; 120 | } 121 | 122 | .se-resizer { 123 | background-color: transparent; 124 | cursor: nwse-resize; 125 | flex: 0 0 auto; 126 | height: 15px; 127 | } 128 | } 129 | 130 | .widget .widget-n-resizer { 131 | background-color: transparent; 132 | display: flex; 133 | height: 5px; 134 | width: 100%; 135 | left: 0; 136 | position: absolute; 137 | top: -2px; 138 | 139 | .nw-resizer { 140 | background-color: transparent; 141 | cursor: nwse-resize; 142 | flex: 0 0 auto; 143 | width: 18px; 144 | } 145 | 146 | .n-resizer { 147 | background-color: transparent; 148 | cursor: ns-resize; 149 | flex: 1 1 auto; 150 | } 151 | 152 | .ne-resizer { 153 | background-color: transparent; 154 | cursor: nesw-resize; 155 | flex: 0 0 auto; 156 | width: 18px; 157 | } 158 | } 159 | 160 | .widget .widget-s-resizer { 161 | background-color: transparent; 162 | bottom: -2px; 163 | display: flex; 164 | height: 5px; 165 | position: absolute; 166 | width: 100%; 167 | left: 0; 168 | 169 | .sw-resizer { 170 | background-color: transparent; 171 | cursor: nesw-resize; 172 | flex: 0 0 auto; 173 | width: 18px; 174 | } 175 | 176 | .s-resizer { 177 | background-color: transparent; 178 | cursor: ns-resize; 179 | flex: 1 1 auto; 180 | } 181 | 182 | .se-resizer { 183 | background-color: transparent; 184 | cursor: nwse-resize; 185 | flex: 0 0 auto; 186 | width: 18px; 187 | } 188 | } 189 | 190 | 191 | .widget .widget-resizer-marquee { 192 | xborder: 2px dashed #09305f; 193 | border: 2px dotted #79a0e0; 194 | position: absolute; 195 | top: -2px; 196 | left: -2px; 197 | z-index: 999999; 198 | } 199 | 200 | .widget .widget-resizer-marquee.n, .widget .widget-resizer-marquee.s { 201 | cursor: ns-resize; 202 | } 203 | 204 | .widget .widget-resizer-marquee.w, .widget .widget-resizer-marquee.e { 205 | cursor: ew-resize; 206 | } 207 | 208 | .widget .widget-resizer-marquee.nw, .widget .widget-resizer-marquee.se { 209 | cursor: nwse-resize; 210 | } 211 | 212 | .widget .widget-resizer-marquee.sw, .widget .widget-resizer-marquee.ne { 213 | cursor: nesw-resize; 214 | } 215 | 216 | .remove-layout-icon { 217 | vertical-align: text-top; 218 | cursor: pointer; 219 | opacity: 0.3; 220 | } 221 | .remove-layout-icon:hover { 222 | opacity: 1; 223 | } 224 | .layout-title { 225 | display: inline-block; 226 | } -------------------------------------------------------------------------------- /src/components/directives/dashboard/widget-settings-template.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 18 | 19 | -------------------------------------------------------------------------------- /src/components/directives/dashboardLayouts/SaveChangesModal.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /src/components/directives/dashboardLayouts/SaveChangesModalCtrl.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.dashboard') 20 | .controller('SaveChangesModalCtrl', ['$scope', '$uibModalInstance', 'layout', function ($scope, $uibModalInstance, layout) { 21 | 22 | // add layout to scope 23 | $scope.layout = layout; 24 | 25 | $scope.ok = function () { 26 | $uibModalInstance.close(); 27 | }; 28 | 29 | $scope.cancel = function () { 30 | $uibModalInstance.dismiss(); 31 | }; 32 | }]); -------------------------------------------------------------------------------- /src/components/directives/dashboardLayouts/SaveChangesModalCtrl.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: SaveChangesModalCtrl', function() { 4 | 5 | var $scope, $uibModalInstance, layout; 6 | 7 | beforeEach(module('ui.dashboard')); 8 | 9 | beforeEach(inject(function($rootScope, $controller){ 10 | $scope = $rootScope.$new(); 11 | 12 | // let's mock $uibModalInstance 13 | $uibModalInstance = { 14 | close: function() {}, 15 | dismiss: function() {} 16 | }; 17 | spyOn($uibModalInstance, 'close'); 18 | spyOn($uibModalInstance, 'dismiss'); 19 | 20 | // let's mock a layout 21 | layout = { 22 | name: 'my-layout', 23 | title: 'My mock layout' 24 | }; 25 | 26 | $controller('SaveChangesModalCtrl', { 27 | $scope: $scope, 28 | $uibModalInstance: $uibModalInstance, 29 | layout: layout 30 | }); 31 | })); 32 | 33 | describe('the controller properties', function() { 34 | 35 | it('should have layout in scope', function() { 36 | expect($scope.layout.name).toEqual('my-layout'); 37 | expect($scope.layout.title).toEqual('My mock layout'); 38 | }); 39 | 40 | it('should call the close function', function() { 41 | $scope.ok(); 42 | expect($uibModalInstance.close).toHaveBeenCalled(); 43 | }) 44 | 45 | it('should call the dismiss function', function() { 46 | $scope.cancel(); 47 | expect($uibModalInstance.dismiss).toHaveBeenCalled(); 48 | }) 49 | 50 | }); 51 | 52 | }); -------------------------------------------------------------------------------- /src/components/directives/dashboardLayouts/dashboardLayouts.html: -------------------------------------------------------------------------------- 1 | 19 |
-------------------------------------------------------------------------------- /src/components/directives/dashboardLayouts/dashboardLayouts.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.dashboard') 20 | .directive('dashboardLayouts', ['LayoutStorage', '$timeout', '$uibModal', 21 | function(LayoutStorage, $timeout, $uibModal) { 22 | return { 23 | scope: true, 24 | templateUrl: function(element, attr) { 25 | return attr.templateUrl ? attr.templateUrl : 'components/directives/dashboardLayouts/dashboardLayouts.html'; 26 | }, 27 | link: function(scope, element, attrs) { 28 | 29 | scope.options = scope.$eval(attrs.dashboardLayouts); 30 | 31 | var layoutStorage = new LayoutStorage(scope.options); 32 | 33 | scope.layouts = layoutStorage.layouts; 34 | 35 | scope.createNewLayout = function() { 36 | var newLayout = { 37 | title: 'Custom', 38 | defaultWidgets: scope.options.defaultWidgets || [] 39 | }; 40 | layoutStorage.add(newLayout); 41 | scope.makeLayoutActive(newLayout); 42 | layoutStorage.save(); 43 | return newLayout; 44 | }; 45 | 46 | scope.removeLayout = function(layout) { 47 | layoutStorage.remove(layout); 48 | layoutStorage.save(); 49 | }; 50 | 51 | scope.makeLayoutActive = function(layout) { 52 | 53 | var current = layoutStorage.getActiveLayout(); 54 | 55 | if (current && current.dashboard.unsavedChangeCount) { 56 | var modalInstance = $uibModal.open({ 57 | templateUrl: 'template/SaveChangesModal.html', 58 | resolve: { 59 | layout: function() { 60 | return layout; 61 | } 62 | }, 63 | controller: 'SaveChangesModalCtrl' 64 | }); 65 | 66 | // Set resolve and reject callbacks for the result promise 67 | modalInstance.result.then( 68 | function() { 69 | current.dashboard.saveDashboard(); 70 | scope._makeLayoutActive(layout); 71 | }, 72 | function() { 73 | scope._makeLayoutActive(layout); 74 | } 75 | ); 76 | } else { 77 | scope._makeLayoutActive(layout); 78 | } 79 | 80 | }; 81 | 82 | scope._makeLayoutActive = function(layout) { 83 | angular.forEach(scope.layouts, function(l) { 84 | if (l !== layout) { 85 | l.active = false; 86 | } else { 87 | l.active = true; 88 | } 89 | }); 90 | layoutStorage.save(); 91 | }; 92 | 93 | scope.isActive = function(layout) { 94 | return !!layout.active; 95 | }; 96 | 97 | scope.editTitle = function(layout) { 98 | if (layout.locked) { 99 | return; 100 | } 101 | 102 | var input = element.find('input[data-layout="' + layout.id + '"]'); 103 | layout.editingTitle = true; 104 | 105 | $timeout(function() { 106 | input.focus()[0].setSelectionRange(0, 9999); 107 | }); 108 | }; 109 | 110 | // saves whatever is in the title input as the new title 111 | scope.saveTitleEdit = function(layout, event) { 112 | layout.editingTitle = false; 113 | layoutStorage.save(); 114 | 115 | // When a browser is open and the user clicks on the tab title to change it, 116 | // upon pressing the Enter key, the page refreshes. 117 | // This statement prevents that. 118 | var evt = event || window.event; 119 | if (evt) { 120 | evt.preventDefault(); 121 | } 122 | }; 123 | 124 | scope.titleLostFocus = function(layout, event) { 125 | // user clicked some where; now we lost focus to the input box 126 | // lets see if we need to save the title 127 | if (layout && layout.editingTitle) { 128 | if (layout.title !== '') { 129 | scope.saveTitleEdit(layout, event); 130 | } else { 131 | // can't save blank title 132 | var input = element.find('input[data-layout="' + layout.id + '"]'); 133 | $timeout(function() { 134 | input.focus(); 135 | }); 136 | } 137 | } 138 | }; 139 | 140 | scope.options.saveLayouts = function() { 141 | layoutStorage.save(true); 142 | }; 143 | scope.options.addWidget = function() { 144 | var layout = layoutStorage.getActiveLayout(); 145 | if (layout) { 146 | layout.dashboard.addWidget.apply(layout.dashboard, arguments); 147 | } 148 | }; 149 | scope.options.prependWidget = function() { 150 | var layout = layoutStorage.getActiveLayout(); 151 | if (layout) { 152 | layout.dashboard.prependWidget.apply(layout.dashboard, arguments); 153 | } 154 | }; 155 | scope.options.loadWidgets = function() { 156 | var layout = layoutStorage.getActiveLayout(); 157 | if (layout) { 158 | layout.dashboard.loadWidgets.apply(layout.dashboard, arguments); 159 | } 160 | }; 161 | scope.options.saveDashboard = function() { 162 | var layout = layoutStorage.getActiveLayout(); 163 | if (layout) { 164 | layout.dashboard.saveDashboard.apply(layout.dashboard, arguments); 165 | } 166 | }; 167 | 168 | var sortableDefaults = { 169 | stop: function() { 170 | scope.options.saveLayouts(); 171 | }, 172 | distance: 5 173 | }; 174 | scope.sortableOptions = angular.extend({}, sortableDefaults, scope.options.sortableOptions || {}); 175 | } 176 | }; 177 | } 178 | ]); -------------------------------------------------------------------------------- /src/components/directives/widget/widget.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.dashboard') 20 | .directive('widget', ['$injector', function ($injector) { 21 | 22 | return { 23 | 24 | controller: 'DashboardWidgetCtrl', 25 | 26 | link: function (scope) { 27 | 28 | var widget = scope.widget; 29 | var dataModelType = widget.dataModelType; 30 | 31 | // set up data source 32 | if (dataModelType) { 33 | var DataModelConstructor; // data model constructor function 34 | 35 | if (angular.isFunction(dataModelType)) { 36 | DataModelConstructor = dataModelType; 37 | } else if (angular.isString(dataModelType)) { 38 | $injector.invoke([dataModelType, function (DataModelType) { 39 | DataModelConstructor = DataModelType; 40 | }]); 41 | } else { 42 | throw new Error('widget dataModelType should be function or string'); 43 | } 44 | 45 | var ds; 46 | if (widget.dataModelArgs) { 47 | ds = new DataModelConstructor(widget.dataModelArgs); 48 | } else { 49 | ds = new DataModelConstructor(); 50 | } 51 | widget.dataModel = ds; 52 | ds.setup(widget, scope); 53 | ds.init(); 54 | scope.$on('$destroy', _.bind(ds.destroy,ds)); 55 | } 56 | 57 | // Compile the widget template, emit add event 58 | scope.compileTemplate(); 59 | scope.$emit('widgetAdded', widget); 60 | 61 | } 62 | 63 | }; 64 | }]); 65 | -------------------------------------------------------------------------------- /src/components/directives/widget/widget.spec.js: -------------------------------------------------------------------------------- 1 | // 'use strict'; 2 | 3 | describe('Directive: widget', function () { 4 | 5 | var element, scope, rootScope, isoScope, compile, provide; 6 | 7 | function Type(args) { 8 | this.args = args; 9 | } 10 | 11 | Type.prototype = { 12 | setup: function () { 13 | }, 14 | init: function () { 15 | }, 16 | destroy: function () { 17 | } 18 | }; 19 | 20 | beforeEach(function () { 21 | spyOn(Type.prototype, 'setup'); 22 | spyOn(Type.prototype, 'init'); 23 | spyOn(Type.prototype, 'destroy'); 24 | // define mock objects here 25 | }); 26 | 27 | // load the directive's module 28 | beforeEach(module('ui.dashboard', function ($provide, $controllerProvider) { 29 | provide = $provide; 30 | // Inject dependencies like this: 31 | $controllerProvider.register('DashboardWidgetCtrl', function ($scope) { 32 | 33 | }); 34 | 35 | })); 36 | 37 | beforeEach(inject(function ($compile, $rootScope) { 38 | // Cache these for reuse 39 | rootScope = $rootScope; 40 | compile = $compile; 41 | 42 | // Other setup, e.g. helper functions, etc. 43 | 44 | // Set up the outer scope 45 | scope = $rootScope.$new(); 46 | scope.widget = { 47 | dataModelType: Type 48 | }; 49 | 50 | compileTemplate = jasmine.createSpy('compileTemplate'); 51 | scope.compileTemplate = compileTemplate; 52 | })); 53 | 54 | function compileWidget() { 55 | // Define and compile the element 56 | element = angular.element('
'); 57 | element = compile(element)(scope); 58 | scope.$digest(); 59 | isoScope = element.isolateScope(); 60 | } 61 | 62 | it('should create a new instance of dataModelType if provided in scope.widget', function () { 63 | compileWidget(); 64 | expect(scope.widget.dataModel instanceof Type).toBe(true); 65 | }); 66 | 67 | it('should call setup and init on the new dataModel', function () { 68 | compileWidget(); 69 | expect(Type.prototype.setup).toHaveBeenCalled(); 70 | expect(Type.prototype.init).toHaveBeenCalled(); 71 | }); 72 | 73 | it('should call compile template', function () { 74 | compileWidget(); 75 | expect(scope.compileTemplate).toHaveBeenCalled(); 76 | }); 77 | 78 | it('should create a new instance of dataModelType from string name', function () { 79 | // register data model with $injector 80 | provide.factory('StringNameDataModel', function () { 81 | return Type; 82 | }); 83 | 84 | scope.widget = { 85 | dataModelType: 'StringNameDataModel' 86 | }; 87 | 88 | compileWidget(); 89 | 90 | expect(scope.widget.dataModel instanceof Type).toBe(true); 91 | expect(Type.prototype.setup).toHaveBeenCalled(); 92 | expect(Type.prototype.init).toHaveBeenCalled(); 93 | }); 94 | 95 | it('should validate data model type', function () { 96 | scope.widget = { 97 | dataModelType: {} 98 | }; 99 | 100 | expect(function () { 101 | compileWidget() 102 | }).toThrowError(); 103 | }); 104 | 105 | it('should use dataModelArgs', function() { 106 | scope.widget.dataModelArgs = 'test data model arg'; 107 | compileWidget(); 108 | expect(scope.widget.dataModel.args).toEqual('test data model arg'); 109 | }); 110 | 111 | }); -------------------------------------------------------------------------------- /src/components/models/DashboardState.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.dashboard') 20 | .factory('DashboardState', ['$log', '$q', function ($log, $q) { 21 | function DashboardState(storage, id, hash, widgetDefinitions, stringify) { 22 | this.storage = storage; 23 | this.id = id; 24 | this.hash = hash; 25 | this.widgetDefinitions = widgetDefinitions; 26 | this.stringify = stringify; 27 | } 28 | 29 | DashboardState.prototype = { 30 | /** 31 | * Takes array of widget instance objects, serializes, 32 | * and saves state. 33 | * 34 | * @param {Array} widgets scope.widgets from dashboard directive 35 | * @return {Boolean} true on success, false on failure 36 | */ 37 | save: function (widgets) { 38 | 39 | if (!this.storage) { 40 | return true; 41 | } 42 | 43 | var serialized = _.map(widgets, function (widget) { 44 | return widget.serialize(); 45 | }); 46 | 47 | var item = { widgets: serialized, hash: this.hash }; 48 | 49 | if (this.stringify) { 50 | item = JSON.stringify(item); 51 | } 52 | 53 | return this.storage.setItem(this.id, item) || true; 54 | }, 55 | 56 | /** 57 | * Loads dashboard state from the storage object. 58 | * Can handle a synchronous response or a promise. 59 | * 60 | * @return {Array|Promise} Array of widget definitions or a promise 61 | */ 62 | load: function () { 63 | 64 | if (!this.storage) { 65 | return null; 66 | } 67 | 68 | var serialized; 69 | 70 | // try loading storage item 71 | serialized = this.storage.getItem( this.id ); 72 | 73 | if (serialized) { 74 | // check for promise 75 | if (angular.isObject(serialized) && angular.isFunction(serialized.then)) { 76 | return this._handleAsyncLoad(serialized); 77 | } 78 | // otherwise handle synchronous load 79 | return this._handleSyncLoad(serialized); 80 | } else { 81 | return null; 82 | } 83 | }, 84 | 85 | _handleSyncLoad: function(serialized) { 86 | 87 | var deserialized, result = []; 88 | 89 | if (!serialized) { 90 | return null; 91 | } 92 | 93 | if (this.stringify) { 94 | try { // to deserialize the string 95 | 96 | deserialized = JSON.parse(serialized); 97 | 98 | } catch (e) { 99 | 100 | // bad JSON, log a warning and return 101 | $log.warn('Serialized dashboard state was malformed and could not be parsed: ', serialized); 102 | return null; 103 | 104 | } 105 | } 106 | else { 107 | deserialized = serialized; 108 | } 109 | 110 | // check hash against current hash 111 | if (deserialized.hash !== this.hash) { 112 | 113 | $log.info('Serialized dashboard from storage was stale (old hash: ' + deserialized.hash + ', new hash: ' + this.hash + ')'); 114 | this.storage.removeItem(this.id); 115 | return null; 116 | 117 | } 118 | 119 | // Cache widgets 120 | var savedWidgetDefs = deserialized.widgets; 121 | 122 | // instantiate widgets from stored data 123 | for (var i = 0; i < savedWidgetDefs.length; i++) { 124 | 125 | // deserialized object 126 | var savedWidgetDef = savedWidgetDefs[i]; 127 | 128 | // widget definition to use 129 | var widgetDefinition = this.widgetDefinitions.getByName(savedWidgetDef.name); 130 | 131 | // check for no widget 132 | if (!widgetDefinition) { 133 | // no widget definition found, remove and return false 134 | $log.warn('Widget with name "' + savedWidgetDef.name + '" was not found in given widget definition objects'); 135 | continue; 136 | } 137 | 138 | // check widget-specific storageHash 139 | if (widgetDefinition.hasOwnProperty('storageHash') && widgetDefinition.storageHash !== savedWidgetDef.storageHash) { 140 | // widget definition was found, but storageHash was stale, removing storage 141 | $log.info('Widget Definition Object with name "' + savedWidgetDef.name + '" was found ' + 142 | 'but the storageHash property on the widget definition is different from that on the ' + 143 | 'serialized widget loaded from storage. hash from storage: "' + savedWidgetDef.storageHash + '"' + 144 | ', hash from WDO: "' + widgetDefinition.storageHash + '"'); 145 | continue; 146 | } 147 | 148 | // push instantiated widget to result array 149 | result.push(savedWidgetDef); 150 | } 151 | 152 | return result; 153 | }, 154 | 155 | _handleAsyncLoad: function(promise) { 156 | var self = this; 157 | var deferred = $q.defer(); 158 | promise.then( 159 | // success 160 | function(res) { 161 | var result = self._handleSyncLoad(res); 162 | if (result) { 163 | deferred.resolve(result); 164 | } else { 165 | deferred.reject(result); 166 | } 167 | }, 168 | // failure 169 | function(res) { 170 | deferred.reject(res); 171 | } 172 | ); 173 | 174 | return deferred.promise; 175 | } 176 | 177 | }; 178 | return DashboardState; 179 | }]); -------------------------------------------------------------------------------- /src/components/models/DashboardState.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Factory: DashboardState', function () { 4 | 5 | // load the service's module 6 | beforeEach(module('ui.dashboard')); 7 | 8 | // instantiate service 9 | var $q, $rootScope, DashboardState, WidgetDefCollection, storageData; 10 | 11 | beforeEach(inject(function (_$q_, _$rootScope_, _DashboardState_, _WidgetDefCollection_) { 12 | $q = _$q_; 13 | $rootScope = _$rootScope_; 14 | DashboardState = _DashboardState_; 15 | WidgetDefCollection = _WidgetDefCollection_; 16 | })); 17 | 18 | var model, storage, widgetDefitions, obj; 19 | 20 | beforeEach(function() { 21 | // let's mock the storage 22 | storage = { 23 | getItem: function(id) { 24 | return obj[id]; 25 | }, 26 | removeItem: function(id) { 27 | delete obj[id]; 28 | }, 29 | setItem: function(id, item) { 30 | storageData[id] = item; 31 | } 32 | }; 33 | 34 | obj = { 35 | id1: JSON.stringify({ 36 | widgets: [ 37 | { 38 | title: 'Widget 1', 39 | name: 'random' 40 | } 41 | ], 42 | }), 43 | id2: { 44 | widgets: [ 45 | { 46 | title: 'Widget 2', 47 | name: 'time' 48 | } 49 | ], 50 | }, 51 | id3: JSON.stringify({ 52 | widgets: [ 53 | { 54 | title: 'Widget 3', 55 | name: 'time_xxxxx' 56 | } 57 | ], 58 | }), 59 | id4: 'BAD JSON STRING' 60 | }; 61 | 62 | widgetDefitions = new WidgetDefCollection([ 63 | { 64 | name: 'random', 65 | directive: 'wt-scope-watch', 66 | attrs: { 67 | value: 'randomValue' 68 | } 69 | }, 70 | { 71 | name: 'time', 72 | directive: 'wt-time' 73 | } 74 | ]); 75 | 76 | model = new DashboardState(storage, 'id1', undefined, widgetDefitions, true); 77 | }); 78 | 79 | it('should be a function', function() { 80 | expect(typeof DashboardState).toEqual('function'); 81 | }); 82 | 83 | describe('the constructor', function() { 84 | 85 | it('should create a dashboard state object', function() { 86 | expect(typeof model).toEqual('object'); 87 | expect(typeof model.storage).toEqual('object') 88 | expect(typeof model.widgetDefinitions).toEqual('object') 89 | expect(model.id).toEqual('id1'); 90 | expect(model.hash).toBeUndefined(); 91 | expect(model.stringify).toBe(true); 92 | }); 93 | 94 | }); 95 | 96 | describe('the load function', function() { 97 | 98 | it('should load widget', function() { 99 | var result = model.load(); 100 | expect(result.length).toEqual(1); 101 | expect(result[0].title).toEqual('Widget 1'); 102 | expect(result[0].name).toEqual('random'); 103 | }); 104 | 105 | it('should load a non-stringify widget', function() { 106 | model.stringify = false; 107 | model.id = 'id2'; 108 | 109 | var result = model.load(); 110 | expect(result.length).toEqual(1); 111 | expect(result[0].title).toEqual('Widget 2'); 112 | expect(result[0].name).toEqual('time'); 113 | }); 114 | 115 | it('should abort when storage is undefined', function() { 116 | delete model.storage; 117 | var result = model.load(); 118 | expect(result).toEqual(null); 119 | }) 120 | 121 | it('should abort when serialized is undefined', function() { 122 | expect(model._handleSyncLoad()).toEqual(null); 123 | }); 124 | 125 | it('should not load anything', function() { 126 | model.id = 'xxx'; 127 | var result = model.load(); 128 | expect(result).toEqual(null); 129 | }); 130 | 131 | it('should return null', function() { 132 | model.id = 'id4'; 133 | var result = model.load(); 134 | expect(result).toEqual(null); 135 | }); 136 | 137 | it('should return null because of outdated hash', function() { 138 | model.hash = 'xxxxxx'; 139 | var result = model.load(); 140 | expect(result).toEqual(null); 141 | }); 142 | 143 | it('should load empty array', function() { 144 | // should not load anything if widget is not in the definition 145 | model.id = 'id3'; 146 | var result = model.load(); 147 | expect(result.length).toEqual(0); 148 | }); 149 | 150 | it('should return null because of stale storage hash', function() { 151 | widgetDefitions[0].storageHash = 'xxxxx'; 152 | var result = model.load(); 153 | expect(result.length).toEqual(0); 154 | }); 155 | 156 | describe('the async functions', function() { 157 | 158 | beforeEach(function() { 159 | // let's mock an async storage 160 | model.storage = { 161 | getItem: function(id) { 162 | var deferred = $q.defer(); 163 | if (id === 'BAD_ID') { 164 | deferred.reject('bad id'); 165 | } else { 166 | deferred.resolve(obj[id]); 167 | } 168 | return deferred.promise; 169 | } 170 | }; 171 | }); 172 | 173 | it('should resolve with one widget', function() { 174 | model.load().then( 175 | function(result) { 176 | expect(result[0].name).toEqual('random'); 177 | expect(result[0].title).toEqual('Widget 1'); 178 | }, 179 | function(error) { 180 | throw 'Error: ' + error 181 | } 182 | ); 183 | $rootScope.$digest(); 184 | }); 185 | 186 | it('should reject with null', function() { 187 | model.id = 'id4'; 188 | model.load().then( 189 | function(result) { 190 | throw 'Expected error but received result: ' + result 191 | }, 192 | function(error) { 193 | expect(error).toEqual(null); 194 | } 195 | ); 196 | $rootScope.$digest(); 197 | }); 198 | 199 | it('should reject by storage.load', function() { 200 | model.id = 'BAD_ID'; 201 | model.load().then( 202 | function(result) { 203 | throw 'Expected error but received result: ' + result 204 | }, 205 | function(error) { 206 | expect(error).toEqual('bad id'); 207 | } 208 | ); 209 | $rootScope.$digest(); 210 | }); 211 | 212 | }); 213 | 214 | }); 215 | 216 | describe('the save function', function() { 217 | 218 | var widgets, func; 219 | 220 | beforeEach(function() { 221 | func = function() { 222 | return { 223 | name: this.name, 224 | title: this.title 225 | }; 226 | }; 227 | 228 | widgets = [ 229 | { 230 | name: 'random', 231 | title: 'My new widget #1', 232 | serialize: func 233 | }, 234 | { 235 | name: 'time', 236 | title: 'My new widget #2', 237 | serialize: func 238 | } 239 | ]; 240 | 241 | storageData = {}; 242 | }); 243 | 244 | it('should add widgets to storageData', function() { 245 | expect(model.save(widgets)).toBe(true); 246 | expect(storageData.id1).toBeDefined(); 247 | }); 248 | 249 | it('should add non-stringify wdigets', function() { 250 | model.stringify = false; 251 | expect(model.save(widgets)).toBe(true); 252 | expect(storageData.id1).toBeDefined(); 253 | }); 254 | 255 | it('should abort', function() { 256 | delete model.storage 257 | expect(model.save(widgets)).toBe(true); 258 | expect(_.isEmpty(storageData)).toBe(true); 259 | }); 260 | 261 | 262 | }); 263 | 264 | }); -------------------------------------------------------------------------------- /src/components/models/LayoutStorage.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.dashboard') 20 | .factory('LayoutStorage', function() { 21 | 22 | var noopStorage = { 23 | setItem: function() { 24 | 25 | }, 26 | getItem: function() { 27 | 28 | }, 29 | removeItem: function() { 30 | 31 | } 32 | }; 33 | 34 | 35 | 36 | function LayoutStorage(options) { 37 | 38 | var defaults = { 39 | storage: noopStorage, 40 | storageHash: '', 41 | stringifyStorage: true 42 | }; 43 | 44 | angular.extend(defaults, options); 45 | angular.extend(options, defaults); 46 | 47 | this.id = options.storageId; 48 | this.storage = options.storage; 49 | this.storageHash = options.storageHash; 50 | this.stringifyStorage = options.stringifyStorage; 51 | this.widgetDefinitions = options.widgetDefinitions; 52 | this.defaultLayouts = options.defaultLayouts; 53 | this.lockDefaultLayouts = options.lockDefaultLayouts; 54 | this.widgetButtons = options.widgetButtons; 55 | this.explicitSave = options.explicitSave; 56 | this.defaultWidgets = options.defaultWidgets; 57 | this.settingsModalOptions = options.settingsModalOptions; 58 | this.onSettingsClose = options.onSettingsClose; 59 | this.onSettingsDismiss = options.onSettingsDismiss; 60 | this.options = options; 61 | this.options.unsavedChangeCount = 0; 62 | 63 | this.layouts = []; 64 | this.states = {}; 65 | this.load(); 66 | this._ensureActiveLayout(); 67 | } 68 | 69 | LayoutStorage.prototype = { 70 | 71 | add: function(layouts) { 72 | if (!angular.isArray(layouts)) { 73 | layouts = [layouts]; 74 | } 75 | var self = this; 76 | angular.forEach(layouts, function(layout) { 77 | layout.dashboard = layout.dashboard || {}; 78 | layout.dashboard.storage = self; 79 | layout.dashboard.storageId = layout.id = self._getLayoutId.call(self,layout); 80 | layout.dashboard.widgetDefinitions = layout.widgetDefinitions || self.widgetDefinitions; 81 | layout.dashboard.stringifyStorage = false; 82 | layout.dashboard.defaultWidgets = layout.defaultWidgets || self.defaultWidgets; 83 | layout.dashboard.widgetButtons = self.widgetButtons; 84 | layout.dashboard.explicitSave = self.explicitSave; 85 | layout.dashboard.settingsModalOptions = self.settingsModalOptions; 86 | layout.dashboard.onSettingsClose = self.onSettingsClose; 87 | layout.dashboard.onSettingsDismiss = self.onSettingsDismiss; 88 | self.layouts.push(layout); 89 | }); 90 | }, 91 | 92 | remove: function(layout) { 93 | var index = this.layouts.indexOf(layout); 94 | if (index >= 0) { 95 | this.layouts.splice(index, 1); 96 | delete this.states[layout.id]; 97 | 98 | // check for active 99 | if (layout.active && this.layouts.length) { 100 | var nextActive = index > 0 ? index - 1 : 0; 101 | this.layouts[nextActive].active = true; 102 | } 103 | } 104 | }, 105 | 106 | save: function() { 107 | 108 | var state = { 109 | layouts: this._serializeLayouts(), 110 | states: this.states, 111 | storageHash: this.storageHash 112 | }; 113 | 114 | if (this.stringifyStorage) { 115 | state = JSON.stringify(state); 116 | } 117 | 118 | this.storage.setItem(this.id, state); 119 | this.options.unsavedChangeCount = 0; 120 | }, 121 | 122 | load: function() { 123 | 124 | var serialized = this.storage.getItem(this.id); 125 | 126 | this.clear(); 127 | 128 | if (serialized) { 129 | // check for promise 130 | if (angular.isObject(serialized) && angular.isFunction(serialized.then)) { 131 | this._handleAsyncLoad(serialized); 132 | } else { 133 | this._handleSyncLoad(serialized); 134 | } 135 | } else { 136 | this._addDefaultLayouts(); 137 | } 138 | }, 139 | 140 | clear: function() { 141 | this.layouts = []; 142 | this.states = {}; 143 | }, 144 | 145 | setItem: function(id, value) { 146 | this.states[id] = value; 147 | this.save(); 148 | }, 149 | 150 | getItem: function(id) { 151 | return this.states[id]; 152 | }, 153 | 154 | removeItem: function(id) { 155 | delete this.states[id]; 156 | this.save(); 157 | }, 158 | 159 | getActiveLayout: function() { 160 | var len = this.layouts.length; 161 | for (var i = 0; i < len; i++) { 162 | var layout = this.layouts[i]; 163 | if (layout.active) { 164 | return layout; 165 | } 166 | } 167 | return false; 168 | }, 169 | 170 | _addDefaultLayouts: function() { 171 | var self = this; 172 | var defaults = this.lockDefaultLayouts ? { locked: true } : {}; 173 | angular.forEach(this.defaultLayouts, function(layout) { 174 | self.add(angular.extend(_.clone(defaults), layout)); 175 | }); 176 | }, 177 | 178 | _serializeLayouts: function() { 179 | var result = []; 180 | angular.forEach(this.layouts, function(l) { 181 | result.push({ 182 | title: l.title, 183 | id: l.id, 184 | active: l.active, 185 | locked: l.locked, 186 | defaultWidgets: l.dashboard.defaultWidgets 187 | }); 188 | }); 189 | return result; 190 | }, 191 | 192 | _handleSyncLoad: function(serialized) { 193 | 194 | var deserialized; 195 | 196 | if (this.stringifyStorage) { 197 | try { 198 | 199 | deserialized = JSON.parse(serialized); 200 | 201 | } catch (e) { 202 | this._addDefaultLayouts(); 203 | return; 204 | } 205 | } else { 206 | 207 | deserialized = serialized; 208 | 209 | } 210 | 211 | if (this.storageHash !== deserialized.storageHash) { 212 | this._addDefaultLayouts(); 213 | return; 214 | } 215 | this.states = deserialized.states; 216 | this.add(deserialized.layouts); 217 | }, 218 | 219 | _handleAsyncLoad: function(promise) { 220 | var self = this; 221 | promise.then( 222 | angular.bind(self, this._handleSyncLoad), 223 | angular.bind(self, this._addDefaultLayouts) 224 | ); 225 | }, 226 | 227 | _ensureActiveLayout: function() { 228 | for (var i = 0; i < this.layouts.length; i++) { 229 | var layout = this.layouts[i]; 230 | if (layout.active) { 231 | return; 232 | } 233 | } 234 | if (this.layouts[0]) { 235 | this.layouts[0].active = true; 236 | } 237 | }, 238 | 239 | _getLayoutId: function(layout) { 240 | if (layout.id) { 241 | return layout.id; 242 | } 243 | var max = 0; 244 | for (var i = 0; i < this.layouts.length; i++) { 245 | var id = this.layouts[i].id; 246 | max = Math.max(max, id * 1); 247 | } 248 | return max + 1; 249 | } 250 | 251 | }; 252 | return LayoutStorage; 253 | }); -------------------------------------------------------------------------------- /src/components/models/WidgetDataModel.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.dashboard') 20 | .factory('WidgetDataModel', function () { 21 | function WidgetDataModel() { 22 | } 23 | 24 | WidgetDataModel.prototype = { 25 | setup: function (widget, scope) { 26 | this.dataAttrName = widget.dataAttrName; 27 | this.dataModelOptions = widget.dataModelOptions; 28 | this.widgetScope = scope; 29 | }, 30 | 31 | updateScope: function (data) { 32 | this.widgetScope.widgetData = data; 33 | }, 34 | 35 | init: function () { 36 | // to be overridden by subclasses 37 | }, 38 | 39 | destroy: function () { 40 | // to be overridden by subclasses 41 | } 42 | }; 43 | 44 | return WidgetDataModel; 45 | }); -------------------------------------------------------------------------------- /src/components/models/WidgetDataModel.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Factory: WidgetDataModel', function () { 4 | 5 | // load the service's module 6 | beforeEach(module('ui.dashboard')); 7 | 8 | // instantiate service 9 | var WidgetDataModel; 10 | beforeEach(inject(function (_WidgetDataModel_) { 11 | WidgetDataModel = _WidgetDataModel_; 12 | })); 13 | 14 | var model; 15 | 16 | beforeEach(function() { 17 | model = new WidgetDataModel(); 18 | }); 19 | 20 | it('should be a function', function() { 21 | expect(typeof WidgetDataModel).toEqual('function'); 22 | }); 23 | 24 | describe('the constructor', function() { 25 | 26 | it('should have functions', function() { 27 | expect(typeof model.setup).toEqual('function'); 28 | expect(typeof model.updateScope).toEqual('function'); 29 | expect(typeof model.init).toEqual('function'); 30 | expect(typeof model.destroy).toEqual('function'); 31 | }); 32 | 33 | }); 34 | 35 | describe('the setup function', function() { 36 | 37 | it('should set widget and scope', function() { 38 | // let's mock some data 39 | var widget = { 40 | dataAttrName: 'test attribute name', 41 | dataModelOptions: 'some options' 42 | }; 43 | var scope = 'the scope'; 44 | 45 | model.setup(widget, scope); 46 | 47 | expect(model.dataAttrName).toEqual('test attribute name'); 48 | expect(model.dataModelOptions).toEqual('some options'); 49 | expect(model.widgetScope).toEqual('the scope'); 50 | }); 51 | 52 | }); 53 | 54 | describe('the updateScope function', function() { 55 | 56 | it('should update widgetData', function() { 57 | model.widgetScope = {}; 58 | model.updateScope('new data'); 59 | expect(model.widgetScope.widgetData).toEqual('new data'); 60 | }); 61 | 62 | }); 63 | 64 | describe('the init function', function() { 65 | 66 | it('should execute without error', function() { 67 | var result = 'some text'; 68 | 69 | expect(function() { 70 | result = model.init() 71 | }).not.toThrow(); 72 | 73 | expect(result).toBeUndefined(); 74 | }); 75 | 76 | }); 77 | 78 | describe('the destroy function', function() { 79 | 80 | it('should execute without error', function() { 81 | var result = 'some text'; 82 | 83 | expect(function() { 84 | result = model.destroy() 85 | }).not.toThrow(); 86 | 87 | expect(result).toBeUndefined(); 88 | }); 89 | 90 | }); 91 | 92 | }); -------------------------------------------------------------------------------- /src/components/models/WidgetDefCollection.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.dashboard') 20 | .factory('WidgetDefCollection', function () { 21 | 22 | function convertToDefinition(d) { 23 | if (typeof d === 'function') { 24 | return new d(); 25 | } 26 | return d; 27 | } 28 | 29 | function WidgetDefCollection(widgetDefs) { 30 | 31 | widgetDefs = widgetDefs.map(convertToDefinition); 32 | 33 | this.push.apply(this, widgetDefs); 34 | 35 | // build (name -> widget definition) map for widget lookup by name 36 | var map = {}; 37 | _.each(widgetDefs, function (widgetDef) { 38 | map[widgetDef.name] = widgetDef; 39 | }); 40 | this.map = map; 41 | } 42 | 43 | WidgetDefCollection.prototype = Object.create(Array.prototype); 44 | 45 | WidgetDefCollection.prototype.getByName = function (name) { 46 | return this.map[name]; 47 | }; 48 | 49 | WidgetDefCollection.prototype.add = function(def) { 50 | def = convertToDefinition(def); 51 | this.push(def); 52 | this.map[def.name] = def; 53 | }; 54 | 55 | return WidgetDefCollection; 56 | }); 57 | -------------------------------------------------------------------------------- /src/components/models/WidgetDefCollection.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Factory: WidgetDefCollection', function () { 4 | 5 | // load the service's module 6 | beforeEach(module('ui.dashboard')); 7 | 8 | // instantiate service 9 | var WidgetDefCollection; 10 | beforeEach(inject(function (_WidgetDefCollection_) { 11 | WidgetDefCollection = _WidgetDefCollection_; 12 | })); 13 | 14 | var widgetDefs = [ 15 | { 16 | name: 'random', 17 | directive: 'wt-scope-watch', 18 | attrs: { 19 | value: 'randomValue' 20 | } 21 | }, 22 | { 23 | name: 'time', 24 | directive: 'wt-time' 25 | } 26 | ]; 27 | 28 | it('should be a function', function() { 29 | expect(typeof WidgetDefCollection).toEqual('function'); 30 | }); 31 | 32 | describe('the constructor', function() { 33 | 34 | it('should create widget definitions object', function() { 35 | var model = new WidgetDefCollection(widgetDefs); 36 | 37 | expect(_.isObject(model)).toBe(true); 38 | expect(model[0].name).toEqual('random'); 39 | expect(model[0].directive).toEqual('wt-scope-watch'); 40 | expect(model[0].attrs).toEqual({ value: 'randomValue' }); 41 | expect(model[1].name).toEqual('time'); 42 | expect(model[1].directive).toEqual('wt-time'); 43 | }); 44 | 45 | }); 46 | 47 | describe('the constructor with definition function', function() { 48 | 49 | it('should create the definition using function', function() { 50 | var func = function() { 51 | return widgetDefs[0]; 52 | }; 53 | var model = new WidgetDefCollection([ func ]); 54 | 55 | expect(_.isObject(model)).toBe(true); 56 | expect(model[0].name).toEqual('random'); 57 | expect(model[0].directive).toEqual('wt-scope-watch'); 58 | expect(model[0].attrs).toEqual({ value: 'randomValue' }); 59 | }); 60 | 61 | }); 62 | 63 | describe('the getByName function', function() { 64 | 65 | it('should return a widget definition', function() { 66 | var model = new WidgetDefCollection(widgetDefs); 67 | var result = model.getByName('random'); 68 | 69 | expect(result.name).toEqual('random'); 70 | expect(result.directive).toEqual('wt-scope-watch'); 71 | expect(result.attrs).toEqual({ value: 'randomValue' }); 72 | }); 73 | 74 | it('should not find anything', function() { 75 | var model = new WidgetDefCollection(widgetDefs); 76 | var result = model.getByName('random'); 77 | 78 | expect(result.name).toEqual('random'); 79 | expect(result.directive).toEqual('wt-scope-watch'); 80 | expect(result.attrs).toEqual({ value: 'randomValue' }); 81 | }) 82 | 83 | }); 84 | 85 | describe('the add function', function() { 86 | 87 | it('should add a widget definition to the collection', function() { 88 | var model = new WidgetDefCollection(widgetDefs); 89 | model.add({ 90 | name: 'new-wt' 91 | }); 92 | 93 | expect(model[0].name).toEqual('random'); 94 | expect(model[1].name).toEqual('time'); 95 | expect(model[2].name).toEqual('new-wt'); 96 | }); 97 | 98 | }); 99 | 100 | }); -------------------------------------------------------------------------------- /src/components/models/WidgetModel.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.dashboard') 20 | .factory('WidgetModel', function ($log) { 21 | 22 | function defaults() { 23 | return { 24 | title: 'Widget', 25 | style: {}, 26 | size: { width: '33%' }, 27 | enableVerticalResize: true, 28 | containerStyle: { width: '33%' }, // default width 29 | contentStyle: {} 30 | }; 31 | }; 32 | 33 | // constructor for widget model instances 34 | function WidgetModel(widgetDefinition, overrides) { 35 | // Extend this with the widget definition object with overrides merged in (deep extended). 36 | angular.extend(this, defaults(), _.merge(angular.copy(widgetDefinition), overrides)); 37 | 38 | this.updateContainerStyle(this.style); 39 | 40 | if (!this.templateUrl && !this.template && !this.directive) { 41 | this.directive = widgetDefinition.name; 42 | } 43 | 44 | if (this.size && _.has(this.size, 'height')) { 45 | this.setHeight(this.size.height); 46 | } 47 | 48 | if (this.style && _.has(this.style, 'width')) { //TODO deprecate style attribute 49 | this.setWidth(this.style.width); 50 | } 51 | 52 | if (this.size && _.has(this.size, 'width')) { 53 | this.setWidth(this.size.width); 54 | } 55 | } 56 | 57 | WidgetModel.prototype = { 58 | // sets the width (and widthUnits) 59 | setWidth: function (width, units) { 60 | width = width.toString(); 61 | units = units || width.replace(/^[-\.\d]+/, '') || '%'; 62 | 63 | this.widthUnits = units; 64 | width = parseFloat(width); 65 | 66 | // check with min width if set, unit refer to width's unit 67 | if (this.size && _.has(this.size, 'minWidth') && _.endsWith(this.size.minWidth, units)) { 68 | width = _.max([parseFloat(this.size.minWidth), width]); 69 | } 70 | if (width < 0 || isNaN(width)) { 71 | $log.warn('malhar-angular-dashboard: setWidth was called when width was ' + width); 72 | return; 73 | } 74 | 75 | if (units === '%') { 76 | width = Math.min(100, width); 77 | width = Math.max(0, width); 78 | } 79 | 80 | this.containerStyle.width = width + '' + units; 81 | 82 | this.updateSize(this.containerStyle); 83 | 84 | return width + units; 85 | }, 86 | 87 | setHeight: function (height) { 88 | this.contentStyle.height = height; 89 | this.updateSize(this.contentStyle); 90 | 91 | return height + 'px'; 92 | }, 93 | 94 | setStyle: function (style) { 95 | this.style = style; 96 | this.updateContainerStyle(style); 97 | }, 98 | 99 | updateSize: function (size) { 100 | angular.extend(this.size, size); 101 | }, 102 | 103 | updateContainerStyle: function (style) { 104 | angular.extend(this.containerStyle, style); 105 | }, 106 | serialize: function() { 107 | return _.pick(this, ['title', 'name', 'style', 'size', 'dataModelOptions', 'attrs', 'storageHash']); 108 | } 109 | }; 110 | 111 | return WidgetModel; 112 | }); -------------------------------------------------------------------------------- /src/components/models/WidgetModel.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Factory: WidgetModel', function () { 4 | 5 | // load the service's module 6 | beforeEach(module('ui.dashboard')); 7 | 8 | // instantiate service 9 | var WidgetModel; 10 | beforeEach(inject(function (_WidgetModel_) { 11 | WidgetModel = _WidgetModel_; 12 | })); 13 | 14 | it('should be a function', function() { 15 | expect(typeof WidgetModel).toEqual('function'); 16 | }); 17 | 18 | describe('the constructor', function() { 19 | var m, Class, Class2, overrides; 20 | 21 | beforeEach(function() { 22 | Class = { 23 | name: 'TestWidget', 24 | attrs: {}, 25 | dataAttrName: 'attr-name', 26 | dataModelType: function TestType() {}, 27 | dataModelOptions: {}, 28 | style: { width: '10em' }, 29 | settingsModalOptions: {}, 30 | onSettingsClose: function() {}, 31 | onSettingsDismiss: function() {}, 32 | funkyChicken: { 33 | cool: false, 34 | fun: true 35 | } 36 | }; 37 | 38 | Class2 = { 39 | name: 'TestWidget2', 40 | attrs: {}, 41 | dataAttrName: 'attr-name', 42 | dataModelType: function TestType() {}, 43 | dataModelOptions: {}, 44 | style: { width: '10em' }, 45 | templateUrl: 'my/url.html', 46 | template: '
some template
' 47 | }; 48 | 49 | overrides = { 50 | size: { 51 | height: '100px' 52 | }, 53 | style: { 54 | width: '15em', 55 | minWidth: '10em' 56 | } 57 | }; 58 | spyOn(WidgetModel.prototype, 'setWidth'); 59 | spyOn(WidgetModel.prototype, 'setHeight'); 60 | m = new WidgetModel(Class, overrides); 61 | }); 62 | 63 | it('should copy class defaults, so that changes on an instance do not change the Class', function() { 64 | m.style.width = '20em'; 65 | expect(Class.style.width).toEqual('10em'); 66 | }); 67 | 68 | it('should call setWidth', function() { 69 | expect(WidgetModel.prototype.setWidth).toHaveBeenCalled(); 70 | }); 71 | 72 | it('should call setHeight', function() { 73 | expect(WidgetModel.prototype.setHeight).toHaveBeenCalled(); 74 | }); 75 | 76 | it('should take overrides as precedent over Class defaults', function() { 77 | expect(m.style.width).toEqual('15em'); 78 | }); 79 | 80 | it('should copy arbitrary data from the widget definition', function() { 81 | expect(m.funkyChicken.cool).toEqual(false); 82 | expect(m.funkyChicken.fun).toEqual(true); 83 | expect(m.funkyChicken===Class.funkyChicken).toEqual(false); 84 | }); 85 | 86 | it('should set templateUrl if and only if it is present on Class', function() { 87 | var m2 = new WidgetModel(Class2, overrides); 88 | expect(m2.templateUrl).toEqual('my/url.html'); 89 | }); 90 | 91 | it('should set template if and only if it is present on Class', function() { 92 | delete Class2.templateUrl; 93 | var m2 = new WidgetModel(Class2, overrides); 94 | expect(m2.template).toEqual('
some template
'); 95 | }); 96 | 97 | it('should look for directive if neither templateUrl nor template is found on Class', function() { 98 | delete Class2.templateUrl; 99 | delete Class2.template; 100 | Class2.directive = 'ng-bind'; 101 | var m2 = new WidgetModel(Class2, overrides); 102 | expect(m2.directive).toEqual('ng-bind'); 103 | }); 104 | 105 | it('should set the name as directive if templateUrl, template, and directive are not defined', function() { 106 | delete Class2.templateUrl; 107 | delete Class2.template; 108 | var m2 = new WidgetModel(Class2, overrides); 109 | expect(m2.directive).toEqual('TestWidget2'); 110 | }); 111 | 112 | it('should not require overrides', function() { 113 | var fn = function() { 114 | var m2 = new WidgetModel(Class); 115 | } 116 | expect(fn).not.toThrow(); 117 | }); 118 | 119 | it('should copy references to settingsModalOptions, onSettingsClose, onSettingsDismiss', function() { 120 | var m = new WidgetModel(Class); 121 | expect(m.settingsModalOptions).toEqual(Class.settingsModalOptions); 122 | expect(m.onSettingsClose).toEqual(Class.onSettingsClose); 123 | expect(m.onSettingsDismiss).toEqual(Class.onSettingsDismiss); 124 | }); 125 | 126 | }); 127 | 128 | describe('setWidth method', function() { 129 | 130 | var context, setWidth; 131 | 132 | beforeEach(function() { 133 | var overrides = { 134 | size: { 135 | minWidth: '10%' 136 | } 137 | }; 138 | context = new WidgetModel(overrides); 139 | setWidth = WidgetModel.prototype.setWidth; 140 | }); 141 | 142 | it('should take one argument as a string with units', function() { 143 | setWidth.call(context, '100px'); 144 | expect(context.containerStyle.width).toEqual('100px'); 145 | }); 146 | 147 | it('should take two args as a number and string as units', function() { 148 | setWidth.call(context, 100, 'px'); 149 | expect(context.containerStyle.width).toEqual('100px'); 150 | }); 151 | 152 | it('should return undefined and not set anything if width is less than 0', function() { 153 | var result = setWidth.call(context, -100, 'em'); 154 | expect(result).toBeUndefined(); 155 | expect(context.containerStyle.width).not.toEqual('-100em'); 156 | }); 157 | 158 | it('should assume % if no unit is given', function() { 159 | setWidth.call(context, 50); 160 | expect(context.containerStyle.width).toEqual('50%'); 161 | }); 162 | 163 | it('should force greater than 0% and less than or equal 100%', function() { 164 | setWidth.call(context, '110%'); 165 | expect(context.containerStyle.width).toEqual('100%'); 166 | }); 167 | 168 | it('should force min width to be used', function() { 169 | setWidth.call(context, 1, '%'); 170 | expect(context.containerStyle.width).toEqual('10%'); 171 | }); 172 | }); 173 | 174 | describe('setHeight method', function() { 175 | var context, setHeight; 176 | 177 | beforeEach(function() { 178 | context = new WidgetModel({}); 179 | setHeight = WidgetModel.prototype.setHeight; 180 | }); 181 | 182 | it('should set correct height', function() { 183 | setHeight.call(context, '200px'); 184 | expect(context.contentStyle.height).toEqual('200px'); 185 | }); 186 | }); 187 | 188 | describe('setStyle method', function() { 189 | var context, setStyle; 190 | 191 | beforeEach(function() { 192 | context = new WidgetModel({}); 193 | setStyle = WidgetModel.prototype.setStyle; 194 | }); 195 | 196 | it('should set correct style', function() { 197 | var style = { 198 | width: '70%', 199 | height: '300px' 200 | }; 201 | setStyle.call(context, style); 202 | expect(context.containerStyle).toEqual(style); 203 | }); 204 | }); 205 | 206 | describe('serialize method', function() { 207 | var context, serialize; 208 | 209 | beforeEach(function() { 210 | var overrides = { 211 | name: 'widget1', 212 | title: 'test widget', 213 | style: { 214 | height: '200px' 215 | }, 216 | size: { 217 | width: '50%' 218 | }, 219 | dataModelOptions: { 220 | value1: '1' 221 | }, 222 | attrs: { 223 | value2: '2' 224 | }, 225 | storageHash: 'xy' 226 | }; 227 | context = new WidgetModel(overrides); 228 | serialize = WidgetModel.prototype.serialize; 229 | }); 230 | 231 | it('should return title, name, stle, sie, dataModelOptions, attrs and storageHash', function() { 232 | var result = serialize.call(context); 233 | expect(result.name).toBeDefined(); 234 | expect(result.title).toBeDefined(); 235 | expect(result.style).toBeDefined(); 236 | expect(result.size).toBeDefined(); 237 | expect(result.dataModelOptions).toBeDefined(); 238 | expect(result.attrs).toBeDefined(); 239 | expect(result.storageHash).toBeDefined(); 240 | }); 241 | }); 242 | 243 | }); -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dtpublic/malhar-angular-dashboard/774e69e0b31ce344605e2aae5695c2d7ae5b90c4/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Malhar Angular Dashboard 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 26 | 27 | 48 | 49 |
50 | 51 |
52 | 53 |
54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /src/person.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dtpublic/malhar-angular-dashboard/774e69e0b31ce344605e2aae5695c2d7ae5b90c4/src/person.png -------------------------------------------------------------------------------- /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/SaveChangesModalCtrl.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: SaveChangesModalCtrl', function() { 4 | 5 | var $scope, $mockModalInstance, layout; 6 | 7 | beforeEach(module('ui.dashboard')); 8 | 9 | beforeEach(inject(function($rootScope, $controller){ 10 | $scope = $rootScope.$new(); 11 | $mockModalInstance = { 12 | close: function() {}, 13 | dismiss: function() {} 14 | }; 15 | layout = {}; 16 | $controller('SaveChangesModalCtrl', { 17 | $scope: $scope, 18 | $modalInstance: $mockModalInstance, 19 | layout: layout 20 | }); 21 | 22 | })); 23 | 24 | it('should set the injected layout to scope.layout', function() { 25 | expect($scope.layout === layout).toEqual(true) 26 | }); 27 | 28 | describe('the ok method', function() { 29 | it('should be a function', function() { 30 | expect(typeof $scope.ok).toEqual('function'); 31 | }); 32 | it('should call close', function() { 33 | spyOn($mockModalInstance, 'close'); 34 | $scope.ok(); 35 | expect($mockModalInstance.close).toHaveBeenCalled(); 36 | }); 37 | }); 38 | 39 | describe('the cancel function', function() { 40 | it('should be a function', function() { 41 | expect(typeof $scope.cancel).toEqual('function'); 42 | }); 43 | it('should call dismiss', function() { 44 | spyOn($mockModalInstance, 'dismiss'); 45 | $scope.cancel(); 46 | expect($mockModalInstance.dismiss).toHaveBeenCalled(); 47 | }); 48 | }); 49 | 50 | }); -------------------------------------------------------------------------------- /test/spec/dashboardState.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Factory: DashboardState', function () { 4 | 5 | // instantiate service 6 | var 7 | $q, 8 | DashboardState, 9 | state, 10 | state_no_storage, 11 | state_no_stringify, 12 | storage, 13 | id, 14 | hash, 15 | widgetDefinitions, 16 | item, 17 | setItemSpy, 18 | getItemSpy, 19 | removeItemSpy, 20 | $log, 21 | $rootScope; 22 | 23 | // load the service's module 24 | beforeEach(module('ui.dashboard', function($provide) { 25 | $log = { 26 | log: function() {}, 27 | warn: function() {}, 28 | info: function() {}, 29 | error: function() {}, 30 | debug: function() {} 31 | }; 32 | $provide.value('$log', $log); 33 | })); 34 | beforeEach(inject(function (_DashboardState_, _$q_, _$rootScope_) { 35 | DashboardState = _DashboardState_; 36 | $q = _$q_; 37 | $rootScope = _$rootScope_; 38 | 39 | storage = { 40 | setItem: function(key, item) { 41 | 42 | }, 43 | getItem: function() { 44 | return item; 45 | }, 46 | removeItem: function() { 47 | 48 | } 49 | }; 50 | 51 | id = 'myDashId'; 52 | 53 | hash = 'someHash'; 54 | 55 | widgetDefinitions = { 56 | getByName: function(name) { 57 | return { name: name }; 58 | } 59 | }; 60 | 61 | state = new DashboardState(storage, id, hash, widgetDefinitions, true); 62 | state_no_storage = new DashboardState(undefined, undefined, undefined, widgetDefinitions, true); 63 | state_no_stringify = new DashboardState(storage, id, hash, widgetDefinitions, false); 64 | 65 | })); 66 | 67 | it('should have save and load methods', function(){ 68 | expect(typeof DashboardState.prototype.load).toEqual('function'); 69 | expect(typeof DashboardState.prototype.save).toEqual('function'); 70 | }); 71 | 72 | it('should set the arguments passed to it in the constructor on the state instance', function() { 73 | expect(state.storage).toEqual(storage); 74 | expect(state.id).toEqual(id); 75 | expect(state.hash).toEqual(hash); 76 | expect(state.widgetDefinitions).toEqual(widgetDefinitions); 77 | }); 78 | 79 | describe('the save function', function() { 80 | 81 | it('should return true if storage is not defined', function() { 82 | expect(state_no_storage.save()).toEqual(true); 83 | }); 84 | 85 | it('should serialize and store widgets passed to it', function() { 86 | spyOn(storage, 'setItem'); 87 | var widgets = [ 88 | { title: 'widget1', name: 'Widget1', size: { width: '50%' }, dataModelOptions: { foo: 'bar' }, storageHash: '123', attrs: { bar: 'baz' } }, 89 | { title: 'widget2', name: 'Widget2', size: { width: '50%' }, dataModelOptions: { foo: 'bar' }, storageHash: '123'}, 90 | { title: 'widget3', name: 'Widget3', size: { width: '100%' }, attrs: { bar: 'baz' } }, 91 | { title: 'widget4', name: 'Widget3', dataModelOptions: { foo: 'baz' }, storageHash: '123', arbitrary: 'value' } 92 | ]; 93 | state.save(widgets); 94 | delete widgets[3].arbitrary; 95 | expect(storage.setItem).toHaveBeenCalledWith(id, JSON.stringify({widgets: widgets, hash: hash})); 96 | }); 97 | 98 | it('should not use JSON.stringify if options.stringifyStorage is false', function() { 99 | spyOn(storage, 'setItem'); 100 | var widgets = [ 101 | { title: 'widget1', name: 'Widget1', size: { width: '50%' }, dataModelOptions: { foo: 'bar' }, storageHash: '123', attrs: { bar: 'baz' } }, 102 | { title: 'widget2', name: 'Widget2', size: { width: '50%' }, dataModelOptions: { foo: 'bar' }, storageHash: '123'}, 103 | { title: 'widget3', name: 'Widget3', size: { width: '100%' }, attrs: { bar: 'baz' } }, 104 | { title: 'widget4', name: 'Widget3', dataModelOptions: { foo: 'baz' }, storageHash: '123' } 105 | ]; 106 | state_no_stringify.save(widgets); 107 | expect(typeof storage.setItem.calls.argsFor(0)[1]).toEqual('object'); 108 | }); 109 | 110 | }); 111 | 112 | describe('the load function', function() { 113 | 114 | var serialized; 115 | 116 | beforeEach(function() { 117 | serialized = { 118 | widgets: [ {name: 'W1'}, {name: 'W2'}, {name: 'W3'} ], 119 | hash: hash 120 | }; 121 | }); 122 | 123 | it('should return null if storage is disabled', function() { 124 | expect(state_no_storage.load()).toEqual(null); 125 | }); 126 | 127 | it('should return null if storage is enabled but no value was found', function() { 128 | spyOn(storage, 'getItem').and.returnValue(null); 129 | expect(state.load() === null).toEqual(true); 130 | }); 131 | 132 | it('should return null and log a warning if the stored string is invalid JSON', function() { 133 | var malformed = JSON.stringify(serialized).replace(/\}$/,''); 134 | spyOn(storage, 'getItem').and.returnValue(malformed); 135 | spyOn($log, 'warn'); 136 | var res = state.load(); 137 | expect(res).toEqual(null); 138 | expect($log.warn).toHaveBeenCalled(); 139 | }); 140 | 141 | it('should return an array of widgets if getItem returns valid serialized dashboard state', function() { 142 | spyOn(storage, 'getItem').and.returnValue(JSON.stringify(serialized)); 143 | var res = state.load(); 144 | expect(res instanceof Array).toEqual(true); 145 | expect(res).toEqual(serialized.widgets); 146 | }); 147 | 148 | it('should return null if getItem returned a falsey value', function() { 149 | var spy = spyOn(storage, 'getItem').and; 150 | spy.returnValue(false); 151 | expect(state.load()).toEqual(null); 152 | spy.returnValue(null); 153 | expect(state.load()).toEqual(null); 154 | spy.returnValue(undefined); 155 | expect(state.load()).toEqual(null); 156 | }); 157 | 158 | it('should return a promise if getItem returns a promise', function() { 159 | var deferred = $q.defer(); 160 | spyOn(storage, 'getItem').and.returnValue(deferred.promise); 161 | var res = state.load(); 162 | expect(typeof res.then).toEqual('function'); 163 | }); 164 | 165 | it('should return null if the stored hash is different from current hash', function() { 166 | serialized.hash = 'notTheSame'; 167 | spyOn(storage, 'getItem').and.returnValue(JSON.stringify(serialized)); 168 | spyOn($log, 'info'); 169 | var res = state.load(); 170 | expect(res).toEqual(null); 171 | expect($log.info).toHaveBeenCalled(); 172 | }); 173 | 174 | it('should not include widgets that have no corresponding WDO', function() { 175 | spyOn(widgetDefinitions, 'getByName').and.returnValue(false); 176 | spyOn($log, 'warn'); 177 | spyOn(storage, 'getItem').and.returnValue(JSON.stringify(serialized)); 178 | var res = state.load(); 179 | expect(res).toEqual([]); 180 | expect($log.warn.calls.count()).toEqual(3); 181 | }); 182 | 183 | it('should not include widgets who have a stale hash', function() { 184 | serialized.widgets[2].storageHash = 'something'; 185 | spyOn($log, 'info'); 186 | spyOn(storage, 'getItem').and.returnValue(JSON.stringify(serialized)); 187 | spyOn(widgetDefinitions, 'getByName').and.callFake(function(name) { 188 | if (name === 'W3') { 189 | return { name: 'W3', storageHash: 'else' }; 190 | } else { 191 | return { name: name }; 192 | } 193 | }); 194 | 195 | var res = state.load(); 196 | expect(res).toEqual(serialized.widgets.slice(0,2)); 197 | expect($log.info).toHaveBeenCalled(); 198 | }); 199 | 200 | describe('the loadPromise (returned from load method)', function() { 201 | 202 | var getItemDeferred, getItemPromise, loadPromise; 203 | 204 | beforeEach(function() { 205 | getItemDeferred = $q.defer(); 206 | getItemPromise = getItemDeferred.promise; 207 | spyOn(storage, 'getItem').and.returnValue(getItemPromise); 208 | loadPromise = state.load(); 209 | }); 210 | 211 | it('should resolve when the getItem promise resolves with a value', function() { 212 | var retval; 213 | loadPromise.then(function(value) { 214 | retval = value; 215 | }); 216 | getItemDeferred.resolve( JSON.stringify(serialized) ); 217 | $rootScope.$apply(); 218 | 219 | expect(retval).not.toBeUndefined(); 220 | }); 221 | 222 | it('should reject when the getItemPromise rejects', function() { 223 | var failed; 224 | loadPromise.then( 225 | function() { 226 | // success 227 | }, 228 | function(value) { 229 | failed = true; 230 | } 231 | ); 232 | 233 | getItemDeferred.reject(); 234 | 235 | $rootScope.$apply(); 236 | 237 | expect(failed).toEqual(true); 238 | }); 239 | 240 | it('should reject when the getItem promise resolves with a falsey (or no) value', function() { 241 | var failed; 242 | loadPromise.then( 243 | function() { 244 | // success 245 | }, 246 | function(value) { 247 | failed = true; 248 | } 249 | ); 250 | 251 | getItemDeferred.resolve( null ); 252 | 253 | $rootScope.$apply(); 254 | 255 | expect(failed).toEqual(true); 256 | }); 257 | 258 | }); 259 | 260 | it('should use JSON.parse if options.stringifyStorage is true', function() { 261 | var parse = JSON.parse; 262 | spyOn(JSON, 'parse').and.callFake(parse); 263 | spyOn(storage, 'getItem').and.returnValue(JSON.stringify(serialized)); 264 | spyOn(widgetDefinitions, 'getByName').and.callFake(function(name) { 265 | if (name === 'W3') { 266 | return { name: 'W3', storageHash: 'else' }; 267 | } else { 268 | return { name: name }; 269 | } 270 | }); 271 | 272 | var res = state.load(); 273 | expect(JSON.parse).toHaveBeenCalled(); 274 | }); 275 | 276 | it('should not use JSON.parse if options.stringifyStorage is false', function() { 277 | var parse = JSON.parse; 278 | spyOn(JSON, 'parse').and.callFake(parse); 279 | spyOn(storage, 'getItem').and.returnValue(serialized); 280 | spyOn(widgetDefinitions, 'getByName').and.callFake(function(name) { 281 | if (name === 'W3') { 282 | return { name: 'W3', storageHash: 'else' }; 283 | } else { 284 | return { name: name }; 285 | } 286 | }); 287 | 288 | var res = state_no_stringify.load(); 289 | expect(JSON.parse).not.toHaveBeenCalled(); 290 | }); 291 | 292 | }); 293 | 294 | }); -------------------------------------------------------------------------------- /test/spec/widgetDataModel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Factory: WidgetDataModel', function () { 4 | 5 | // load the service's module 6 | beforeEach(module('ui.dashboard')); 7 | 8 | // instantiate service 9 | var WidgetDataModel, m, widget, scope; 10 | 11 | beforeEach(inject(function (_WidgetDataModel_) { 12 | WidgetDataModel = _WidgetDataModel_; 13 | m = new WidgetDataModel(); 14 | widget = { 15 | dataAttrName: 'testing', 16 | dataModelOptions: { opt: true } 17 | }; 18 | scope = { 19 | fake: 'scope' 20 | }; 21 | })); 22 | 23 | it('should be a function', function() { 24 | expect(typeof WidgetDataModel).toEqual('function'); 25 | }); 26 | 27 | describe('setup method', function() { 28 | 29 | it('should set dataAttrName, dataModelOptions, and widgetScope from args', function() { 30 | m.setup(widget, scope); 31 | expect(m.dataAttrName).toEqual(widget.dataAttrName); 32 | expect(m.dataModelOptions).toEqual(widget.dataModelOptions); 33 | expect(m.widgetScope).toEqual(scope); 34 | }); 35 | 36 | }); 37 | 38 | describe('updateScope method', function() { 39 | 40 | it('should set scope.widgetData to passed data', function() { 41 | m.setup(widget, scope); 42 | var newData = []; 43 | m.updateScope(newData); 44 | expect(scope.widgetData).toEqual(newData); 45 | }); 46 | 47 | }); 48 | 49 | describe('init method', function() { 50 | it('should be an empty (noop) implementation', function() { 51 | expect(typeof m.init).toEqual('function'); 52 | expect(m.init).not.toThrow(); 53 | }); 54 | }); 55 | 56 | describe('destroy method', function() { 57 | it('should be an empty (noop) implementation', function() { 58 | expect(typeof m.destroy).toEqual('function'); 59 | expect(m.destroy).not.toThrow(); 60 | }); 61 | }); 62 | 63 | }); -------------------------------------------------------------------------------- /test/spec/widgetSettingsCtrl.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Controller: WidgetSettingsCtrl', function() { 4 | 5 | var $scope, widget, tplName, $modalInstance; 6 | 7 | beforeEach(module('ui.dashboard')); 8 | 9 | beforeEach(inject(function($rootScope, $controller){ 10 | $scope = $rootScope.$new(); 11 | widget = { title: 'Test Title' }; 12 | tplName = 'some/url/html'; 13 | $modalInstance = { 14 | close: function() { 15 | 16 | }, 17 | dismiss: function() { 18 | 19 | } 20 | }; 21 | $controller('WidgetSettingsCtrl', { 22 | $scope: $scope, 23 | $modalInstance: $modalInstance, 24 | widget: widget, 25 | optionsTemplateUrl: tplName 26 | }); 27 | })); 28 | 29 | it('should add widget to the dialog scope', function() { 30 | expect($scope.widget).toEqual(widget); 31 | }); 32 | 33 | describe('the ok method', function() { 34 | it('should call close with $scope.result and $scope.widget', function() { 35 | spyOn($modalInstance, 'close'); 36 | $scope.ok(); 37 | expect($modalInstance.close).toHaveBeenCalled(); 38 | expect($modalInstance.close.calls.argsFor(0)[0] === $scope.result).toEqual(true); 39 | }); 40 | }); 41 | 42 | describe('the cancel method', function() { 43 | it('should call dismiss', function() { 44 | spyOn($modalInstance, 'dismiss'); 45 | $scope.cancel(); 46 | expect($modalInstance.dismiss).toHaveBeenCalled(); 47 | }); 48 | }); 49 | 50 | }); --------------------------------------------------------------------------------