├── .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 |
148 | a mistyped address
149 | an out-of-date link
150 |
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: '',
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: '',
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","");
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 | Name
5 | Qty
6 | Unit
7 | Total
8 |
9 |
10 | {{::item.name}}
11 | {{item.qty}}
12 | {{item.price | currency}}
13 | {{item.total | currency}}
14 |
15 |
16 |
17 |
18 | The cart is empty
19 |
20 |
--------------------------------------------------------------------------------
/src/app/template/cartSummary.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Cart Total:
5 | {{cart.total | currency}}
6 |
7 |
8 | Total Qty:
9 | {{cart.qty}}
10 |
11 |
12 | Most Expensive:
13 | {{cart.expItem.qty}} {{cart.expItem.name}} @{{cart.expItem.price | currency}}
14 |
15 |
16 | Cheapest:
17 | {{cart.cheapItem.qty}} {{cart.cheapItem.name}} @{{cart.cheapItem.price | currency}}
18 |
19 |
20 |
21 | The cart is empty
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/app/template/configurableWidgetModalOptions.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/template/customSettingsTemplate.html:
--------------------------------------------------------------------------------
1 |
5 |
16 |
--------------------------------------------------------------------------------
/src/app/template/dynamicData.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/app/template/dynamicOptions.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Change widget to:
4 |
5 | List
6 | Thumbnail
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 | List
6 | Thumbnail
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 |
--------------------------------------------------------------------------------
/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 | {{::person.name}}
4 | {{::person.email}}
5 | {{::person.phone}}
6 |
7 |
8 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
69 |
--------------------------------------------------------------------------------
/src/components/directives/dashboard/dashboard.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
7 |
You have {{layout.dashboard.unsavedChangeCount}} unsaved changes on this dashboard. Would you like to save them?
8 |
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 |
28 |
29 |
30 |
39 |
40 |
41 |
46 |
47 |
48 |
49 |
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 | });
--------------------------------------------------------------------------------