├── .bowerrc
├── favicon.ico
├── src
├── app
│ ├── index.js
│ ├── vendor.less
│ ├── template
│ │ ├── resizable.html
│ │ ├── peopleThumbnail.html
│ │ ├── configurableWidgetModalOptions.html
│ │ ├── fluid.html
│ │ ├── layouts.html
│ │ ├── peopleList.html
│ │ ├── view.html
│ │ ├── dynamicData.html
│ │ ├── cartSummary.html
│ │ ├── cartDetail.html
│ │ ├── dynamicOptions.html
│ │ ├── widgetSpecificSettings.html
│ │ ├── customSettingsTemplate.html
│ │ └── dynamicOptionsContainer.html
│ ├── demo.less
│ ├── explicitSave.spec.js
│ ├── index.less
│ ├── explicitSave.js
│ ├── directives.js
│ ├── dataModel.js
│ ├── layouts.js
│ ├── resize.js
│ ├── dynamicData.js
│ ├── cartDataModel.js
│ ├── resize.spec.js
│ ├── dataModel.spec.js
│ ├── dynamicOptions.js
│ ├── layouts.spec.js
│ ├── cartDataModel.spec.js
│ ├── demo.spec.js
│ ├── dynamicOptions.spec.js
│ ├── dynamicData.spec.js
│ ├── directives.spec.js
│ ├── customWidgetSettings.js
│ ├── demo.js
│ └── customWidgetSettings.spec.js
├── person.png
├── favicon.ico
├── components
│ ├── directives
│ │ ├── dashboardLayouts
│ │ │ ├── SaveChangesModal.html
│ │ │ ├── SaveChangesModalCtrl.js
│ │ │ ├── dashboardLayouts.html
│ │ │ ├── SaveChangesModalCtrl.spec.js
│ │ │ └── dashboardLayouts.js
│ │ ├── dashboard
│ │ │ ├── widget-settings-template.html
│ │ │ ├── WidgetSettingsCtrl.js
│ │ │ ├── WidgetSettingsCtrl.spec.js
│ │ │ ├── altDashboard.html
│ │ │ ├── dashboard.html
│ │ │ └── dashboard.less
│ │ └── widget
│ │ │ ├── widget.js
│ │ │ └── widget.spec.js
│ └── models
│ │ ├── WidgetDataModel.js
│ │ ├── WidgetDefCollection.js
│ │ ├── WidgetDataModel.spec.js
│ │ ├── WidgetDefCollection.spec.js
│ │ ├── WidgetModel.js
│ │ ├── DashboardState.js
│ │ ├── DashboardState.spec.js
│ │ ├── LayoutStorage.js
│ │ └── WidgetModel.spec.js
├── index.html
└── 404.html
├── docs
├── scope.png
└── AngularJSDashboard.png
├── .gitignore
├── .travis.yml
├── test
├── runner.html
├── .jshintrc
└── spec
│ ├── SaveChangesModalCtrl.spec.js
│ ├── widgetSettingsCtrl.spec.js
│ ├── widgetDataModel.js
│ └── dashboardState.js
├── .editorconfig
├── circle.yml
├── gulpfile.js
├── Dockerfile
├── gulp
├── watch.js
├── e2e-tests.js
├── unit-tests.js
├── inject.js
├── build.js
├── styles.js
├── server.js
├── proxy.js
└── build-demo.js
├── e2e
├── main.po.js
└── main.spec.js
├── karma.conf.js
├── .jshintrc
├── protractor.conf.js
├── bower.json
├── package.json
├── CONTRIBUTING.md
├── Gruntfile.js
└── dist
└── malhar-angular-dashboard.css
/.bowerrc:
--------------------------------------------------------------------------------
1 | {
2 | "directory": "bower_components"
3 | }
4 |
--------------------------------------------------------------------------------
/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dtpublic/malhar-angular-dashboard/HEAD/favicon.ico
--------------------------------------------------------------------------------
/src/app/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | angular.module('dashboard', ['ui.bootstrap']);
4 |
--------------------------------------------------------------------------------
/docs/scope.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dtpublic/malhar-angular-dashboard/HEAD/docs/scope.png
--------------------------------------------------------------------------------
/src/person.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dtpublic/malhar-angular-dashboard/HEAD/src/person.png
--------------------------------------------------------------------------------
/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dtpublic/malhar-angular-dashboard/HEAD/src/favicon.ico
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | bower_components/
3 | .sass-cache/
4 | .tmp/
5 | dist/
6 | /demo/
7 | /coverage
--------------------------------------------------------------------------------
/docs/AngularJSDashboard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dtpublic/malhar-angular-dashboard/HEAD/docs/AngularJSDashboard.png
--------------------------------------------------------------------------------
/src/app/vendor.less:
--------------------------------------------------------------------------------
1 | @import '../../bower_components/bootstrap/less/bootstrap.less';
2 |
3 | @icon-font-path: '/fonts/';
4 |
--------------------------------------------------------------------------------
/.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'
--------------------------------------------------------------------------------
/src/app/template/resizable.html:
--------------------------------------------------------------------------------
1 |
7 |
You have {{layout.dashboard.unsavedChangeCount}} unsaved changes on this dashboard. Would you like to save them?
8 |
9 |
10 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/app/template/cartSummary.html:
--------------------------------------------------------------------------------
1 |
16 |
--------------------------------------------------------------------------------
/src/app/template/customSettingsTemplate.html:
--------------------------------------------------------------------------------
1 |
5 |
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/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/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/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/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/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/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/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/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: '