diff --git a/.bowerrc b/.bowerrc
new file mode 100644
index 0000000..69fad35
--- /dev/null
+++ b/.bowerrc
@@ -0,0 +1,3 @@
+{
+ "directory": "bower_components"
+}
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..e717f5e
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,13 @@
+# http://editorconfig.org
+root = true
+
+[*]
+indent_style = space
+indent_size = 2
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+
+[*.md]
+trim_trailing_whitespace = false
diff --git a/.gitignore b/.gitignore
index f021a04..cd2b4a8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,6 @@
-node_modules
-bower_components
-.idea
-.DS_Store
-/template/dashboard.js
-/coverage
-/demo_dist
\ No newline at end of file
+node_modules/
+bower_components/
+.sass-cache/
+.tmp/
+dist/
+/demo/
\ No newline at end of file
diff --git a/.jshintrc b/.jshintrc
index 3353f07..f0ad7c9 100644
--- a/.jshintrc
+++ b/.jshintrc
@@ -1,6 +1,5 @@
{
"node": true,
- "browser": true,
"esnext": true,
"bitwise": true,
"camelcase": true,
@@ -18,9 +17,17 @@
"strict": true,
"trailing": true,
"smarttabs": true,
+ "white": true,
+ "validthis": true,
"globals": {
"angular": false,
- "_": false,
- "jQuery": false
+ "inject": false,
+ "describe": false,
+ "it": false,
+ "before": false,
+ "beforeEach": false,
+ "after": false,
+ "afterEach": false,
+ "expect": false
}
}
diff --git a/.travis.yml b/.travis.yml
index a80b6e0..c82580f 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -2,5 +2,7 @@ language: node_js
node_js:
- '0.10'
before_script:
- - 'npm install -g bower grunt-cli'
+ - 'npm install -g bower gulp'
- 'bower install'
+script:
+ - 'gulp'
\ No newline at end of file
diff --git a/README.md b/README.md
index 7733c86..9b219ab 100644
--- a/README.md
+++ b/README.md
@@ -156,10 +156,10 @@ It is possible to use your own template for the dashboard and widget markup (rep
| defaultWidgets | Array | n/a | yes | List of objects where an object is `{ name: [NAME_OF_WIDGET_DEFINITION] }`. TODO: Allow just list of names.
| widgetButtons | Boolean | true | no | Display buttons for adding and removing widgets.
| storage | Object | null | no | If defined, this object should implement three methods: `setItem`, `getItem`, and `removeItem`. See the **Persistence** section below.
-| storageId | String | null | no (yes if `storage` is defined) | This is used as the first parameter passed to the three `storage` methods above. See the **Persistence** | section below.
+| storageId | String | null | no (yes if `storage` is defined) | This is used as the first parameter passed to the three `storage` methods above. See the **Persistence** section below.
| storageHash | String | '' | no | This is used to validate/invalidate loaded state. See the **Persistence** section below.
-| stringifyStorage | Boolean | true | no | If set to true, the dashboard state will be converted to a JSON string before being passed to `storage.setItem`. Likewise, it will be | passed through JSON.parse after being retrieved from `storage.getItem`. See the **Persistence** section below.
-| explicitSave | Boolean | false | no | The dashboard will not automatically save to storage for every change. Saves must instead be called explicitly using the `saveDashboard` | method that is attached to the option event upon initialization.
+| stringifyStorage | Boolean | true | no | If set to true, the dashboard state will be converted to a JSON string before being passed to `storage.setItem`. Likewise, it will be passed through JSON.parse after being retrieved from `storage.getItem`. See the **Persistence** section below.
+| explicitSave | Boolean | false | no | The dashboard will not automatically save to storage for every change. Saves must instead be called explicitly using the `saveDashboard` method that is attached to the option event upon initialization.
| sortableOptions | Object | n/a | no | Allows to specify the various [sortable options](http://api.jqueryui.com/sortable/#options) of the underlying jQuery UI Sortable.
| hideWidgetSettings | Boolean | false | no | If true, the cog button in the top right corner of each widget will not be present. |
| hideWidgetClose | Boolean | false | no | If true, the "x" button in the top right corner of each widget will not be present. |
@@ -176,24 +176,26 @@ You can think of Widget Definition Objects as a __class__ and the widgets on the
| key | type | default value | required | description
| ----------------- | ------ | ------------- | -------- | -----------
-| name | Object | n/a | true | Name of Widget Definition Object. If no `templateUrl`, `template`, or `directive` are on the Widget Definition | Object, this is assumed to be a directive name. In other words, the `directive` attribute is set to this value.
+| name | String | n/a | true | Name of Widget Definition Object. If no `templateUrl`, `template`, or `directive` are on the Widget Definition Object, this is assumed to be a directive name. In other words, the `directive` attribute is set to this value.
| title | String | n/a | false | Default title of widget instances
| attrs | Object | n/a | false | Map of attributes to add to the markup of the widget. Changes to these will be stored when using the `storage` option | (see **Persistence** section below).
| templateUrl | String | n/a | false | URL of template to use for widget content
| template | String | n/a | false | String template (ignored if templateUrl is present)
| directive | String | n/a | false | HTML-injectable directive name (eg. `"ng-show"`)
| dataModelType | Function or String | n/a | false | Constructor for the dataModel object, which provides data to the widget (see below for more information).
-| dataModelOptions | Object | n/a | false | Arbitrary values to supply to the dataModel. Available on dataModel instance as this.dataModelOptions. Serializable | values in this object will also be saved if `storage` is being used (see the **Persistence** section below).
+| dataModelOptions | Object | n/a | false | Arbitrary values to supply to the dataModel. Available on dataModel instance as this.dataModelOptions. Serializable values in this object will also be saved if `storage` is being used (see the **Persistence** section below).
| dataModelArgs | Object | n/a | false | Object to be passed to data model constructor function. This object is not serialized by default and if defined should be present in widget definitions.
| dataAttrName | String | n/a | false | Name of attribute to bind `widgetData` model
-| storageHash | String | n/a | false | This is analogous to the `storageHash` option on the dashboard, except at a widget-level instead of a dashboard-wide | level. This can be helpful if you would only like to invalidate stored state of one widget at a time instead of all widgets.
+| storageHash | String | n/a | false | This is analogous to the `storageHash` option on the dashboard, except at a widget-level instead of a dashboard-wide level. This can be helpful if you would only like to invalidate stored state of one widget at a time instead of all widgets.
| settingsModalOptions | Object | see below | no | Overrides same-named option in dashboard options for this widget. See the **Custom Widget Settings** section below. |
| size | Object | n/a | false | Widget size, e.g { width: '50%', height: '250px' } |
| style | Object | n/a | false | Widget style, e.g { float: 'right' } |
| enableVerticalResize | Boolean | true | false | Option to enable/disable vertical resize. Should be provided in "widgetDefinitions" since it is not serialized by default. |
| onSettingsClose | Function | see below | no | Overrides same-named option in dashboard options for this widget. See the **Custom Widget Settings** section below. |
| onSettingsDismiss | Function | see below | no | Overrides same-named option in dashboard options for this widget. See the **Custom Widget Settings** section below. |
+| serialize | Function | see below | no | Define this to override how this widget gets saved to storage. See **persistence** section below. |
+As of v1.0.0, you can also add arbitrary data to your WDOs and this data will be copied to your widget. Keep in mind though, that if you want to SAVE some of this arbitrary info with storage, you will need to implement your own serialize method that includes this (see the **persistence** section below).
### Widget Resize
@@ -263,7 +265,7 @@ This dashboard component offers a means to save the state of the user's dashboar
- widget titles
- any serializable data stored in `dataModelOptions` if the widget instance has a `ds` (instantiated `dataModelType`)
-There are three options you can specify in the `dashboardOptions` object relating to persistence:
+There are four options you can specify in the `dashboardOptions` object relating to persistence:
### `storage` (Object)
This object will be used by the dashboard to save its state. It should implement the following three methods:
@@ -284,6 +286,19 @@ This string will be stored along with the dashboard state. Then later, when stat
### `stringifyStorage` (Boolean)
By default (`stringifyStorage=true`), the dashboard will convert its state (a JavaScript Object) to a string using `JSON.stringify` before passing it to `storage.setItem`. Additionally, the dashboard will assume that `storage.getItem` will return a JSON string and try to parse it with `JSON.parse`. This works with `window.localStorage` nicely, since objects cannot be used as `value` in `localStorage.setItem(key, value)`. However, if you are implementing your own `storage` and would not like this stringification business, set `stringifyStorage` to `false`.
+There are also two options you can specify on WDOs that relate to persistence:
+
+### `storageHash` (String)
+Analogous to the `storageHash` option on the dashboard, except at a widget-level instead of a dashboard-wide level. This can be helpful if you would only like to invalidate stored state of one widget at a time instead of all widgets.
+
+### `serialize` (Function)
+This function will determine how the state of the widget gets saved. It takes no arguments and should return a `JSON.stringify`able object. The default implementation is as follows:
+```js
+serialize: function() {
+ return _.pick(this, ['title', 'name', 'style', 'size', 'dataModelOptions', 'attrs', 'storageHash']);
+}
+```
+See [_.pick](https://lodash.com/docs#pick) for more details. The most common use-case for this would be to add another key to this list, or remove a key.
Custom Widget Settings
----------------------
@@ -377,7 +392,7 @@ key | type | default value | required | description
--- | ---- | ------------- | -------- | -----------
widgetDefinitions | Array | n/a | yes | Same as in `dashboardOptions`
lockDefaultLayouts | Boolean| false | no | `true` to lock default layouts (prevent from removing and renaming), layout lock can also be controlled with `locked` layout property
- defaultLayouts | Array | n/a | yes | List of objects where an object is `{ title: [STRING_LAYOUT_TITLE], active: [BOOLEAN_ACTIVE_STATE], locked: [BOOLEAN], defaultWidgets: [ARRAY_DEFAULT_WIDGETS] }`. Note that `defaultWidgets` is the same as in `dashboardOptions`.
+ defaultLayouts | Array | n/a | yes | List of objects where an object is `{ title: [STRING_LAYOUT_TITLE], active: [BOOLEAN_ACTIVE_STATE], locked: [BOOLEAN], defaultWidgets: [ARRAY_DEFAULT_WIDGETS], widgetDefinitions: [ARRAY_OF_WIDGET_DEFS] }`. Note that `defaultWidgets` is the same as in `dashboardOptions`. Also note that the `widgetDefinitions` array is optional on individual default layouts. By default, layouts will use the `widgetDefintions` from the dashboardLayouts options object. See issue #83.
widgetButtons | Boolean | true | no | Same as in `dashboardOptions`
storage | Object | null | no | Same as in `dashboardOptions`, only the saved objects look like: `{ layouts: [...], states: {...}, storageHash: '' }`
storageId | String | null | no (yes if `storage` is defined) | This is used as the first parameter passed to the three `storage` methods `setItem`, `getItem`, `removeItem`. See the **Persistence** section above.
diff --git a/bower.json b/bower.json
index c14ca71..216d8eb 100644
--- a/bower.json
+++ b/bower.json
@@ -1,20 +1,28 @@
{
"name": "malhar-angular-dashboard",
- "main": "./dist/angular-ui-dashboard.js",
- "version": "0.8.2",
- "license": "Apache License, v2.0",
+ "version": "1.0.0",
"dependencies": {
- "jquery": "~2.0.3",
+ "bootstrap": "~3.3.1",
+ "angular-bootstrap": "0.12.x",
"angular": "~1.3",
- "angular-bootstrap": "~0.11.0",
- "angular-ui-sortable": "~0.13.1",
- "jquery-ui": "~1.11.0",
- "lodash": "~2.4.1"
+ "angular-ui-sortable": "~0.13.3",
+ "lodash": "~3.1"
},
+ "main": [
+ "dist/malhar-angular-dashboard.css",
+ "dist/malhar-angular-dashboard.js"
+ ],
"devDependencies": {
- "angular-route": "~1.3",
"angular-mocks": "~1.3",
- "angular-markdown-directive": "~0.3.0",
- "bootstrap": "~3.2.0"
+ "angular-route": "~1.3",
+ "angular-markdown-directive": "~0.3.1"
+ },
+ "overrides": {
+ "showdown": {
+ "main": "src/showdown.js"
+ }
+ },
+ "resolutions": {
+ "angular": "~1.3"
}
}
diff --git a/demo/index.html b/demo/index.html
deleted file mode 100644
index 2d407c4..0000000
--- a/demo/index.html
+++ /dev/null
@@ -1,74 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/dist/malhar-angular-dashboard.css b/dist/malhar-angular-dashboard.css
new file mode 100644
index 0000000..58e4b77
--- /dev/null
+++ b/dist/malhar-angular-dashboard.css
@@ -0,0 +1,80 @@
+.dashboard-widget-area {
+ margin: 10px 0 30px;
+ min-height: 200px;
+}
+.widget-container {
+ float: left;
+ display: inline-block;
+ width: 33%;
+ padding-bottom: 1em;
+}
+.widget {
+ margin: 0 1em 0 0;
+ background-color: white;
+ border: 2px solid #444;
+ border-radius: 5px;
+ position: relative;
+ height: 100%;
+}
+.widget-header {
+ overflow: hidden;
+}
+.widget-header .label {
+ display: inline-block;
+ vertical-align: middle;
+}
+.widget-header .glyphicon {
+ cursor: pointer;
+ float: right;
+ opacity: 0.5;
+ margin-left: 5px;
+}
+.widget-header .glyphicon:hover {
+ opacity: 1;
+}
+.widget-header .widget-title {
+ vertical-align: middle;
+}
+.widget-header form.widget-title {
+ display: inline;
+}
+.widget-header form.widget-title input.form-control {
+ width: auto;
+ display: inline-block;
+}
+.widget-content {
+ overflow: hidden;
+}
+.widget .widget-ew-resizer {
+ position: absolute;
+ width: 5px;
+ right: -2px;
+ height: 100%;
+ top: 0;
+ cursor: ew-resize;
+}
+.widget .widget-s-resizer {
+ cursor: ns-resize;
+ height: 5px;
+ width: 100%;
+ bottom: -7px;
+ left: 0;
+}
+.widget .widget-resizer-marquee {
+ box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.5);
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: 2;
+}
+.remove-layout-icon {
+ vertical-align: text-top;
+ cursor: pointer;
+ opacity: 0.3;
+}
+.remove-layout-icon:hover {
+ opacity: 1;
+}
+.layout-title {
+ display: inline-block;
+}
diff --git a/dist/angular-ui-dashboard.js b/dist/malhar-angular-dashboard.js
similarity index 74%
rename from dist/angular-ui-dashboard.js
rename to dist/malhar-angular-dashboard.js
index 0e1fe52..faaad9d 100644
--- a/dist/angular-ui-dashboard.js
+++ b/dist/malhar-angular-dashboard.js
@@ -18,11 +18,21 @@
angular.module('ui.dashboard', ['ui.bootstrap', 'ui.sortable']);
angular.module('ui.dashboard')
+
.directive('dashboard', ['WidgetModel', 'WidgetDefCollection', '$modal', 'DashboardState', '$log', function (WidgetModel, WidgetDefCollection, $modal, DashboardState, $log) {
+
+ var sortableDefaults = {
+ stop: function () {
+ scope.saveDashboard();
+ },
+ handle: '.widget-header',
+ distance: 5
+ };
+
return {
restrict: 'A',
templateUrl: function(element, attr) {
- return attr.templateUrl ? attr.templateUrl : 'template/dashboard.html';
+ return attr.templateUrl ? attr.templateUrl : 'components/directives/dashboard/dashboard.html';
},
scope: true,
@@ -33,7 +43,7 @@ angular.module('ui.dashboard')
hideWidgetSettings: false,
hideWidgetClose: false,
settingsModalOptions: {
- templateUrl: 'template/widget-settings-template.html',
+ templateUrl: 'components/directives/dashboard/widget-settings-template.html',
controller: 'WidgetSettingsCtrl'
},
onSettingsClose: function(result, widget) { // NOTE: dashboard scope is also passed as 3rd argument
@@ -44,18 +54,6 @@ angular.module('ui.dashboard')
}
};
- // from dashboard="options"
- // scope.options = scope.$eval(attrs.dashboard);
-
- // extend default settingsModalOptions
- // scope.options.settingsModalOptions = scope.options.settingsModalOptions || {};
-
- // extend options with defaults
- // angular.extend(defaults.settingsModalOptions, scope.options.settingsModalOptions);
- // angular.extend(scope.options.settingsModalOptions, defaults.settingsModalOptions);
- // angular.extend(defaults, scope.options);
- // angular.extend(scope.options, defaults);
-
// from dashboard="options"
scope.options = scope.$eval(attrs.dashboard);
@@ -71,15 +69,7 @@ angular.module('ui.dashboard')
// Shallow options
_.defaults(scope.options, defaults);
- // jQuery.extend(true, defaults, scope.options);
- // jQuery.extend(scope.options, defaults);
-
- var sortableDefaults = {
- stop: function () {
- scope.saveDashboard();
- },
- handle: '.widget-header'
- };
+ // sortable options
scope.sortableOptions = angular.extend({}, sortableDefaults, scope.options.sortableOptions || {});
}],
@@ -88,7 +78,6 @@ angular.module('ui.dashboard')
// Save default widget config for reset
scope.defaultWidgets = scope.options.defaultWidgets;
- //scope.widgetDefs = scope.options.widgetDefinitions;
scope.widgetDefs = new WidgetDefCollection(scope.options.widgetDefinitions);
var count = 1;
@@ -106,6 +95,13 @@ angular.module('ui.dashboard')
* @param {Object} widgetToInstantiate The definition object of the widget to be instantiated
*/
scope.addWidget = function (widgetToInstantiate, doNotSave) {
+
+ if (typeof widgetToInstantiate === 'string') {
+ widgetToInstantiate = {
+ name: widgetToInstantiate
+ };
+ }
+
var defaultWidgetDefinition = scope.widgetDefs.getByName(widgetToInstantiate.name);
if (!defaultWidgetDefinition) {
throw 'Widget ' + widgetToInstantiate.name + ' is not found.';
@@ -113,22 +109,14 @@ angular.module('ui.dashboard')
// Determine the title for the new widget
var title;
- if (widgetToInstantiate.title) {
- title = widgetToInstantiate.title;
- } else if (defaultWidgetDefinition.title) {
- title = defaultWidgetDefinition.title;
- } else {
- title = 'Widget ' + count++;
+ if (!widgetToInstantiate.title && !defaultWidgetDefinition.title) {
+ widgetToInstantiate.title = 'Widget ' + count++;
}
- // Deep extend a new object for instantiation
- widgetToInstantiate = jQuery.extend(true, {}, defaultWidgetDefinition, widgetToInstantiate);
-
// Instantiation
- var widget = new WidgetModel(widgetToInstantiate, {
- title: title
- });
+ var widget = new WidgetModel(defaultWidgetDefinition, widgetToInstantiate);
+ // Add to the widgets array
scope.widgets.push(widget);
if (!doNotSave) {
scope.saveDashboard();
@@ -301,153 +289,11 @@ angular.module('ui.dashboard')
};
}]);
-/*
- * Copyright (c) 2014 DataTorrent, Inc. ALL Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-'use strict';
-
-angular.module('ui.dashboard')
- .directive('dashboardLayouts', ['LayoutStorage', '$timeout', '$modal',
- function(LayoutStorage, $timeout, $modal) {
- return {
- scope: true,
- templateUrl: function(element, attr) {
- return attr.templateUrl ? attr.templateUrl : 'template/dashboard-layouts.html';
- },
- link: function(scope, element, attrs) {
-
- scope.options = scope.$eval(attrs.dashboardLayouts);
-
- var layoutStorage = new LayoutStorage(scope.options);
-
- scope.layouts = layoutStorage.layouts;
-
- scope.createNewLayout = function() {
- var newLayout = {
- title: 'Custom',
- defaultWidgets: scope.options.defaultWidgets || []
- };
- layoutStorage.add(newLayout);
- scope.makeLayoutActive(newLayout);
- layoutStorage.save();
- return newLayout;
- };
-
- scope.removeLayout = function(layout) {
- layoutStorage.remove(layout);
- layoutStorage.save();
- };
-
- scope.makeLayoutActive = function(layout) {
-
- var current = layoutStorage.getActiveLayout();
-
- if (current && current.dashboard.unsavedChangeCount) {
- var modalInstance = $modal.open({
- templateUrl: 'template/save-changes-modal.html',
- resolve: {
- layout: function() {
- return layout;
- }
- },
- controller: 'SaveChangesModalCtrl'
- });
-
- // Set resolve and reject callbacks for the result promise
- modalInstance.result.then(
- function() {
- current.dashboard.saveDashboard();
- scope._makeLayoutActive(layout);
- },
- function() {
- scope._makeLayoutActive(layout);
- }
- );
- } else {
- scope._makeLayoutActive(layout);
- }
-
- };
-
- scope._makeLayoutActive = function(layout) {
- angular.forEach(scope.layouts, function(l) {
- if (l !== layout) {
- l.active = false;
- } else {
- l.active = true;
- }
- });
- layoutStorage.save();
- };
-
- scope.isActive = function(layout) {
- return !!layout.active;
- };
-
- scope.editTitle = function(layout) {
- if (layout.locked) {
- return;
- }
-
- var input = element.find('input[data-layout="' + layout.id + '"]');
- layout.editingTitle = true;
-
- $timeout(function() {
- input.focus()[0].setSelectionRange(0, 9999);
- });
- };
-
- // saves whatever is in the title input as the new title
- scope.saveTitleEdit = function(layout) {
- layout.editingTitle = false;
- layoutStorage.save();
- };
-
- scope.options.saveLayouts = function() {
- layoutStorage.save(true);
- };
- scope.options.addWidget = function() {
- var layout = layoutStorage.getActiveLayout();
- if (layout) {
- layout.dashboard.addWidget.apply(layout.dashboard, arguments);
- }
- };
- scope.options.loadWidgets = function() {
- var layout = layoutStorage.getActiveLayout();
- if (layout) {
- layout.dashboard.loadWidgets.apply(layout.dashboard, arguments);
- }
- };
- scope.options.saveDashboard = function() {
- var layout = layoutStorage.getActiveLayout();
- if (layout) {
- layout.dashboard.saveDashboard.apply(layout.dashboard, arguments);
- }
- };
-
- var sortableDefaults = {
- stop: function() {
- scope.options.saveLayouts();
- },
- };
- scope.sortableOptions = angular.extend({}, sortableDefaults, scope.options.sortableOptions || {});
- }
- };
- }
- ]);
+angular.module("ui.dashboard").run(["$templateCache", function($templateCache) {$templateCache.put("components/directives/dashboard/altDashboard.html","\n");
+$templateCache.put("components/directives/dashboard/dashboard.html","");
+$templateCache.put("components/directives/dashboard/widget-settings-template.html","\n\n\n\n");
+$templateCache.put("components/directives/dashboardLayouts/SaveChangesModal.html","\n\n\n
You have {{layout.dashboard.unsavedChangeCount}} unsaved changes on this dashboard. Would you like to save them?
\n
\n\n");
+$templateCache.put("components/directives/dashboardLayouts/dashboardLayouts.html","\n
");}]);
/*
* Copyright (c) 2014 DataTorrent, Inc. ALL Rights Reserved.
*
@@ -532,429 +378,437 @@ angular.module('ui.dashboard')
'use strict';
angular.module('ui.dashboard')
- .factory('LayoutStorage', function() {
-
- var noopStorage = {
- setItem: function() {
+ .controller('DashboardWidgetCtrl', ['$scope', '$element', '$compile', '$window', '$timeout',
+ function($scope, $element, $compile, $window, $timeout) {
- },
- getItem: function() {
+ $scope.status = {
+ isopen: false
+ };
- },
- removeItem: function() {
+ // Fills "container" with compiled view
+ $scope.makeTemplateString = function() {
- }
- };
+ var widget = $scope.widget;
-
+ // First, build template string
+ var templateString = '';
- function LayoutStorage(options) {
+ if (widget.templateUrl) {
- var defaults = {
- storage: noopStorage,
- storageHash: '',
- stringifyStorage: true
- };
+ // Use ng-include for templateUrl
+ templateString = '
';
- angular.extend(defaults, options);
- angular.extend(options, defaults);
+ } else if (widget.template) {
- this.id = options.storageId;
- this.storage = options.storage;
- this.storageHash = options.storageHash;
- this.stringifyStorage = options.stringifyStorage;
- this.widgetDefinitions = options.widgetDefinitions;
- this.defaultLayouts = options.defaultLayouts;
- this.lockDefaultLayouts = options.lockDefaultLayouts;
- this.widgetButtons = options.widgetButtons;
- this.explicitSave = options.explicitSave;
- this.defaultWidgets = options.defaultWidgets;
- this.settingsModalOptions = options.settingsModalOptions;
- this.onSettingsClose = options.onSettingsClose;
- this.onSettingsDismiss = options.onSettingsDismiss;
- this.options = options;
- this.options.unsavedChangeCount = 0;
+ // Direct string template
+ templateString = widget.template;
- this.layouts = [];
- this.states = {};
- this.load();
- this._ensureActiveLayout();
- }
+ } else {
- LayoutStorage.prototype = {
+ // Assume attribute directive
+ templateString = '= 0) {
- this.layouts.splice(index, 1);
- delete this.states[layout.id];
+ // Check for specified attributes
+ if (widget.attrs) {
- // check for active
- if (layout.active && this.layouts.length) {
- var nextActive = index > 0 ? index - 1 : 0;
- this.layouts[nextActive].active = true;
+ // First check directive name attr
+ if (widget.attrs[widget.directive]) {
+ templateString += '="' + widget.attrs[widget.directive] + '"';
+ }
+
+ // Add attributes
+ _.each(widget.attrs, function(value, attr) {
+
+ // make sure we aren't reusing directive attr
+ if (attr !== widget.directive) {
+ templateString += ' ' + attr + '="' + value + '"';
+ }
+
+ });
}
+ templateString += '>
';
}
- },
+ return templateString;
+ };
- save: function() {
+ $scope.grabResizer = function(e) {
- var state = {
- layouts: this._serializeLayouts(),
- states: this.states,
- storageHash: this.storageHash
- };
+ var widget = $scope.widget;
+ var widgetElm = $element.find('.widget');
- if (this.stringifyStorage) {
- state = JSON.stringify(state);
+ // ignore middle- and right-click
+ if (e.which !== 1) {
+ return;
}
- this.storage.setItem(this.id, state);
- this.options.unsavedChangeCount = 0;
- },
+ e.stopPropagation();
+ e.originalEvent.preventDefault();
- load: function() {
+ // get the starting horizontal position
+ var initX = e.clientX;
+ // console.log('initX', initX);
- var serialized = this.storage.getItem(this.id);
+ // Get the current width of the widget and dashboard
+ var pixelWidth = widgetElm.width();
+ var pixelHeight = widgetElm.height();
+ var widgetStyleWidth = widget.containerStyle.width;
+ var widthUnits = widget.widthUnits;
+ var unitWidth = parseFloat(widgetStyleWidth);
- this.clear();
+ // create marquee element for resize action
+ var $marquee = angular.element('
');
+ widgetElm.append($marquee);
- if (serialized) {
- // check for promise
- if (angular.isObject(serialized) && angular.isFunction(serialized.then)) {
- this._handleAsyncLoad(serialized);
- } else {
- this._handleSyncLoad(serialized);
- }
- } else {
- this._addDefaultLayouts();
- }
- },
+ // determine the unit/pixel ratio
+ var transformMultiplier = unitWidth / pixelWidth;
- clear: function() {
- this.layouts = [];
- this.states = {};
- },
+ // updates marquee with preview of new width
+ var mousemove = function(e) {
+ var curX = e.clientX;
+ var pixelChange = curX - initX;
+ var newWidth = pixelWidth + pixelChange;
+ $marquee.css('width', newWidth + 'px');
+ };
- setItem: function(id, value) {
- this.states[id] = value;
- this.save();
- },
+ // sets new widget width on mouseup
+ var mouseup = function(e) {
+ // remove listener and marquee
+ jQuery($window).off('mousemove', mousemove);
+ $marquee.remove();
- getItem: function(id) {
- return this.states[id];
- },
+ // calculate change in units
+ var curX = e.clientX;
+ var pixelChange = curX - initX;
+ var unitChange = Math.round(pixelChange * transformMultiplier * 100) / 100;
- removeItem: function(id) {
- delete this.states[id];
- this.save();
- },
+ // add to initial unit width
+ var newWidth = unitWidth * 1 + unitChange;
+ widget.setWidth(newWidth, widthUnits);
+ $scope.$emit('widgetChanged', widget);
+ $scope.$apply();
+ $scope.$broadcast('widgetResized', {
+ width: newWidth
+ });
+ };
- getActiveLayout: function() {
- var len = this.layouts.length;
- for (var i = 0; i < len; i++) {
- var layout = this.layouts[i];
- if (layout.active) {
- return layout;
- }
+ jQuery($window).on('mousemove', mousemove).one('mouseup', mouseup);
+ };
+
+ //TODO refactor
+ $scope.grabSouthResizer = function(e) {
+ var widgetElm = $element.find('.widget');
+
+ // ignore middle- and right-click
+ if (e.which !== 1) {
+ return;
}
- return false;
- },
- _addDefaultLayouts: function() {
- var self = this;
- var defaults = this.lockDefaultLayouts ? { locked: true } : {};
- angular.forEach(this.defaultLayouts, function(layout) {
- self.add(angular.extend(_.clone(defaults), layout));
- });
- },
+ e.stopPropagation();
+ e.originalEvent.preventDefault();
- _serializeLayouts: function() {
- var result = [];
- angular.forEach(this.layouts, function(l) {
- result.push({
- title: l.title,
- id: l.id,
- active: l.active,
- locked: l.locked,
- defaultWidgets: l.dashboard.defaultWidgets
- });
- });
- return result;
- },
+ // get the starting horizontal position
+ var initY = e.clientY;
+ // console.log('initX', initX);
- _handleSyncLoad: function(serialized) {
-
- var deserialized;
+ // Get the current width of the widget and dashboard
+ var pixelWidth = widgetElm.width();
+ var pixelHeight = widgetElm.height();
- if (this.stringifyStorage) {
- try {
+ // create marquee element for resize action
+ var $marquee = angular.element('
');
+ widgetElm.append($marquee);
- deserialized = JSON.parse(serialized);
+ // updates marquee with preview of new height
+ var mousemove = function(e) {
+ var curY = e.clientY;
+ var pixelChange = curY - initY;
+ var newHeight = pixelHeight + pixelChange;
+ $marquee.css('height', newHeight + 'px');
+ };
- } catch (e) {
- this._addDefaultLayouts();
- return;
- }
- } else {
+ // sets new widget width on mouseup
+ var mouseup = function(e) {
+ // remove listener and marquee
+ jQuery($window).off('mousemove', mousemove);
+ $marquee.remove();
- deserialized = serialized;
+ // calculate height change
+ var curY = e.clientY;
+ var pixelChange = curY - initY;
- }
+ //var widgetContainer = widgetElm.parent(); // widget container responsible for holding widget width and height
+ var widgetContainer = widgetElm.find('.widget-content');
- if (this.storageHash !== deserialized.storageHash) {
- this._addDefaultLayouts();
- return;
- }
- this.states = deserialized.states;
- this.add(deserialized.layouts);
- },
+ var diff = pixelChange;
+ var height = parseInt(widgetContainer.css('height'), 10);
+ var newHeight = (height + diff);
- _handleAsyncLoad: function(promise) {
- var self = this;
- promise.then(
- angular.bind(self, this._handleSyncLoad),
- angular.bind(self, this._addDefaultLayouts)
- );
- },
+ //$scope.widget.style.height = newHeight + 'px';
- _ensureActiveLayout: function() {
- for (var i = 0; i < this.layouts.length; i++) {
- var layout = this.layouts[i];
- if (layout.active) {
- return;
- }
- }
- if (this.layouts[0]) {
- this.layouts[0].active = true;
- }
- },
+ $scope.widget.setHeight(newHeight + 'px');
- _getLayoutId: function(layout) {
- if (layout.id) {
- return layout.id;
- }
- var max = 0;
- for (var i = 0; i < this.layouts.length; i++) {
- var id = this.layouts[i].id;
- max = Math.max(max, id * 1);
- }
- return max + 1;
- }
+ $scope.$emit('widgetChanged', $scope.widget);
+ $scope.$apply(); // make AngularJS to apply style changes
- };
- return LayoutStorage;
- });
-/*
- * Copyright (c) 2014 DataTorrent, Inc. ALL Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
+ $scope.$broadcast('widgetResized', {
+ height: newHeight
+ });
+ };
-'use strict';
+ jQuery($window).on('mousemove', mousemove).one('mouseup', mouseup);
+ };
-angular.module('ui.dashboard')
- .factory('DashboardState', ['$log', '$q', function ($log, $q) {
- function DashboardState(storage, id, hash, widgetDefinitions, stringify) {
- this.storage = storage;
- this.id = id;
- this.hash = hash;
- this.widgetDefinitions = widgetDefinitions;
- this.stringify = stringify;
- }
+ // replaces widget title with input
+ $scope.editTitle = function(widget) {
+ var widgetElm = $element.find('.widget');
+ widget.editingTitle = true;
+ // HACK: get the input to focus after being displayed.
+ $timeout(function() {
+ widgetElm.find('form.widget-title input:eq(0)').focus()[0].setSelectionRange(0, 9999);
+ });
+ };
- DashboardState.prototype = {
- /**
- * Takes array of widget instance objects, serializes,
- * and saves state.
- *
- * @param {Array} widgets scope.widgets from dashboard directive
- * @return {Boolean} true on success, false on failure
- */
- save: function (widgets) {
-
- if (!this.storage) {
- return true;
- }
+ // saves whatever is in the title input as the new title
+ $scope.saveTitleEdit = function(widget) {
+ widget.editingTitle = false;
+ $scope.$emit('widgetChanged', widget);
+ };
- var serialized = _.map(widgets, function (widget) {
- var widgetObject = {
- title: widget.title,
- name: widget.name,
- style: widget.style,
- size: widget.size,
- dataModelOptions: widget.dataModelOptions,
- storageHash: widget.storageHash,
- attrs: widget.attrs
- };
+ $scope.compileTemplate = function() {
+ var container = $scope.findWidgetContainer($element);
+ var templateString = $scope.makeTemplateString();
+ var widgetElement = angular.element(templateString);
- return widgetObject;
- });
+ container.empty();
+ container.append(widgetElement);
+ $compile(widgetElement)($scope);
+ };
- var item = { widgets: serialized, hash: this.hash };
+ $scope.findWidgetContainer = function(element) {
+ // widget placeholder is the first (and only) child of .widget-content
+ return element.find('.widget-content');
+ };
+ }
+ ]);
+/*
+ * Copyright (c) 2014 DataTorrent, Inc. ALL Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
- if (this.stringify) {
- item = JSON.stringify(item);
- }
+'use strict';
- this.storage.setItem(this.id, item);
- return true;
- },
+angular.module('ui.dashboard')
+ .directive('dashboardLayouts', ['LayoutStorage', '$timeout', '$modal',
+ function(LayoutStorage, $timeout, $modal) {
+ return {
+ scope: true,
+ templateUrl: function(element, attr) {
+ return attr.templateUrl ? attr.templateUrl : 'components/directives/dashboardLayouts/dashboardLayouts.html';
+ },
+ link: function(scope, element, attrs) {
- /**
- * Loads dashboard state from the storage object.
- * Can handle a synchronous response or a promise.
- *
- * @return {Array|Promise} Array of widget definitions or a promise
- */
- load: function () {
+ scope.options = scope.$eval(attrs.dashboardLayouts);
- if (!this.storage) {
- return null;
- }
+ var layoutStorage = new LayoutStorage(scope.options);
- var serialized;
+ scope.layouts = layoutStorage.layouts;
- // try loading storage item
- serialized = this.storage.getItem( this.id );
+ scope.createNewLayout = function() {
+ var newLayout = {
+ title: 'Custom',
+ defaultWidgets: scope.options.defaultWidgets || []
+ };
+ layoutStorage.add(newLayout);
+ scope.makeLayoutActive(newLayout);
+ layoutStorage.save();
+ return newLayout;
+ };
- if (serialized) {
- // check for promise
- if (angular.isObject(serialized) && angular.isFunction(serialized.then)) {
- return this._handleAsyncLoad(serialized);
- }
- // otherwise handle synchronous load
- return this._handleSyncLoad(serialized);
- } else {
- return null;
- }
- },
+ scope.removeLayout = function(layout) {
+ layoutStorage.remove(layout);
+ layoutStorage.save();
+ };
- _handleSyncLoad: function(serialized) {
+ scope.makeLayoutActive = function(layout) {
- var deserialized, result = [];
+ var current = layoutStorage.getActiveLayout();
- if (!serialized) {
- return null;
- }
+ if (current && current.dashboard.unsavedChangeCount) {
+ var modalInstance = $modal.open({
+ templateUrl: 'template/SaveChangesModal.html',
+ resolve: {
+ layout: function() {
+ return layout;
+ }
+ },
+ controller: 'SaveChangesModalCtrl'
+ });
- if (this.stringify) {
- try { // to deserialize the string
+ // Set resolve and reject callbacks for the result promise
+ modalInstance.result.then(
+ function() {
+ current.dashboard.saveDashboard();
+ scope._makeLayoutActive(layout);
+ },
+ function() {
+ scope._makeLayoutActive(layout);
+ }
+ );
+ } else {
+ scope._makeLayoutActive(layout);
+ }
- deserialized = JSON.parse(serialized);
+ };
- } catch (e) {
+ scope._makeLayoutActive = function(layout) {
+ angular.forEach(scope.layouts, function(l) {
+ if (l !== layout) {
+ l.active = false;
+ } else {
+ l.active = true;
+ }
+ });
+ layoutStorage.save();
+ };
- // bad JSON, log a warning and return
- $log.warn('Serialized dashboard state was malformed and could not be parsed: ', serialized);
- return null;
+ scope.isActive = function(layout) {
+ return !!layout.active;
+ };
- }
- }
- else {
- deserialized = serialized;
- }
+ scope.editTitle = function(layout) {
+ if (layout.locked) {
+ return;
+ }
- // check hash against current hash
- if (deserialized.hash !== this.hash) {
+ var input = element.find('input[data-layout="' + layout.id + '"]');
+ layout.editingTitle = true;
- $log.info('Serialized dashboard from storage was stale (old hash: ' + deserialized.hash + ', new hash: ' + this.hash + ')');
- this.storage.removeItem(this.id);
- return null;
+ $timeout(function() {
+ input.focus()[0].setSelectionRange(0, 9999);
+ });
+ };
- }
+ // saves whatever is in the title input as the new title
+ scope.saveTitleEdit = function(layout) {
+ layout.editingTitle = false;
+ layoutStorage.save();
+ };
- // Cache widgets
- var savedWidgetDefs = deserialized.widgets;
+ scope.options.saveLayouts = function() {
+ layoutStorage.save(true);
+ };
+ scope.options.addWidget = function() {
+ var layout = layoutStorage.getActiveLayout();
+ if (layout) {
+ layout.dashboard.addWidget.apply(layout.dashboard, arguments);
+ }
+ };
+ scope.options.loadWidgets = function() {
+ var layout = layoutStorage.getActiveLayout();
+ if (layout) {
+ layout.dashboard.loadWidgets.apply(layout.dashboard, arguments);
+ }
+ };
+ scope.options.saveDashboard = function() {
+ var layout = layoutStorage.getActiveLayout();
+ if (layout) {
+ layout.dashboard.saveDashboard.apply(layout.dashboard, arguments);
+ }
+ };
- // instantiate widgets from stored data
- for (var i = 0; i < savedWidgetDefs.length; i++) {
+ var sortableDefaults = {
+ stop: function() {
+ scope.options.saveLayouts();
+ },
+ distance: 5
+ };
+ scope.sortableOptions = angular.extend({}, sortableDefaults, scope.options.sortableOptions || {});
+ }
+ };
+ }
+ ]);
+/*
+ * Copyright (c) 2014 DataTorrent, Inc. ALL Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
- // deserialized object
- var savedWidgetDef = savedWidgetDefs[i];
+'use strict';
- // widget definition to use
- var widgetDefinition = this.widgetDefinitions.getByName(savedWidgetDef.name);
+angular.module('ui.dashboard')
+ .controller('SaveChangesModalCtrl', ['$scope', '$modalInstance', 'layout', function ($scope, $modalInstance, layout) {
+
+ // add layout to scope
+ $scope.layout = layout;
- // check for no widget
- if (!widgetDefinition) {
- // no widget definition found, remove and return false
- $log.warn('Widget with name "' + savedWidgetDef.name + '" was not found in given widget definition objects');
- continue;
- }
+ $scope.ok = function () {
+ $modalInstance.close();
+ };
- // check widget-specific storageHash
- if (widgetDefinition.hasOwnProperty('storageHash') && widgetDefinition.storageHash !== savedWidgetDef.storageHash) {
- // widget definition was found, but storageHash was stale, removing storage
- $log.info('Widget Definition Object with name "' + savedWidgetDef.name + '" was found ' +
- 'but the storageHash property on the widget definition is different from that on the ' +
- 'serialized widget loaded from storage. hash from storage: "' + savedWidgetDef.storageHash + '"' +
- ', hash from WDO: "' + widgetDefinition.storageHash + '"');
- continue;
- }
+ $scope.cancel = function () {
+ $modalInstance.dismiss();
+ };
+ }]);
+/*
+ * Copyright (c) 2014 DataTorrent, Inc. ALL Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
- // push instantiated widget to result array
- result.push(savedWidgetDef);
- }
+'use strict';
- return result;
- },
+angular.module('ui.dashboard')
+ .controller('WidgetSettingsCtrl', ['$scope', '$modalInstance', 'widget', function ($scope, $modalInstance, widget) {
+ // add widget to scope
+ $scope.widget = widget;
- _handleAsyncLoad: function(promise) {
- var self = this;
- var deferred = $q.defer();
- promise.then(
- // success
- function(res) {
- var result = self._handleSyncLoad(res);
- if (result) {
- deferred.resolve(result);
- } else {
- deferred.reject(result);
- }
- },
- // failure
- function(res) {
- deferred.reject(res);
- }
- );
+ // set up result object
+ $scope.result = jQuery.extend(true, {}, widget);
- return deferred.promise;
- }
+ $scope.ok = function () {
+ $modalInstance.close($scope.result);
+ };
+ $scope.cancel = function () {
+ $modalInstance.dismiss('cancel');
};
- return DashboardState;
}]);
/*
* Copyright (c) 2014 DataTorrent, Inc. ALL Rights Reserved.
@@ -975,32 +829,94 @@ angular.module('ui.dashboard')
'use strict';
angular.module('ui.dashboard')
- .factory('WidgetDataModel', function () {
- function WidgetDataModel() {
+ .factory('WidgetModel', ["$log", function ($log) {
+
+ function defaults() {
+ return {
+ title: 'Widget',
+ style: {},
+ size: {},
+ enableVerticalResize: true,
+ containerStyle: { width: '33%' }, // default width
+ contentStyle: {}
+ };
+ };
+
+ // constructor for widget model instances
+ function WidgetModel(widgetDefinition, overrides) {
+
+ // Extend this with the widget definition object with overrides merged in (deep extended).
+ angular.extend(this, defaults(), _.merge(angular.copy(widgetDefinition), overrides));
+
+ this.updateContainerStyle(this.style);
+
+ if (!this.templateUrl && !this.template && !this.directive) {
+ this.directive = widgetDefinition.name;
+ }
+
+ if (this.size && _.has(this.size, 'height')) {
+ this.setHeight(this.size.height);
+ }
+
+ if (this.style && _.has(this.style, 'width')) { //TODO deprecate style attribute
+ this.setWidth(this.style.width);
+ }
+
+ if (this.size && _.has(this.size, 'width')) {
+ this.setWidth(this.size.width);
+ }
}
- WidgetDataModel.prototype = {
- setup: function (widget, scope) {
- this.dataAttrName = widget.dataAttrName;
- this.dataModelOptions = widget.dataModelOptions;
- this.widgetScope = scope;
+ WidgetModel.prototype = {
+ // sets the width (and widthUnits)
+ setWidth: function (width, units) {
+ width = width.toString();
+ units = units || width.replace(/^[-\.\d]+/, '') || '%';
+
+ this.widthUnits = units;
+ width = parseFloat(width);
+
+ if (width < 0 || isNaN(width)) {
+ $log.warn('malhar-angular-dashboard: setWidth was called when width was ' + width);
+ return false;
+ }
+
+ if (units === '%') {
+ width = Math.min(100, width);
+ width = Math.max(0, width);
+ }
+
+ this.containerStyle.width = width + '' + units;
+
+ this.updateSize(this.containerStyle);
+
+ return true;
},
- updateScope: function (data) {
- this.widgetScope.widgetData = data;
+ setHeight: function (height) {
+ this.contentStyle.height = height;
+ this.updateSize(this.contentStyle);
},
- init: function () {
- // to be overridden by subclasses
+ setStyle: function (style) {
+ this.style = style;
+ this.updateContainerStyle(style);
},
- destroy: function () {
- // to be overridden by subclasses
+ updateSize: function (size) {
+ angular.extend(this.size, size);
+ },
+
+ updateContainerStyle: function (style) {
+ angular.extend(this.containerStyle, style);
+ },
+ serialize: function() {
+ return _.pick(this, ['title', 'name', 'style', 'size', 'dataModelOptions', 'attrs', 'storageHash']);
}
};
- return WidgetDataModel;
- });
+ return WidgetModel;
+ }]);
/*
* Copyright (c) 2014 DataTorrent, Inc. ALL Rights Reserved.
*
@@ -1021,7 +937,18 @@ angular.module('ui.dashboard')
angular.module('ui.dashboard')
.factory('WidgetDefCollection', function () {
+
+ function convertToDefinition(d) {
+ if (typeof d === 'function') {
+ return new d();
+ }
+ return d;
+ }
+
function WidgetDefCollection(widgetDefs) {
+
+ widgetDefs = widgetDefs.map(convertToDefinition);
+
this.push.apply(this, widgetDefs);
// build (name -> widget definition) map for widget lookup by name
@@ -1038,6 +965,12 @@ angular.module('ui.dashboard')
return this.map[name];
};
+ WidgetDefCollection.prototype.add = function(def) {
+ def = convertToDefinition(def);
+ this.push(def);
+ this.map[def.name] = def;
+ }
+
return WidgetDefCollection;
});
/*
@@ -1058,134 +991,286 @@ angular.module('ui.dashboard')
'use strict';
-angular.module('ui.dashboard')
- .factory('WidgetModel', function ($log) {
- // constructor for widget model instances
- function WidgetModel(Class, overrides) {
- var defaults = {
- title: 'Widget',
- name: Class.name,
- attrs: Class.attrs,
- dataAttrName: Class.dataAttrName,
- dataModelType: Class.dataModelType,
- dataModelArgs: Class.dataModelArgs, // used in data model constructor, not serialized
- //AW Need deep copy of options to support widget options editing
- dataModelOptions: Class.dataModelOptions,
- settingsModalOptions: Class.settingsModalOptions,
- onSettingsClose: Class.onSettingsClose,
- onSettingsDismiss: Class.onSettingsDismiss,
- style: Class.style || {},
- size: Class.size || {},
- enableVerticalResize: (Class.enableVerticalResize === false) ? false : true
- };
+angular.module('ui.dashboard')
+ .factory('WidgetDataModel', function () {
+ function WidgetDataModel() {
+ }
+
+ WidgetDataModel.prototype = {
+ setup: function (widget, scope) {
+ this.dataAttrName = widget.dataAttrName;
+ this.dataModelOptions = widget.dataModelOptions;
+ this.widgetScope = scope;
+ },
+
+ updateScope: function (data) {
+ this.widgetScope.widgetData = data;
+ },
+
+ init: function () {
+ // to be overridden by subclasses
+ },
+
+ destroy: function () {
+ // to be overridden by subclasses
+ }
+ };
+
+ return WidgetDataModel;
+ });
+/*
+ * Copyright (c) 2014 DataTorrent, Inc. ALL Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+'use strict';
+
+angular.module('ui.dashboard')
+ .factory('LayoutStorage', function() {
+
+ var noopStorage = {
+ setItem: function() {
+
+ },
+ getItem: function() {
+
+ },
+ removeItem: function() {
+
+ }
+ };
+
+
+
+ function LayoutStorage(options) {
+
+ var defaults = {
+ storage: noopStorage,
+ storageHash: '',
+ stringifyStorage: true
+ };
+
+ angular.extend(defaults, options);
+ angular.extend(options, defaults);
+
+ this.id = options.storageId;
+ this.storage = options.storage;
+ this.storageHash = options.storageHash;
+ this.stringifyStorage = options.stringifyStorage;
+ this.widgetDefinitions = options.widgetDefinitions;
+ this.defaultLayouts = options.defaultLayouts;
+ this.lockDefaultLayouts = options.lockDefaultLayouts;
+ this.widgetButtons = options.widgetButtons;
+ this.explicitSave = options.explicitSave;
+ this.defaultWidgets = options.defaultWidgets;
+ this.settingsModalOptions = options.settingsModalOptions;
+ this.onSettingsClose = options.onSettingsClose;
+ this.onSettingsDismiss = options.onSettingsDismiss;
+ this.options = options;
+ this.options.unsavedChangeCount = 0;
+
+ this.layouts = [];
+ this.states = {};
+ this.load();
+ this._ensureActiveLayout();
+ }
+
+ LayoutStorage.prototype = {
+
+ add: function(layouts) {
+ if (!angular.isArray(layouts)) {
+ layouts = [layouts];
+ }
+ var self = this;
+ angular.forEach(layouts, function(layout) {
+ layout.dashboard = layout.dashboard || {};
+ layout.dashboard.storage = self;
+ layout.dashboard.storageId = layout.id = self._getLayoutId.call(self,layout);
+ layout.dashboard.widgetDefinitions = layout.widgetDefinitions || self.widgetDefinitions;
+ layout.dashboard.stringifyStorage = false;
+ layout.dashboard.defaultWidgets = layout.defaultWidgets || self.defaultWidgets;
+ layout.dashboard.widgetButtons = self.widgetButtons;
+ layout.dashboard.explicitSave = self.explicitSave;
+ layout.dashboard.settingsModalOptions = self.settingsModalOptions;
+ layout.dashboard.onSettingsClose = self.onSettingsClose;
+ layout.dashboard.onSettingsDismiss = self.onSettingsDismiss;
+ self.layouts.push(layout);
+ });
+ },
+
+ remove: function(layout) {
+ var index = this.layouts.indexOf(layout);
+ if (index >= 0) {
+ this.layouts.splice(index, 1);
+ delete this.states[layout.id];
+
+ // check for active
+ if (layout.active && this.layouts.length) {
+ var nextActive = index > 0 ? index - 1 : 0;
+ this.layouts[nextActive].active = true;
+ }
+ }
+ },
+
+ save: function() {
+
+ var state = {
+ layouts: this._serializeLayouts(),
+ states: this.states,
+ storageHash: this.storageHash
+ };
+
+ if (this.stringifyStorage) {
+ state = JSON.stringify(state);
+ }
+
+ this.storage.setItem(this.id, state);
+ this.options.unsavedChangeCount = 0;
+ },
+
+ load: function() {
+
+ var serialized = this.storage.getItem(this.id);
+
+ this.clear();
+
+ if (serialized) {
+ // check for promise
+ if (angular.isObject(serialized) && angular.isFunction(serialized.then)) {
+ this._handleAsyncLoad(serialized);
+ } else {
+ this._handleSyncLoad(serialized);
+ }
+ } else {
+ this._addDefaultLayouts();
+ }
+ },
+
+ clear: function() {
+ this.layouts = [];
+ this.states = {};
+ },
- overrides = overrides || {};
- angular.extend(this, angular.copy(defaults), overrides);
- this.containerStyle = { width: '33%' }; // default width
- this.contentStyle = {};
- this.updateContainerStyle(this.style);
+ setItem: function(id, value) {
+ this.states[id] = value;
+ this.save();
+ },
- if (Class.templateUrl) {
- this.templateUrl = Class.templateUrl;
- } else if (Class.template) {
- this.template = Class.template;
- } else {
- var directive = Class.directive || Class.name;
- this.directive = directive;
- }
+ getItem: function(id) {
+ return this.states[id];
+ },
- if (this.size && _.has(this.size, 'height')) {
- this.setHeight(this.size.height);
- }
+ removeItem: function(id) {
+ delete this.states[id];
+ this.save();
+ },
- if (this.style && _.has(this.style, 'width')) { //TODO deprecate style attribute
- this.setWidth(this.style.width);
- }
+ getActiveLayout: function() {
+ var len = this.layouts.length;
+ for (var i = 0; i < len; i++) {
+ var layout = this.layouts[i];
+ if (layout.active) {
+ return layout;
+ }
+ }
+ return false;
+ },
- if (this.size && _.has(this.size, 'width')) {
- this.setWidth(this.size.width);
- }
- }
+ _addDefaultLayouts: function() {
+ var self = this;
+ var defaults = this.lockDefaultLayouts ? { locked: true } : {};
+ angular.forEach(this.defaultLayouts, function(layout) {
+ self.add(angular.extend(_.clone(defaults), layout));
+ });
+ },
- WidgetModel.prototype = {
- // sets the width (and widthUnits)
- setWidth: function (width, units) {
- width = width.toString();
- units = units || width.replace(/^[-\.\d]+/, '') || '%';
+ _serializeLayouts: function() {
+ var result = [];
+ angular.forEach(this.layouts, function(l) {
+ result.push({
+ title: l.title,
+ id: l.id,
+ active: l.active,
+ locked: l.locked,
+ defaultWidgets: l.dashboard.defaultWidgets
+ });
+ });
+ return result;
+ },
- this.widthUnits = units;
- width = parseFloat(width);
+ _handleSyncLoad: function(serialized) {
+
+ var deserialized;
- if (width < 0 || isNaN(width)) {
- $log.warn('malhar-angular-dashboard: setWidth was called when width was ' + width);
- return false;
- }
+ if (this.stringifyStorage) {
+ try {
- if (units === '%') {
- width = Math.min(100, width);
- width = Math.max(0, width);
- }
+ deserialized = JSON.parse(serialized);
- this.containerStyle.width = width + '' + units;
+ } catch (e) {
+ this._addDefaultLayouts();
+ return;
+ }
+ } else {
- this.updateSize(this.containerStyle);
+ deserialized = serialized;
- return true;
- },
+ }
- setHeight: function (height) {
- this.contentStyle.height = height;
- this.updateSize(this.contentStyle);
+ if (this.storageHash !== deserialized.storageHash) {
+ this._addDefaultLayouts();
+ return;
+ }
+ this.states = deserialized.states;
+ this.add(deserialized.layouts);
},
- setStyle: function (style) {
- this.style = style;
- this.updateContainerStyle(style);
+ _handleAsyncLoad: function(promise) {
+ var self = this;
+ promise.then(
+ angular.bind(self, this._handleSyncLoad),
+ angular.bind(self, this._addDefaultLayouts)
+ );
},
- updateSize: function (size) {
- angular.extend(this.size, size);
+ _ensureActiveLayout: function() {
+ for (var i = 0; i < this.layouts.length; i++) {
+ var layout = this.layouts[i];
+ if (layout.active) {
+ return;
+ }
+ }
+ if (this.layouts[0]) {
+ this.layouts[0].active = true;
+ }
},
- updateContainerStyle: function (style) {
- angular.extend(this.containerStyle, style);
+ _getLayoutId: function(layout) {
+ if (layout.id) {
+ return layout.id;
+ }
+ var max = 0;
+ for (var i = 0; i < this.layouts.length; i++) {
+ var id = this.layouts[i].id;
+ max = Math.max(max, id * 1);
+ }
+ return max + 1;
}
- };
-
- return WidgetModel;
- });
-/*
- * Copyright (c) 2014 DataTorrent, Inc. ALL Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-'use strict';
-
-angular.module('ui.dashboard')
- .controller('SaveChangesModalCtrl', ['$scope', '$modalInstance', 'layout', function ($scope, $modalInstance, layout) {
-
- // add layout to scope
- $scope.layout = layout;
-
- $scope.ok = function () {
- $modalInstance.close();
- };
- $scope.cancel = function () {
- $modalInstance.dismiss();
};
- }]);
+ return LayoutStorage;
+ });
/*
* Copyright (c) 2014 DataTorrent, Inc. ALL Rights Reserved.
*
@@ -1205,429 +1290,164 @@ angular.module('ui.dashboard')
'use strict';
angular.module('ui.dashboard')
- .controller('DashboardWidgetCtrl', ['$scope', '$element', '$compile', '$window', '$timeout',
- function($scope, $element, $compile, $window, $timeout) {
-
- $scope.status = {
- isopen: false
- };
-
- // Fills "container" with compiled view
- $scope.makeTemplateString = function() {
-
- var widget = $scope.widget;
-
- // First, build template string
- var templateString = '';
-
- if (widget.templateUrl) {
-
- // Use ng-include for templateUrl
- templateString = '
';
-
- } else if (widget.template) {
-
- // Direct string template
- templateString = widget.template;
-
- } else {
-
- // Assume attribute directive
- templateString = '
';
- }
- return templateString;
- };
-
- $scope.grabResizer = function(e) {
-
- var widget = $scope.widget;
- var widgetElm = $element.find('.widget');
+ .factory('DashboardState', ['$log', '$q', function ($log, $q) {
+ function DashboardState(storage, id, hash, widgetDefinitions, stringify) {
+ this.storage = storage;
+ this.id = id;
+ this.hash = hash;
+ this.widgetDefinitions = widgetDefinitions;
+ this.stringify = stringify;
+ }
- // ignore middle- and right-click
- if (e.which !== 1) {
- return;
+ DashboardState.prototype = {
+ /**
+ * Takes array of widget instance objects, serializes,
+ * and saves state.
+ *
+ * @param {Array} widgets scope.widgets from dashboard directive
+ * @return {Boolean} true on success, false on failure
+ */
+ save: function (widgets) {
+
+ if (!this.storage) {
+ return true;
}
- e.stopPropagation();
- e.originalEvent.preventDefault();
-
- // get the starting horizontal position
- var initX = e.clientX;
- // console.log('initX', initX);
-
- // Get the current width of the widget and dashboard
- var pixelWidth = widgetElm.width();
- var pixelHeight = widgetElm.height();
- var widgetStyleWidth = widget.containerStyle.width;
- var widthUnits = widget.widthUnits;
- var unitWidth = parseFloat(widgetStyleWidth);
-
- // create marquee element for resize action
- var $marquee = angular.element('
');
- widgetElm.append($marquee);
+ var serialized = _.map(widgets, function (widget) {
+ return widget.serialize();
+ });
- // determine the unit/pixel ratio
- var transformMultiplier = unitWidth / pixelWidth;
+ var item = { widgets: serialized, hash: this.hash };
- // updates marquee with preview of new width
- var mousemove = function(e) {
- var curX = e.clientX;
- var pixelChange = curX - initX;
- var newWidth = pixelWidth + pixelChange;
- $marquee.css('width', newWidth + 'px');
- };
+ if (this.stringify) {
+ item = JSON.stringify(item);
+ }
- // sets new widget width on mouseup
- var mouseup = function(e) {
- // remove listener and marquee
- jQuery($window).off('mousemove', mousemove);
- $marquee.remove();
+ this.storage.setItem(this.id, item);
+ return true;
+ },
- // calculate change in units
- var curX = e.clientX;
- var pixelChange = curX - initX;
- var unitChange = Math.round(pixelChange * transformMultiplier * 100) / 100;
+ /**
+ * Loads dashboard state from the storage object.
+ * Can handle a synchronous response or a promise.
+ *
+ * @return {Array|Promise} Array of widget definitions or a promise
+ */
+ load: function () {
- // add to initial unit width
- var newWidth = unitWidth * 1 + unitChange;
- widget.setWidth(newWidth, widthUnits);
- $scope.$emit('widgetChanged', widget);
- $scope.$apply();
- $scope.$broadcast('widgetResized', {
- width: newWidth
- });
- };
+ if (!this.storage) {
+ return null;
+ }
- jQuery($window).on('mousemove', mousemove).one('mouseup', mouseup);
- };
+ var serialized;
- //TODO refactor
- $scope.grabSouthResizer = function(e) {
- var widgetElm = $element.find('.widget');
+ // try loading storage item
+ serialized = this.storage.getItem( this.id );
- // ignore middle- and right-click
- if (e.which !== 1) {
- return;
+ if (serialized) {
+ // check for promise
+ if (angular.isObject(serialized) && angular.isFunction(serialized.then)) {
+ return this._handleAsyncLoad(serialized);
+ }
+ // otherwise handle synchronous load
+ return this._handleSyncLoad(serialized);
+ } else {
+ return null;
}
+ },
- e.stopPropagation();
- e.originalEvent.preventDefault();
-
- // get the starting horizontal position
- var initY = e.clientY;
- // console.log('initX', initX);
-
- // Get the current width of the widget and dashboard
- var pixelWidth = widgetElm.width();
- var pixelHeight = widgetElm.height();
+ _handleSyncLoad: function(serialized) {
- // create marquee element for resize action
- var $marquee = angular.element('
');
- widgetElm.append($marquee);
+ var deserialized, result = [];
- // updates marquee with preview of new height
- var mousemove = function(e) {
- var curY = e.clientY;
- var pixelChange = curY - initY;
- var newHeight = pixelHeight + pixelChange;
- $marquee.css('height', newHeight + 'px');
- };
+ if (!serialized) {
+ return null;
+ }
- // sets new widget width on mouseup
- var mouseup = function(e) {
- // remove listener and marquee
- jQuery($window).off('mousemove', mousemove);
- $marquee.remove();
+ if (this.stringify) {
+ try { // to deserialize the string
- // calculate height change
- var curY = e.clientY;
- var pixelChange = curY - initY;
+ deserialized = JSON.parse(serialized);
- //var widgetContainer = widgetElm.parent(); // widget container responsible for holding widget width and height
- var widgetContainer = widgetElm.find('.widget-content');
+ } catch (e) {
- var diff = pixelChange;
- var height = parseInt(widgetContainer.css('height'), 10);
- var newHeight = (height + diff);
+ // bad JSON, log a warning and return
+ $log.warn('Serialized dashboard state was malformed and could not be parsed: ', serialized);
+ return null;
- //$scope.widget.style.height = newHeight + 'px';
+ }
+ }
+ else {
+ deserialized = serialized;
+ }
- $scope.widget.setHeight(newHeight + 'px');
+ // check hash against current hash
+ if (deserialized.hash !== this.hash) {
- $scope.$emit('widgetChanged', $scope.widget);
- $scope.$apply(); // make AngularJS to apply style changes
+ $log.info('Serialized dashboard from storage was stale (old hash: ' + deserialized.hash + ', new hash: ' + this.hash + ')');
+ this.storage.removeItem(this.id);
+ return null;
- $scope.$broadcast('widgetResized', {
- height: newHeight
- });
- };
+ }
- jQuery($window).on('mousemove', mousemove).one('mouseup', mouseup);
- };
+ // Cache widgets
+ var savedWidgetDefs = deserialized.widgets;
- // replaces widget title with input
- $scope.editTitle = function(widget) {
- var widgetElm = $element.find('.widget');
- widget.editingTitle = true;
- // HACK: get the input to focus after being displayed.
- $timeout(function() {
- widgetElm.find('form.widget-title input:eq(0)').focus()[0].setSelectionRange(0, 9999);
- });
- };
+ // instantiate widgets from stored data
+ for (var i = 0; i < savedWidgetDefs.length; i++) {
- // saves whatever is in the title input as the new title
- $scope.saveTitleEdit = function(widget) {
- widget.editingTitle = false;
- $scope.$emit('widgetChanged', widget);
- };
+ // deserialized object
+ var savedWidgetDef = savedWidgetDefs[i];
- $scope.compileTemplate = function() {
- var container = $scope.findWidgetContainer($element);
- var templateString = $scope.makeTemplateString();
- var widgetElement = angular.element(templateString);
+ // widget definition to use
+ var widgetDefinition = this.widgetDefinitions.getByName(savedWidgetDef.name);
- container.empty();
- container.append(widgetElement);
- $compile(widgetElement)($scope);
- };
+ // check for no widget
+ if (!widgetDefinition) {
+ // no widget definition found, remove and return false
+ $log.warn('Widget with name "' + savedWidgetDef.name + '" was not found in given widget definition objects');
+ continue;
+ }
- $scope.findWidgetContainer = function(element) {
- // widget placeholder is the first (and only) child of .widget-content
- return element.find('.widget-content');
- };
- }
- ]);
-/*
- * Copyright (c) 2014 DataTorrent, Inc. ALL Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
+ // check widget-specific storageHash
+ if (widgetDefinition.hasOwnProperty('storageHash') && widgetDefinition.storageHash !== savedWidgetDef.storageHash) {
+ // widget definition was found, but storageHash was stale, removing storage
+ $log.info('Widget Definition Object with name "' + savedWidgetDef.name + '" was found ' +
+ 'but the storageHash property on the widget definition is different from that on the ' +
+ 'serialized widget loaded from storage. hash from storage: "' + savedWidgetDef.storageHash + '"' +
+ ', hash from WDO: "' + widgetDefinition.storageHash + '"');
+ continue;
+ }
-'use strict';
+ // push instantiated widget to result array
+ result.push(savedWidgetDef);
+ }
-angular.module('ui.dashboard')
- .controller('WidgetSettingsCtrl', ['$scope', '$modalInstance', 'widget', function ($scope, $modalInstance, widget) {
- // add widget to scope
- $scope.widget = widget;
+ return result;
+ },
- // set up result object
- $scope.result = jQuery.extend(true, {}, widget);
+ _handleAsyncLoad: function(promise) {
+ var self = this;
+ var deferred = $q.defer();
+ promise.then(
+ // success
+ function(res) {
+ var result = self._handleSyncLoad(res);
+ if (result) {
+ deferred.resolve(result);
+ } else {
+ deferred.reject(result);
+ }
+ },
+ // failure
+ function(res) {
+ deferred.reject(res);
+ }
+ );
- $scope.ok = function () {
- $modalInstance.close($scope.result);
- };
+ return deferred.promise;
+ }
- $scope.cancel = function () {
- $modalInstance.dismiss('cancel');
};
- }]);
-angular.module("ui.dashboard").run(["$templateCache", function($templateCache) {
-
- $templateCache.put("template/alt-dashboard.html",
- "\n" +
- "
\n" +
- "\n" +
- "
\n" +
- "
\n"
- );
-
- $templateCache.put("template/dashboard-layouts.html",
- "\n" +
- "
"
- );
-
- $templateCache.put("template/dashboard.html",
- "\n" +
- "
\n" +
- "\n" +
- "
\n" +
- "
"
- );
-
- $templateCache.put("template/save-changes-modal.html",
- "\n" +
- "\n" +
- "\n" +
- "
You have {{layout.dashboard.unsavedChangeCount}} unsaved changes on this dashboard. Would you like to save them?
\n" +
- "
\n" +
- "\n" +
- ""
- );
-
- $templateCache.put("template/widget-default-content.html",
- ""
- );
-
- $templateCache.put("template/widget-settings-template.html",
- "\n" +
- "\n" +
- "\n" +
- "\n" +
- ""
- );
-
-}]);
+ return DashboardState;
+ }]);
\ No newline at end of file
diff --git a/e2e/main.po.js b/e2e/main.po.js
new file mode 100644
index 0000000..6b88871
--- /dev/null
+++ b/e2e/main.po.js
@@ -0,0 +1,15 @@
+/**
+ * This file uses the Page Object pattern to define the main page for tests
+ * https://docs.google.com/presentation/d/1B6manhG0zEXkC-H-tPo2vwU06JhL8w9-XCF9oehXzAQ
+ */
+
+'use strict';
+
+var MainPage = function() {
+ this.jumbEl = element(by.css('.jumbotron'));
+ this.h1El = this.jumbEl.element(by.css('h1'));
+ this.imgEl = this.jumbEl.element(by.css('img'));
+ this.thumbnailEls = element(by.css('body')).all(by.repeater('awesomeThing in awesomeThings'));
+};
+
+module.exports = new MainPage();
diff --git a/e2e/main.spec.js b/e2e/main.spec.js
new file mode 100644
index 0000000..da89d22
--- /dev/null
+++ b/e2e/main.spec.js
@@ -0,0 +1,21 @@
+'use strict';
+
+describe('The main view', function () {
+ var page;
+
+ beforeEach(function () {
+ browser.get('http://localhost:3000/index.html');
+ page = require('./main.po');
+ });
+
+ it('should include jumbotron with correct data', function() {
+ expect(page.h1El.getText()).toBe('\'Allo, \'Allo!');
+ expect(page.imgEl.getAttribute('src')).toMatch(/assets\/images\/yeoman.png$/);
+ expect(page.imgEl.getAttribute('alt')).toBe('I\'m Yeoman');
+ });
+
+ it('list more than 5 awesome things', function () {
+ expect(page.thumbnailEls.count()).toBeGreaterThan(5);
+ });
+
+});
diff --git a/favicon.ico b/favicon.ico
new file mode 100644
index 0000000..03156aa
Binary files /dev/null and b/favicon.ico differ
diff --git a/gulp/build-demo.js b/gulp/build-demo.js
new file mode 100644
index 0000000..62bef05
--- /dev/null
+++ b/gulp/build-demo.js
@@ -0,0 +1,82 @@
+'use strict';
+
+var gulp = require('gulp');
+
+var paths = gulp.paths;
+
+var $ = require('gulp-load-plugins')({
+ pattern: ['gulp-*', 'main-bower-files', 'uglify-save-license', 'del']
+});
+
+gulp.task('demo:partials', function () {
+ return gulp.src([
+ paths.src + '/{app,components}/**/*.html',
+ paths.tmp + '/{app,components}/**/*.html'
+ ])
+ .pipe($.angularTemplatecache('templateCacheHtml.js', {
+ module: 'app'
+ }))
+ .pipe(gulp.dest(paths.tmp + '/partials/'));
+});
+
+gulp.task('demo:html', ['inject', 'demo:partials'], function () {
+ var partialsInjectFile = gulp.src(paths.tmp + '/partials/templateCacheHtml.js', { read: false });
+ var partialsInjectOptions = {
+ starttag: '',
+ ignorePath: paths.tmp + '/partials',
+ addRootSlash: false
+ };
+
+ var htmlFilter = $.filter('*.html');
+ var jsFilter = $.filter('**/*.js');
+ var cssFilter = $.filter('**/*.css');
+ var assets;
+
+ return gulp.src(paths.tmp + '/serve/*.html')
+ .pipe($.inject(partialsInjectFile, partialsInjectOptions))
+ .pipe(assets = $.useref.assets())
+ .pipe($.rev())
+ .pipe(jsFilter)
+ .pipe($.ngAnnotate())
+ .pipe($.uglify({preserveComments: $.uglifySaveLicense}))
+ .pipe(jsFilter.restore())
+ .pipe(cssFilter)
+ .pipe($.replace('../bootstrap/fonts', 'fonts'))
+ .pipe($.csso())
+ .pipe(cssFilter.restore())
+ .pipe(assets.restore())
+ .pipe($.useref())
+ .pipe($.revReplace())
+ .pipe(htmlFilter)
+ .pipe($.minifyHtml({
+ empty: true,
+ spare: true,
+ quotes: true
+ }))
+ .pipe(htmlFilter.restore())
+ .pipe(gulp.dest(paths.demo + '/'))
+ .pipe($.size({ title: paths.demo + '/', showFiles: true }));
+});
+
+gulp.task('demo:images', function () {
+ return gulp.src(paths.src + '/assets/images/**/*')
+ .pipe(gulp.dest(paths.demo + '/assets/images/'));
+});
+
+gulp.task('demo:fonts', function () {
+ return gulp.src($.mainBowerFiles())
+ .pipe($.filter('**/*.{eot,svg,ttf,woff}'))
+ .pipe($.flatten())
+ .pipe(gulp.dest(paths.demo + '/fonts/'));
+});
+
+gulp.task('demo:misc', function () {
+ return gulp.src(paths.src + '/**/*.ico')
+ .pipe(gulp.dest(paths.demo + '/'));
+});
+
+gulp.task('demo:clean', function (done) {
+ $.del([paths.demo + '/', paths.tmp + '/'], done);
+});
+
+gulp.task('build:demo', ['demo:html', 'demo:images', 'demo:fonts', 'demo:misc']);
diff --git a/gulp/build.js b/gulp/build.js
new file mode 100644
index 0000000..dacb5f7
--- /dev/null
+++ b/gulp/build.js
@@ -0,0 +1,47 @@
+'use strict';
+
+var gulp = require('gulp');
+
+var paths = gulp.paths;
+
+var $ = require('gulp-load-plugins')({
+ pattern: ['gulp-*', 'main-bower-files', 'uglify-save-license', 'del']
+});
+
+gulp.task('partials', function () {
+ return gulp.src([
+ paths.src + '/components/**/*.html'
+ ])
+ .pipe($.angularTemplatecache('templateCacheHtml.js', {
+ module: 'ui.dashboard',
+ root: 'components'
+ }))
+ .pipe(gulp.dest(paths.tmp + '/partials/'));
+});
+
+gulp.task('clean', function (done) {
+ $.del([paths.dist + '/', paths.tmp + '/'], done);
+});
+
+gulp.task('build:js', ['partials'], function() {
+ return gulp.src([
+ paths.src + '/components/**/!(*.spec|*_e2e)+(.js)',
+ paths.tmp + '/partials/templateCacheHtml.js'
+ ])
+ .pipe($.angularFilesort())
+ .pipe($.concat('malhar-angular-dashboard.js'))
+ .pipe($.ngAnnotate())
+ .pipe(gulp.dest(paths.dist))
+
+});
+
+gulp.task('build:css', function() {
+ return gulp.src([
+ paths.src + '/components/**/*.less'
+ ])
+ .pipe($.concat('malhar-angular-dashboard.less'))
+ .pipe($.less())
+ .pipe(gulp.dest(paths.dist));
+});
+
+gulp.task('build', ['build:js', 'build:css']);
diff --git a/gulp/e2e-tests.js b/gulp/e2e-tests.js
new file mode 100644
index 0000000..99ab2c5
--- /dev/null
+++ b/gulp/e2e-tests.js
@@ -0,0 +1,35 @@
+'use strict';
+
+var gulp = require('gulp');
+
+var $ = require('gulp-load-plugins')();
+
+var browserSync = require('browser-sync');
+
+var paths = gulp.paths;
+
+// Downloads the selenium webdriver
+gulp.task('webdriver-update', $.protractor.webdriver_update);
+
+gulp.task('webdriver-standalone', $.protractor.webdriver_standalone);
+
+function runProtractor (done) {
+
+ gulp.src(paths.e2e + '/**/*.js')
+ .pipe($.protractor.protractor({
+ configFile: 'protractor.conf.js',
+ }))
+ .on('error', function (err) {
+ // Make sure failed tests cause gulp to exit non-zero
+ throw err;
+ })
+ .on('end', function () {
+ // Close browser sync server
+ browserSync.exit();
+ done();
+ });
+}
+
+gulp.task('protractor', ['protractor:src']);
+gulp.task('protractor:src', ['serve:e2e', 'webdriver-update'], runProtractor);
+gulp.task('protractor:dist', ['serve:e2e-dist', 'webdriver-update'], runProtractor);
diff --git a/gulp/inject.js b/gulp/inject.js
new file mode 100644
index 0000000..0cbf793
--- /dev/null
+++ b/gulp/inject.js
@@ -0,0 +1,42 @@
+'use strict';
+
+var gulp = require('gulp');
+
+var paths = gulp.paths;
+
+var $ = require('gulp-load-plugins')();
+
+var wiredep = require('wiredep').stream;
+
+gulp.task('inject', ['styles'], function () {
+
+ var injectStyles = gulp.src([
+ paths.tmp + '/serve/{app,components}/**/*.css',
+ '!' + paths.tmp + '/serve/app/vendor.css'
+ ], { read: false });
+
+ var injectScripts = gulp.src([
+ paths.src + '/{app,components}/**/*.js',
+ paths.tmp + '/partials/templateCacheHtml.js',
+ '!' + paths.src + '/{app,components}/**/*.spec.js',
+ '!' + paths.src + '/{app,components}/**/*.mock.js'
+ ]).pipe($.angularFilesort());
+
+ var injectOptions = {
+ ignorePath: [paths.src, paths.tmp + '/serve', paths.tmp + '/partials'],
+ addRootSlash: false
+ };
+
+ var wiredepOptions = {
+ devDependencies: true,
+ directory: 'bower_components',
+ exclude: [/bootstrap\.css/, /bootstrap\.css/, /foundation\.css/]
+ };
+
+ return gulp.src(paths.src + '/index.html')
+ .pipe($.inject(injectStyles, injectOptions))
+ .pipe($.inject(injectScripts, injectOptions))
+ .pipe(wiredep(wiredepOptions))
+ .pipe(gulp.dest(paths.tmp + '/serve'));
+
+});
diff --git a/gulp/proxy.js b/gulp/proxy.js
new file mode 100644
index 0000000..2fcd734
--- /dev/null
+++ b/gulp/proxy.js
@@ -0,0 +1,65 @@
+ /*jshint unused:false */
+
+/***************
+
+ This file allow to configure a proxy system plugged into BrowserSync
+ in order to redirect backend requests while still serving and watching
+ files from the web project
+
+ IMPORTANT: The proxy is disabled by default.
+
+ If you want to enable it, watch at the configuration options and finally
+ change the `module.exports` at the end of the file
+
+***************/
+
+'use strict';
+
+var httpProxy = require('http-proxy');
+var chalk = require('chalk');
+
+/*
+ * Location of your backend server
+ */
+var proxyTarget = 'http://server/context/';
+
+var proxy = httpProxy.createProxyServer({
+ target: proxyTarget
+});
+
+proxy.on('error', function(error, req, res) {
+ res.writeHead(500, {
+ 'Content-Type': 'text/plain'
+ });
+
+ console.error(chalk.red('[Proxy]'), error);
+});
+
+/*
+ * The proxy middleware is an Express middleware added to BrowserSync to
+ * handle backend request and proxy them to your backend.
+ */
+function proxyMiddleware(req, res, next) {
+ /*
+ * This test is the switch of each request to determine if the request is
+ * for a static file to be handled by BrowserSync or a backend request to proxy.
+ *
+ * The existing test is a standard check on the files extensions but it may fail
+ * for your needs. If you can, you could also check on a context in the url which
+ * may be more reliable but can't be generic.
+ */
+ 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)) {
+ next();
+ } else {
+ proxy.web(req, res);
+ }
+}
+
+/*
+ * This is where you activate or not your proxy.
+ *
+ * The first line activate if and the second one ignored it
+ */
+
+//module.exports = [proxyMiddleware];
+module.exports = [];
diff --git a/gulp/server.js b/gulp/server.js
new file mode 100644
index 0000000..1cad49f
--- /dev/null
+++ b/gulp/server.js
@@ -0,0 +1,60 @@
+'use strict';
+
+var gulp = require('gulp');
+
+var paths = gulp.paths;
+
+var util = require('util');
+
+var browserSync = require('browser-sync');
+
+var middleware = require('./proxy');
+
+function browserSyncInit(baseDir, files, browser) {
+ browser = browser === undefined ? 'default' : browser;
+
+ var routes = null;
+ if(baseDir === paths.src || (util.isArray(baseDir) && baseDir.indexOf(paths.src) !== -1)) {
+ routes = {
+ '/bower_components': 'bower_components'
+ };
+ }
+
+ browserSync.instance = browserSync.init(files, {
+ startPath: '/',
+ server: {
+ baseDir: baseDir,
+ middleware: middleware,
+ routes: routes
+ },
+ browser: browser
+ });
+}
+
+gulp.task('serve', ['watch'], function () {
+ browserSyncInit([
+ paths.tmp + '/serve',
+ paths.src,
+ paths.bower + '/bootstrap',
+ paths.tmp + '/partials/'
+ ], [
+ paths.tmp + '/serve/{app,components}/**/*.css',
+ paths.src + '/{app,components}/**/*.js',
+ paths.src + 'src/assets/images/**/*',
+ paths.tmp + '/serve/*.html',
+ paths.tmp + '/serve/{app,components}/**/*.html',
+ paths.src + '/{app,components}/**/*.html'
+ ]);
+});
+
+gulp.task('serve:dist', ['build:demo'], function () {
+ browserSyncInit(paths.demo);
+});
+
+gulp.task('serve:e2e', ['inject'], function () {
+ browserSyncInit([paths.tmp + '/serve', paths.src], null, []);
+});
+
+gulp.task('serve:e2e-dist', ['build:demo'], function () {
+ browserSyncInit(paths.demo, null, []);
+});
diff --git a/gulp/styles.js b/gulp/styles.js
new file mode 100644
index 0000000..a033b26
--- /dev/null
+++ b/gulp/styles.js
@@ -0,0 +1,53 @@
+'use strict';
+
+var gulp = require('gulp');
+
+var paths = gulp.paths;
+
+var $ = require('gulp-load-plugins')();
+
+gulp.task('styles', function () {
+
+ var lessOptions = {
+ paths: [
+ 'bower_components',
+ paths.src + '/app',
+ paths.src + '/components'
+ ]
+ };
+
+ var injectFiles = gulp.src([
+ paths.src + '/{app,components}/**/*.less',
+ '!' + paths.src + '/app/index.less',
+ '!' + paths.src + '/app/vendor.less'
+ ], { read: false });
+
+ var injectOptions = {
+ transform: function(filePath) {
+ filePath = filePath.replace(paths.src + '/app/', '');
+ filePath = filePath.replace(paths.src + '/components/', '../components/');
+ return '@import \'' + filePath + '\';';
+ },
+ starttag: '// injector',
+ endtag: '// endinjector',
+ addRootSlash: false
+ };
+
+ var indexFilter = $.filter('index.less');
+
+ return gulp.src([
+ paths.src + '/app/index.less',
+ paths.src + '/app/vendor.less'
+ ])
+ .pipe(indexFilter)
+ .pipe($.inject(injectFiles, injectOptions))
+ .pipe(indexFilter.restore())
+ .pipe($.less())
+
+ .pipe($.autoprefixer())
+ .on('error', function handleError(err) {
+ console.error(err.toString());
+ this.emit('end');
+ })
+ .pipe(gulp.dest(paths.tmp + '/serve/app/'));
+});
diff --git a/gulp/unit-tests.js b/gulp/unit-tests.js
new file mode 100644
index 0000000..998a185
--- /dev/null
+++ b/gulp/unit-tests.js
@@ -0,0 +1,37 @@
+'use strict';
+
+var gulp = require('gulp');
+
+var $ = require('gulp-load-plugins')();
+
+var merge = require('merge-stream');
+var wiredep = require('wiredep');
+
+var paths = gulp.paths;
+
+function runTests (singleRun, done) {
+ var bowerDeps = wiredep({
+ directory: 'bower_components',
+ exclude: ['bootstrap-sass-official'],
+ dependencies: true,
+ devDependencies: true
+ });
+ var testFiles = gulp.src(bowerDeps.js);
+ var srcFiles = gulp.src([
+ paths.src + '/{app,components}/**/*.js',
+ paths.tmp + '/partials/templateCacheHtml.js'
+ ]).pipe($.angularFilesort());
+
+ return merge(testFiles, srcFiles)
+ .pipe($.karma({
+ configFile: 'karma.conf.js',
+ action: (singleRun)? 'run': 'watch'
+ }))
+ .on('error', function (err) {
+ // Make sure failed tests cause gulp to exit non-zero
+ throw err;
+ });
+}
+
+gulp.task('test', ['partials'], function () { return runTests(true /* singleRun */) });
+gulp.task('test:auto', ['partials'], function () { return runTests(false /* singleRun */) });
\ No newline at end of file
diff --git a/gulp/watch.js b/gulp/watch.js
new file mode 100644
index 0000000..e771c18
--- /dev/null
+++ b/gulp/watch.js
@@ -0,0 +1,21 @@
+'use strict';
+
+var gulp = require('gulp');
+
+var paths = gulp.paths;
+
+gulp.task('watch', ['inject'], function () {
+ var globs = [
+ paths.src + '/{app,components}/**/*.html',
+ paths.tmp + '/{app,components}/**/*.html'
+ ];
+
+ gulp.watch(globs, ['demo:partials']);
+
+ gulp.watch([
+ paths.src + '/{app,components}/**/*.less',
+ paths.src + '/{app,components}/**/*.js',
+ paths.tmp + '/partials/**/*.js',
+ 'bower.json'
+ ], ['inject']);
+});
diff --git a/gulpfile.js b/gulpfile.js
index ff99059..d179dbf 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -1,48 +1,18 @@
-var gulp = require('gulp');
-
-var clean = require('gulp-clean');
-var rimraf = require('gulp-rimraf');
-var minifyCss = require('gulp-minify-css');
-var uglify = require('gulp-uglify');
-var usemin = require('gulp-usemin');
+'use strict';
-var dev = {
- dir: 'demo',
- index: 'demo/index.html',
- views: ['demo/view.html', 'demo/layouts.html'],
- fonts: 'bower_components/bootstrap/fonts/*'
-};
+var gulp = require('gulp');
-var prod = {
- dir: 'demo_dist',
- fonts: 'demo_dist/fonts'
+gulp.paths = {
+ src: 'src',
+ dist: 'dist',
+ demo: 'demo',
+ tmp: '.tmp',
+ e2e: 'e2e',
+ bower: 'bower_components'
};
-var options = {
- clean: { read: false },
- uglify: { mangle: false }
-};
+require('require-dir')('./gulp');
-gulp.task('clean', function() {
- return gulp.src(prod.dir, options.clean)
- .pipe(rimraf({ force: true }));
+gulp.task('default', ['clean','test'], function () {
+ gulp.start('build');
});
-
-gulp.task('copy', function() {
- gulp.src(dev.fonts)
- .pipe(gulp.dest(prod.fonts));
-
- gulp.src(dev.views)
- .pipe(gulp.dest(prod.dir));
-});
-
-gulp.task('demo_dist', ['clean', 'copy'], function() {
- gulp.src(dev.index)
- .pipe(usemin({
- css: [minifyCss()],
- js: [uglify(options.uglify)]
- }))
- .pipe(gulp.dest(prod.dir));
-});
-
-gulp.task('serve', ['styles', 'server', 'watch']);
\ No newline at end of file
diff --git a/karma.conf.js b/karma.conf.js
index eb6d52d..12e3ff3 100644
--- a/karma.conf.js
+++ b/karma.conf.js
@@ -1,71 +1,17 @@
-// Karma configuration
-// http://karma-runner.github.io/0.10/config/configuration-file.html
+'use strict';
module.exports = function(config) {
- config.set({
-
- preprocessors: {
- // which files to show in coverage report
- 'src/**/*.js': ['coverage']
- },
-
- reporters: ['dots', 'coverage'],
-
- coverageReporter: {
- type: 'html',
- dir: 'coverage/'
- },
- // base path, that will be used to resolve files and exclude
- basePath: '',
+ config.set({
+ autoWatch : false,
- // testing framework to use (jasmine/mocha/qunit/...)
frameworks: ['jasmine'],
- // list of files / patterns to load in the browser
- files: [
- 'bower_components/jquery/dist/jquery.js',
- 'bower_components/lodash/dist/lodash.js',
- 'bower_components/angular/angular.js',
- 'bower_components/angular-mocks/angular-mocks.js',
- 'bower_components/angular-bootstrap/ui-bootstrap-tpls.js',
- 'src/directives/dashboard.js',
- 'src/directives/*.js',
- 'src/models/*.js',
- 'src/controllers/*.js',
- 'template/*.js',
- 'test/mock/**/*.js',
- 'test/spec/**/*.js'
- ],
-
- // list of files / patterns to exclude
- exclude: [],
-
- // web server port
- port: 8080,
-
- // level of logging
- // possible values: LOG_DISABLE || LOG_ERROR || LOG_WARN || LOG_INFO || LOG_DEBUG
- logLevel: config.LOG_INFO,
-
-
- // enable / disable watching file and executing tests whenever any file changes
- autoWatch: true,
-
-
- // Start these browsers, currently available:
- // - Chrome
- // - ChromeCanary
- // - Firefox
- // - Opera
- // - Safari (only Mac)
- // - PhantomJS
- // - IE (only Windows)
- browsers: ['PhantomJS'],
-
+ browsers : ['PhantomJS'],
- // Continuous Integration mode
- // if true, it capture browsers, run tests and exit
- singleRun: false
+ plugins : [
+ 'karma-phantomjs-launcher',
+ 'karma-jasmine'
+ ]
});
};
diff --git a/package.json b/package.json
index 126685f..fd35c65 100644
--- a/package.json
+++ b/package.json
@@ -1,61 +1,57 @@
{
"name": "malhar-angular-dashboard",
- "version": "0.8.2",
+ "version": "1.0.0",
"author": "https://github.com/DataTorrent/malhar-angular-dashboard/graphs/contributors",
"homepage": "https://github.com/DataTorrent/malhar-angular-dashboard",
"license": "Apache License, v2.0",
"dependencies": {},
"devDependencies": {
- "grunt": "~0.4.2",
- "grunt-angular-templates": "~0.3.0",
- "grunt-autoprefixer": "~0.2.0",
- "grunt-concurrent": "~0.3.0",
- "grunt-contrib-clean": "~0.5.0",
- "grunt-contrib-coffee": "~0.7.0",
- "grunt-contrib-compass": "~0.5.0",
- "grunt-contrib-concat": "~0.3.0",
- "grunt-contrib-connect": "~0.5.0",
- "grunt-contrib-copy": "~0.4.1",
- "grunt-contrib-cssmin": "~0.6.0",
- "grunt-contrib-htmlmin": "~0.1.3",
- "grunt-contrib-imagemin": "~0.2.0",
- "grunt-contrib-jshint": "~0.6.0",
- "grunt-contrib-uglify": "~0.2.0",
- "grunt-contrib-watch": "~0.5.2",
- "grunt-google-cdn": "~0.2.0",
- "grunt-html2js": "~0.1.3",
- "grunt-karma": "~0.8.3",
- "grunt-rev": "~0.1.0",
- "grunt-svgmin": "~0.2.0",
- "grunt-usemin": "~0.1.11",
- "karma": "~0.12.0",
- "karma-chrome-launcher": "^0.1.3",
- "karma-coffee-preprocessor": "~0.1.2",
- "karma-coverage": "^0.2.1",
- "karma-firefox-launcher": "~0.1.3",
- "karma-html2js-preprocessor": "~0.1.0",
- "karma-jasmine": "~0.2.2",
- "karma-ng-html2js-preprocessor": "~0.1.0",
- "karma-ng-scenario": "~0.1.0",
- "karma-phantomjs-launcher": "~0.1.1",
- "karma-requirejs": "~0.2.1",
- "karma-script-launcher": "~0.1.0",
- "load-grunt-tasks": "~0.1.0",
- "requirejs": "~2.1.9",
- "time-grunt": "~0.1.0",
- "gulp-usemin": "^0.3.7",
- "gulp": "^3.8.6",
- "gulp-uglify": "^0.3.1",
- "gulp-minify-html": "^0.1.4",
- "gulp-minify-css": "^0.3.7",
- "gulp-rev": "^1.0.0",
- "gulp-htmlmin": "^0.1.3",
- "gulp-rimraf": "^0.1.0"
+ "browser-sync": "~1.7.1",
+ "chalk": "~0.5.1",
+ "del": "~0.1.3",
+ "gulp": "~3.8.10",
+ "gulp-angular-filesort": "~1.0.4",
+ "gulp-angular-templatecache": "~1.4.2",
+ "gulp-autoprefixer": "~2.0.0",
+ "gulp-concat": "^2.4.3",
+ "gulp-consolidate": "~0.1.2",
+ "gulp-csso": "~0.2.9",
+ "gulp-filter": "~1.0.2",
+ "gulp-flatten": "~0.0.4",
+ "gulp-inject": "~1.0.2",
+ "gulp-jshint": "~1.9.0",
+ "gulp-karma": "~0.0.4",
+ "gulp-less": "~1.3.6",
+ "gulp-load-plugins": "~0.7.1",
+ "gulp-minify-html": "~0.1.7",
+ "gulp-ng-annotate": "~0.3.6",
+ "gulp-protractor": "~0.0.11",
+ "gulp-rename": "~1.2.0",
+ "gulp-replace": "~0.5.0",
+ "gulp-rev": "~2.0.1",
+ "gulp-rev-replace": "~0.3.1",
+ "gulp-size": "~1.1.0",
+ "gulp-uglify": "~1.0.1",
+ "gulp-useref": "~1.0.2",
+ "http-proxy": "~1.7.0",
+ "jshint-stylish": "~1.0.0",
+ "karma-jasmine": "~0.3.1",
+ "karma-phantomjs-launcher": "~0.1.4",
+ "main-bower-files": "~2.4.0",
+ "merge-stream": "^0.1.7",
+ "protractor": "~1.4.0",
+ "require-dir": "~0.1.0",
+ "uglify-save-license": "~0.4.1",
+ "wiredep": "~2.2.0"
},
"engines": {
- "node": ">=0.8.0"
+ "node": ">=0.10.0"
},
+ "main": [
+ "dist/malhar-angular-dashboard.css",
+ "dist/malhar-angular-dashboard.js"
+ ],
"scripts": {
- "test": "grunt test"
+ "test": "gulp test"
}
}
diff --git a/protractor.conf.js b/protractor.conf.js
new file mode 100644
index 0000000..0f43a9e
--- /dev/null
+++ b/protractor.conf.js
@@ -0,0 +1,25 @@
+'use strict';
+
+var paths = require('./.yo-rc.json')['generator-gulp-angular'].props.paths;
+
+// An example configuration file.
+exports.config = {
+ // The address of a running selenium server.
+ //seleniumAddress: 'http://localhost:4444/wd/hub',
+ //seleniumServerJar: deprecated, this should be set on node_modules/protractor/config.json
+
+ // Capabilities to be passed to the webdriver instance.
+ capabilities: {
+ 'browserName': 'chrome'
+ },
+
+ // Spec patterns are relative to the current working directly when
+ // protractor is called.
+ specs: [paths.e2e + '/**/*.js'],
+
+ // Options to be passed to Jasmine-node.
+ jasmineNodeOpts: {
+ showColors: true,
+ defaultTimeoutInterval: 30000
+ }
+};
diff --git a/src/404.html b/src/404.html
new file mode 100644
index 0000000..fdace4a
--- /dev/null
+++ b/src/404.html
@@ -0,0 +1,157 @@
+
+
+
+
+ Page Not Found :(
+
+
+
+
+
Not found :(
+
Sorry, but the page you were trying to view does not exist.
+
It looks like this was the result of either:
+
+ a mistyped address
+ an out-of-date link
+
+
+
+
+
+
diff --git a/src/angular-ui-dashboard.css b/src/angular-ui-dashboard.css
deleted file mode 100644
index 6b5b717..0000000
--- a/src/angular-ui-dashboard.css
+++ /dev/null
@@ -1,88 +0,0 @@
-.dashboard-widget-area {
- margin: 10px 0 30px;
- min-height: 200px;
-}
-
-.widget-container {
- float:left;
- display: inline-block;
- width: 33%;
- padding-bottom: 1em;
-}
-
-.widget {
- margin: 0 1em 0 0;
- background-color: white;
- border: 2px solid #444;
- border-radius: 5px;
- position: relative;
- height: 100%;
-}
-.widget-header {
- overflow: hidden;
-}
-.widget-header .label {
- display: inline-block;
- vertical-align: middle;
-}
-.widget-header .glyphicon {
- cursor: pointer;
- float: right;
- opacity: 0.5;
- margin-left: 5px;
-}
-.widget-header .glyphicon:hover {
- opacity: 1;
-}
-.widget-header .widget-title {
- vertical-align: middle;
-}
-.widget-header form.widget-title {
- display: inline;
-}
-
-.widget-header form.widget-title input.form-control {
- width: auto;
- display: inline-block;
-}
-
-.widget-content {
- overflow: hidden;
-}
-
-.widget .widget-ew-resizer {
- position: absolute;
- width: 5px;
- right: -2px;
- height:100%;
- top:0;
- cursor: ew-resize;
-}
-
-.widget .widget-s-resizer {
- cursor: ns-resize;
- height: 5px;
- width: 100%;
- bottom: -7px;
- left: 0;
-}
-
-.widget .widget-resizer-marquee {
- box-shadow: inset 0 0 0 1px rgba(0,0,0,0.5);
- position: absolute;
- top: 0;
- left: 0;
- z-index: 2;
-}
-
-.remove-layout-icon {
- vertical-align: text-top;
- cursor: pointer;
- opacity: 0.3;
-}
-.remove-layout-icon:hover {
- opacity: 1;
-}
-.layout-title {
- display: inline-block;
-}
\ No newline at end of file
diff --git a/demo/scripts/customWidgetSettings.js b/src/app/customWidgetSettings.js
similarity index 100%
rename from demo/scripts/customWidgetSettings.js
rename to src/app/customWidgetSettings.js
diff --git a/demo/scripts/dataModel.js b/src/app/dataModel.js
similarity index 100%
rename from demo/scripts/dataModel.js
rename to src/app/dataModel.js
diff --git a/demo/scripts/demo.js b/src/app/demo.js
similarity index 92%
rename from demo/scripts/demo.js
rename to src/app/demo.js
index 7784ed0..8d6978e 100644
--- a/demo/scripts/demo.js
+++ b/src/app/demo.js
@@ -24,19 +24,19 @@ angular.module('app', [
.config(function ($routeProvider) {
$routeProvider
.when('/', {
- templateUrl: 'view.html',
+ templateUrl: 'app/template/view.html',
controller: 'DemoCtrl',
title: 'simple',
description: 'This is the simplest demo.'
})
.when('/resize', {
- templateUrl: 'view.html',
+ templateUrl: 'app/template/view.html',
controller: 'ResizeDemoCtrl',
title: 'resize',
description: 'This demo showcases widget resizing.'
})
.when('/custom-settings', {
- templateUrl: 'view.html',
+ templateUrl: 'app/template/view.html',
controller: 'CustomSettingsDemoCtrl',
title: 'custom widget settings',
description: 'This demo showcases overriding the widget settings dialog/modal ' +
@@ -45,7 +45,7 @@ angular.module('app', [
'that controls RandomDataModel.'
})
.when('/explicit-saving', {
- templateUrl: 'view.html',
+ templateUrl: 'app/template/view.html',
controller: 'ExplicitSaveDemoCtrl',
title: 'explicit saving',
description: 'This demo showcases an option to only save the dashboard state '+
@@ -53,7 +53,7 @@ angular.module('app', [
'updates as you make saveable changes.'
})
.when('/layouts', {
- templateUrl: 'layouts.html',
+ templateUrl: 'app/template/layouts.html',
controller: 'LayoutsDemoCtrl',
title: 'dashboard layouts',
description: 'This demo showcases the ability to have "dashboard layouts", ' +
@@ -61,7 +61,7 @@ angular.module('app', [
'information, take a look at [issue #31](https://github.com/DataTorrent/malhar-angular-dashboard/issues/31)'
})
.when('/layouts/explicit-saving', {
- templateUrl: 'layouts.html',
+ templateUrl: 'app/template/layouts.html',
controller: 'LayoutsDemoExplicitSaveCtrl',
title: 'layouts explicit saving',
description: 'This demo showcases dashboard layouts with explicit saving enabled.'
@@ -94,7 +94,7 @@ angular.module('app', [
},
{
name: 'resizable',
- templateUrl: 'template/resizable.html',
+ templateUrl: 'app/template/resizable.html',
attrs: {
class: 'demo-widget-resizable'
}
diff --git a/demo/demo.css b/src/app/demo.less
similarity index 100%
rename from demo/demo.css
rename to src/app/demo.less
diff --git a/demo/scripts/directives.js b/src/app/directives.js
similarity index 100%
rename from demo/scripts/directives.js
rename to src/app/directives.js
diff --git a/demo/scripts/explicitSave.js b/src/app/explicitSave.js
similarity index 100%
rename from demo/scripts/explicitSave.js
rename to src/app/explicitSave.js
diff --git a/src/app/index.js b/src/app/index.js
new file mode 100644
index 0000000..b4cfb97
--- /dev/null
+++ b/src/app/index.js
@@ -0,0 +1,3 @@
+'use strict';
+
+angular.module('dashboard', ['ui.bootstrap']);
diff --git a/src/app/index.less b/src/app/index.less
new file mode 100644
index 0000000..87955cb
--- /dev/null
+++ b/src/app/index.less
@@ -0,0 +1,16 @@
+.browsehappy {
+ margin: 0.2em 0;
+ background: #ccc;
+ color: #000;
+ padding: 0.2em 0;
+}
+
+.thumbnail {
+ height: 200px;
+
+ img.pull-right {
+ width: 50px;
+ }
+}
+// injector
+// endinjector
diff --git a/demo/scripts/layouts.js b/src/app/layouts.js
similarity index 100%
rename from demo/scripts/layouts.js
rename to src/app/layouts.js
diff --git a/demo/scripts/resize.js b/src/app/resize.js
similarity index 100%
rename from demo/scripts/resize.js
rename to src/app/resize.js
diff --git a/demo/template/configurableWidgetModalOptions.html b/src/app/template/configurableWidgetModalOptions.html
similarity index 100%
rename from demo/template/configurableWidgetModalOptions.html
rename to src/app/template/configurableWidgetModalOptions.html
diff --git a/demo/template/customSettingsTemplate.html b/src/app/template/customSettingsTemplate.html
similarity index 100%
rename from demo/template/customSettingsTemplate.html
rename to src/app/template/customSettingsTemplate.html
diff --git a/demo/template/fluid.html b/src/app/template/fluid.html
similarity index 100%
rename from demo/template/fluid.html
rename to src/app/template/fluid.html
diff --git a/demo/layouts.html b/src/app/template/layouts.html
similarity index 100%
rename from demo/layouts.html
rename to src/app/template/layouts.html
diff --git a/demo/template/resizable.html b/src/app/template/resizable.html
similarity index 100%
rename from demo/template/resizable.html
rename to src/app/template/resizable.html
diff --git a/demo/view.html b/src/app/template/view.html
similarity index 100%
rename from demo/view.html
rename to src/app/template/view.html
diff --git a/demo/template/widgetSpecificSettings.html b/src/app/template/widgetSpecificSettings.html
similarity index 100%
rename from demo/template/widgetSpecificSettings.html
rename to src/app/template/widgetSpecificSettings.html
diff --git a/src/app/vendor.less b/src/app/vendor.less
new file mode 100644
index 0000000..2d0f5cc
--- /dev/null
+++ b/src/app/vendor.less
@@ -0,0 +1,3 @@
+@import '../../bower_components/bootstrap/less/bootstrap.less';
+
+@icon-font-path: '/fonts/';
diff --git a/src/controllers/widgetSettingsCtrl.js b/src/components/directives/dashboard/WidgetSettingsCtrl.js
similarity index 100%
rename from src/controllers/widgetSettingsCtrl.js
rename to src/components/directives/dashboard/WidgetSettingsCtrl.js
diff --git a/template/alt-dashboard.html b/src/components/directives/dashboard/altDashboard.html
similarity index 100%
rename from template/alt-dashboard.html
rename to src/components/directives/dashboard/altDashboard.html
diff --git a/template/dashboard.html b/src/components/directives/dashboard/dashboard.html
similarity index 91%
rename from template/dashboard.html
rename to src/components/directives/dashboard/dashboard.html
index dc0b665..08dbaff 100644
--- a/template/dashboard.html
+++ b/src/components/directives/dashboard/dashboard.html
@@ -38,6 +38,7 @@
{{widget.name}}
+
diff --git a/src/directives/dashboard.js b/src/components/directives/dashboard/dashboard.js
similarity index 86%
rename from src/directives/dashboard.js
rename to src/components/directives/dashboard/dashboard.js
index bf65ee2..204f835 100644
--- a/src/directives/dashboard.js
+++ b/src/components/directives/dashboard/dashboard.js
@@ -18,11 +18,13 @@
angular.module('ui.dashboard', ['ui.bootstrap', 'ui.sortable']);
angular.module('ui.dashboard')
+
.directive('dashboard', ['WidgetModel', 'WidgetDefCollection', '$modal', 'DashboardState', '$log', function (WidgetModel, WidgetDefCollection, $modal, DashboardState, $log) {
+
return {
restrict: 'A',
templateUrl: function(element, attr) {
- return attr.templateUrl ? attr.templateUrl : 'template/dashboard.html';
+ return attr.templateUrl ? attr.templateUrl : 'components/directives/dashboard/dashboard.html';
},
scope: true,
@@ -33,7 +35,7 @@ angular.module('ui.dashboard')
hideWidgetSettings: false,
hideWidgetClose: false,
settingsModalOptions: {
- templateUrl: 'template/widget-settings-template.html',
+ templateUrl: 'components/directives/dashboard/widget-settings-template.html',
controller: 'WidgetSettingsCtrl'
},
onSettingsClose: function(result, widget) { // NOTE: dashboard scope is also passed as 3rd argument
@@ -44,18 +46,6 @@ angular.module('ui.dashboard')
}
};
- // from dashboard="options"
- // scope.options = scope.$eval(attrs.dashboard);
-
- // extend default settingsModalOptions
- // scope.options.settingsModalOptions = scope.options.settingsModalOptions || {};
-
- // extend options with defaults
- // angular.extend(defaults.settingsModalOptions, scope.options.settingsModalOptions);
- // angular.extend(scope.options.settingsModalOptions, defaults.settingsModalOptions);
- // angular.extend(defaults, scope.options);
- // angular.extend(scope.options, defaults);
-
// from dashboard="options"
scope.options = scope.$eval(attrs.dashboard);
@@ -71,14 +61,13 @@ angular.module('ui.dashboard')
// Shallow options
_.defaults(scope.options, defaults);
- // jQuery.extend(true, defaults, scope.options);
- // jQuery.extend(scope.options, defaults);
-
+ // sortable options
var sortableDefaults = {
stop: function () {
scope.saveDashboard();
},
- handle: '.widget-header'
+ handle: '.widget-header',
+ distance: 5
};
scope.sortableOptions = angular.extend({}, sortableDefaults, scope.options.sortableOptions || {});
@@ -88,7 +77,6 @@ angular.module('ui.dashboard')
// Save default widget config for reset
scope.defaultWidgets = scope.options.defaultWidgets;
- //scope.widgetDefs = scope.options.widgetDefinitions;
scope.widgetDefs = new WidgetDefCollection(scope.options.widgetDefinitions);
var count = 1;
@@ -106,6 +94,13 @@ angular.module('ui.dashboard')
* @param {Object} widgetToInstantiate The definition object of the widget to be instantiated
*/
scope.addWidget = function (widgetToInstantiate, doNotSave) {
+
+ if (typeof widgetToInstantiate === 'string') {
+ widgetToInstantiate = {
+ name: widgetToInstantiate
+ };
+ }
+
var defaultWidgetDefinition = scope.widgetDefs.getByName(widgetToInstantiate.name);
if (!defaultWidgetDefinition) {
throw 'Widget ' + widgetToInstantiate.name + ' is not found.';
@@ -113,22 +108,14 @@ angular.module('ui.dashboard')
// Determine the title for the new widget
var title;
- if (widgetToInstantiate.title) {
- title = widgetToInstantiate.title;
- } else if (defaultWidgetDefinition.title) {
- title = defaultWidgetDefinition.title;
- } else {
- title = 'Widget ' + count++;
+ if (!widgetToInstantiate.title && !defaultWidgetDefinition.title) {
+ widgetToInstantiate.title = 'Widget ' + count++;
}
- // Deep extend a new object for instantiation
- widgetToInstantiate = jQuery.extend(true, {}, defaultWidgetDefinition, widgetToInstantiate);
-
// Instantiation
- var widget = new WidgetModel(widgetToInstantiate, {
- title: title
- });
+ var widget = new WidgetModel(defaultWidgetDefinition, widgetToInstantiate);
+ // Add to the widgets array
scope.widgets.push(widget);
if (!doNotSave) {
scope.saveDashboard();
diff --git a/dist/angular-ui-dashboard.css b/src/components/directives/dashboard/dashboard.less
similarity index 100%
rename from dist/angular-ui-dashboard.css
rename to src/components/directives/dashboard/dashboard.less
diff --git a/test/spec/dashboard.spec.js b/src/components/directives/dashboard/dashboard.spec.js
similarity index 98%
rename from test/spec/dashboard.spec.js
rename to src/components/directives/dashboard/dashboard.spec.js
index f99da8c..610b348 100644
--- a/test/spec/dashboard.spec.js
+++ b/src/components/directives/dashboard/dashboard.spec.js
@@ -214,6 +214,13 @@ describe('Directive: dashboard', function () {
expect(childScope.saveDashboard).toHaveBeenCalled();
});
+ it('should support passing just the widget name as a string', function() {
+ spyOn(childScope.widgetDefs, 'getByName').and.returnValue({ title: 'defaultTitle', name: 'A' });
+ childScope.addWidget('A');
+ expect(childScope.widgetDefs.getByName).toHaveBeenCalledWith('A');
+ expect(widgetCreated.title).toEqual('defaultTitle');
+ });
+
describe('@awashbrook Test Case', function() {
beforeEach(function() {
spyOn(childScope.widgetDefs, 'getByName').and.returnValue(widgetDefault = {
@@ -476,7 +483,7 @@ describe('Directive: dashboard', function () {
expect(modalOptions.resolve.widget() === widget).toEqual(true);
});
- it('should set the templateUrl in modal options to the default ("template/widget-settings-template.html")', function() {
+ it('should set the templateUrl in modal options to the default ("components/directives/dashboard/widget-settings-template.html")', function() {
var widget = {};
var dfr = $q.defer();
spyOn(mockModal, 'open').and.callFake(function(options) {
@@ -486,7 +493,7 @@ describe('Directive: dashboard', function () {
};
});
childScope.openWidgetSettings(widget);
- expect(modalOptions.templateUrl).toEqual('template/widget-settings-template.html');
+ expect(modalOptions.templateUrl).toEqual('components/directives/dashboard/widget-settings-template.html');
});
it('should set the templateUrl in modal options to scope.options.settingsModalOptions.templateUrl', function() {
diff --git a/template/widget-settings-template.html b/src/components/directives/dashboard/widget-settings-template.html
similarity index 100%
rename from template/widget-settings-template.html
rename to src/components/directives/dashboard/widget-settings-template.html
diff --git a/template/save-changes-modal.html b/src/components/directives/dashboardLayouts/SaveChangesModal.html
similarity index 100%
rename from template/save-changes-modal.html
rename to src/components/directives/dashboardLayouts/SaveChangesModal.html
diff --git a/src/controllers/SaveChangesModalCtrl.js b/src/components/directives/dashboardLayouts/SaveChangesModalCtrl.js
similarity index 100%
rename from src/controllers/SaveChangesModalCtrl.js
rename to src/components/directives/dashboardLayouts/SaveChangesModalCtrl.js
diff --git a/template/dashboard-layouts.html b/src/components/directives/dashboardLayouts/dashboardLayouts.html
similarity index 92%
rename from template/dashboard-layouts.html
rename to src/components/directives/dashboardLayouts/dashboardLayouts.html
index 969b6ab..6dcad78 100644
--- a/template/dashboard-layouts.html
+++ b/src/components/directives/dashboardLayouts/dashboardLayouts.html
@@ -16,4 +16,4 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/src/directives/dashboard-layouts.js b/src/components/directives/dashboardLayouts/dashboardLayouts.js
similarity index 95%
rename from src/directives/dashboard-layouts.js
rename to src/components/directives/dashboardLayouts/dashboardLayouts.js
index 28a9420..3477d54 100644
--- a/src/directives/dashboard-layouts.js
+++ b/src/components/directives/dashboardLayouts/dashboardLayouts.js
@@ -22,7 +22,7 @@ angular.module('ui.dashboard')
return {
scope: true,
templateUrl: function(element, attr) {
- return attr.templateUrl ? attr.templateUrl : 'template/dashboard-layouts.html';
+ return attr.templateUrl ? attr.templateUrl : 'components/directives/dashboardLayouts/dashboardLayouts.html';
},
link: function(scope, element, attrs) {
@@ -54,7 +54,7 @@ angular.module('ui.dashboard')
if (current && current.dashboard.unsavedChangeCount) {
var modalInstance = $modal.open({
- templateUrl: 'template/save-changes-modal.html',
+ templateUrl: 'template/SaveChangesModal.html',
resolve: {
layout: function() {
return layout;
@@ -139,6 +139,7 @@ angular.module('ui.dashboard')
stop: function() {
scope.options.saveLayouts();
},
+ distance: 5
};
scope.sortableOptions = angular.extend({}, sortableDefaults, scope.options.sortableOptions || {});
}
diff --git a/test/spec/dashboard-layouts.spec.js b/src/components/directives/dashboardLayouts/dashboardLayouts.spec.js
similarity index 95%
rename from test/spec/dashboard-layouts.spec.js
rename to src/components/directives/dashboardLayouts/dashboardLayouts.spec.js
index 5369b8e..7d26b67 100644
--- a/test/spec/dashboard-layouts.spec.js
+++ b/src/components/directives/dashboardLayouts/dashboardLayouts.spec.js
@@ -335,8 +335,9 @@ describe('Directive: dashboard-layouts', function () {
it('should call dashboard.addWidget method of the active layout', function() {
options.addWidget(1,2,3);
expect(mockDash.dashboard.addWidget).toHaveBeenCalled();
- expect(mockDash.dashboard.addWidget.calls.first().object).toEqual(mockDash.dashboard);
- expect(mockDash.dashboard.addWidget.calls.first().args).toEqual([1,2,3]);
+ var firstCall = mockDash.dashboard.addWidget.calls.first();
+ expect(firstCall.object).toEqual(mockDash.dashboard);
+ expect(firstCall.args).toEqual([1,2,3]);
});
it('should do nothing if there is no active layout', function() {
@@ -353,8 +354,9 @@ describe('Directive: dashboard-layouts', function () {
it('should call dashboard.loadWidgets of the current layout', function() {
options.loadWidgets(1,2,3);
expect(mockDash.dashboard.loadWidgets).toHaveBeenCalled();
- expect(mockDash.dashboard.loadWidgets.calls.first().object).toEqual(mockDash.dashboard);
- expect(mockDash.dashboard.loadWidgets.calls.first().args).toEqual([1,2,3]);
+ var firstCall = mockDash.dashboard.loadWidgets.calls.first();
+ expect(firstCall.object).toEqual(mockDash.dashboard);
+ expect(firstCall.args).toEqual([1,2,3]);
});
it('should do nothing if there is no active layout', function() {
@@ -371,8 +373,9 @@ describe('Directive: dashboard-layouts', function () {
it('should call dashboard.saveDashboard of the current layout', function() {
options.saveDashboard(1,2,3);
expect(mockDash.dashboard.saveDashboard).toHaveBeenCalled();
- expect(mockDash.dashboard.saveDashboard.calls.first().object).toEqual(mockDash.dashboard);
- expect(mockDash.dashboard.saveDashboard.calls.first().args).toEqual([1,2,3]);
+ var firstCall = mockDash.dashboard.saveDashboard.calls.first();
+ expect(firstCall.object).toEqual(mockDash.dashboard);
+ expect(firstCall.args).toEqual([1,2,3]);
});
it('should do nothing if there is no active layout', function() {
diff --git a/src/controllers/dashboardWidgetCtrl.js b/src/components/directives/widget/DashboardWidgetCtrl.js
similarity index 100%
rename from src/controllers/dashboardWidgetCtrl.js
rename to src/components/directives/widget/DashboardWidgetCtrl.js
diff --git a/test/spec/dashboardWidgetCtrl.js b/src/components/directives/widget/DashboardWidgetCtrl.spec.js
similarity index 100%
rename from test/spec/dashboardWidgetCtrl.js
rename to src/components/directives/widget/DashboardWidgetCtrl.spec.js
diff --git a/src/directives/widget.js b/src/components/directives/widget/widget.js
similarity index 100%
rename from src/directives/widget.js
rename to src/components/directives/widget/widget.js
diff --git a/test/spec/widget.js b/src/components/directives/widget/widget.spec.js
similarity index 100%
rename from test/spec/widget.js
rename to src/components/directives/widget/widget.spec.js
diff --git a/src/models/dashboardState.js b/src/components/models/DashboardState.js
similarity index 94%
rename from src/models/dashboardState.js
rename to src/components/models/DashboardState.js
index e28f538..67948ea 100644
--- a/src/models/dashboardState.js
+++ b/src/components/models/DashboardState.js
@@ -41,17 +41,7 @@ angular.module('ui.dashboard')
}
var serialized = _.map(widgets, function (widget) {
- var widgetObject = {
- title: widget.title,
- name: widget.name,
- style: widget.style,
- size: widget.size,
- dataModelOptions: widget.dataModelOptions,
- storageHash: widget.storageHash,
- attrs: widget.attrs
- };
-
- return widgetObject;
+ return widget.serialize();
});
var item = { widgets: serialized, hash: this.hash };
diff --git a/src/models/LayoutStorage.js b/src/components/models/LayoutStorage.js
similarity index 98%
rename from src/models/LayoutStorage.js
rename to src/components/models/LayoutStorage.js
index 39662f8..3685fd3 100644
--- a/src/models/LayoutStorage.js
+++ b/src/components/models/LayoutStorage.js
@@ -77,7 +77,7 @@ angular.module('ui.dashboard')
layout.dashboard = layout.dashboard || {};
layout.dashboard.storage = self;
layout.dashboard.storageId = layout.id = self._getLayoutId.call(self,layout);
- layout.dashboard.widgetDefinitions = self.widgetDefinitions;
+ layout.dashboard.widgetDefinitions = layout.widgetDefinitions || self.widgetDefinitions;
layout.dashboard.stringifyStorage = false;
layout.dashboard.defaultWidgets = layout.defaultWidgets || self.defaultWidgets;
layout.dashboard.widgetButtons = self.widgetButtons;
diff --git a/test/spec/LayoutStorage.spec.js b/src/components/models/LayoutStorage.spec.js
similarity index 94%
rename from test/spec/LayoutStorage.spec.js
rename to src/components/models/LayoutStorage.spec.js
index c92ec7a..3310cad 100644
--- a/test/spec/LayoutStorage.spec.js
+++ b/src/components/models/LayoutStorage.spec.js
@@ -340,6 +340,26 @@ describe('Factory: LayoutStorage', function () {
expect(newLayouts[1].dashboard.defaultWidgets).toEqual(options.defaultWidgets);
});
+ it('should look for widgetDefinitions on storage options if not supplied on layout definition', function() {
+ options.widgetDefinitions = [{name: 'a'}, {name: 'b'}, {name: 'c'}];
+ storage = new LayoutStorage(options);
+
+ var newLayouts = [ { title: 'my-layout', widgetDefinitions: [] }, { title: 'my-layout-2' } ];
+ storage.add(newLayouts);
+ expect(newLayouts[0].dashboard.widgetDefinitions === newLayouts[0].widgetDefinitions).toEqual(true);
+ expect(newLayouts[1].dashboard.widgetDefinitions === options.widgetDefinitions).toEqual(true);
+ });
+
+ it('should use widgetDefinitions if supplied in the layout definition', function() {
+ options.widgetDefinitions = [{name: 'a'}, {name: 'b'}, {name: 'c'}];
+ storage = new LayoutStorage(options);
+
+ var newLayouts = [ { title: 'my-layout', widgetDefinitions: [] }, { title: 'my-layout-2' } ];
+ storage.add(newLayouts);
+ expect(newLayouts[0].dashboard.widgetDefinitions).toEqual([]);
+ expect(newLayouts[1].dashboard.widgetDefinitions).toEqual(options.widgetDefinitions);
+ });
+
});
describe('the remove method', function() {
diff --git a/src/models/widgetDataModel.js b/src/components/models/WidgetDataModel.js
similarity index 100%
rename from src/models/widgetDataModel.js
rename to src/components/models/WidgetDataModel.js
diff --git a/src/models/widgetDefCollection.js b/src/components/models/WidgetDefCollection.js
similarity index 77%
rename from src/models/widgetDefCollection.js
rename to src/components/models/WidgetDefCollection.js
index e49c90e..beaa214 100644
--- a/src/models/widgetDefCollection.js
+++ b/src/components/models/WidgetDefCollection.js
@@ -18,7 +18,18 @@
angular.module('ui.dashboard')
.factory('WidgetDefCollection', function () {
+
+ function convertToDefinition(d) {
+ if (typeof d === 'function') {
+ return new d();
+ }
+ return d;
+ }
+
function WidgetDefCollection(widgetDefs) {
+
+ widgetDefs = widgetDefs.map(convertToDefinition);
+
this.push.apply(this, widgetDefs);
// build (name -> widget definition) map for widget lookup by name
@@ -35,5 +46,11 @@ angular.module('ui.dashboard')
return this.map[name];
};
+ WidgetDefCollection.prototype.add = function(def) {
+ def = convertToDefinition(def);
+ this.push(def);
+ this.map[def.name] = def;
+ }
+
return WidgetDefCollection;
});
\ No newline at end of file
diff --git a/src/models/widgetModel.js b/src/components/models/WidgetModel.js
similarity index 65%
rename from src/models/widgetModel.js
rename to src/components/models/WidgetModel.js
index 7f0a533..3799b77 100644
--- a/src/models/widgetModel.js
+++ b/src/components/models/WidgetModel.js
@@ -18,38 +18,28 @@
angular.module('ui.dashboard')
.factory('WidgetModel', function ($log) {
+
+ function defaults() {
+ return {
+ title: 'Widget',
+ style: {},
+ size: {},
+ enableVerticalResize: true,
+ containerStyle: { width: '33%' }, // default width
+ contentStyle: {}
+ };
+ };
+
// constructor for widget model instances
- function WidgetModel(Class, overrides) {
- var defaults = {
- title: 'Widget',
- name: Class.name,
- attrs: Class.attrs,
- dataAttrName: Class.dataAttrName,
- dataModelType: Class.dataModelType,
- dataModelArgs: Class.dataModelArgs, // used in data model constructor, not serialized
- //AW Need deep copy of options to support widget options editing
- dataModelOptions: Class.dataModelOptions,
- settingsModalOptions: Class.settingsModalOptions,
- onSettingsClose: Class.onSettingsClose,
- onSettingsDismiss: Class.onSettingsDismiss,
- style: Class.style || {},
- size: Class.size || {},
- enableVerticalResize: (Class.enableVerticalResize === false) ? false : true
- };
-
- overrides = overrides || {};
- angular.extend(this, angular.copy(defaults), overrides);
- this.containerStyle = { width: '33%' }; // default width
- this.contentStyle = {};
+ function WidgetModel(widgetDefinition, overrides) {
+
+ // Extend this with the widget definition object with overrides merged in (deep extended).
+ angular.extend(this, defaults(), _.merge(angular.copy(widgetDefinition), overrides));
+
this.updateContainerStyle(this.style);
- if (Class.templateUrl) {
- this.templateUrl = Class.templateUrl;
- } else if (Class.template) {
- this.template = Class.template;
- } else {
- var directive = Class.directive || Class.name;
- this.directive = directive;
+ if (!this.templateUrl && !this.template && !this.directive) {
+ this.directive = widgetDefinition.name;
}
if (this.size && _.has(this.size, 'height')) {
@@ -107,6 +97,9 @@ angular.module('ui.dashboard')
updateContainerStyle: function (style) {
angular.extend(this.containerStyle, style);
+ },
+ serialize: function() {
+ return _.pick(this, ['title', 'name', 'style', 'size', 'dataModelOptions', 'attrs', 'storageHash']);
}
};
diff --git a/test/spec/widgetModel.js b/src/components/models/WidgetModel.spec.js
similarity index 92%
rename from test/spec/widgetModel.js
rename to src/components/models/WidgetModel.spec.js
index ec508c2..151e560 100644
--- a/test/spec/widgetModel.js
+++ b/src/components/models/WidgetModel.spec.js
@@ -28,7 +28,11 @@ describe('Factory: WidgetModel', function () {
style: { width: '10em' },
settingsModalOptions: {},
onSettingsClose: function() {},
- onSettingsDismiss: function() {}
+ onSettingsDismiss: function() {},
+ funkyChicken: {
+ cool: false,
+ fun: true
+ }
};
Class2 = {
@@ -64,14 +68,15 @@ describe('Factory: WidgetModel', function () {
expect(m.style.width).toEqual('15em');
});
- it('should set templateUrl if and only if it is present on Class', function() {
- var m2 = new WidgetModel(Class2, overrides);
- expect(m2.templateUrl).toEqual('my/url.html');
+ it('should copy arbitrary data from the widget definition', function() {
+ expect(m.funkyChicken.cool).toEqual(false);
+ expect(m.funkyChicken.fun).toEqual(true);
+ expect(m.funkyChicken===Class.funkyChicken).toEqual(false);
});
- it('should NOT set template if templateUrl was specified', function() {
+ it('should set templateUrl if and only if it is present on Class', function() {
var m2 = new WidgetModel(Class2, overrides);
- expect(m2.template).toBeUndefined();
+ expect(m2.templateUrl).toEqual('my/url.html');
});
it('should set template if and only if it is present on Class', function() {
diff --git a/src/favicon.ico b/src/favicon.ico
new file mode 100644
index 0000000..6527905
Binary files /dev/null and b/src/favicon.ico differ
diff --git a/src/index.html b/src/index.html
new file mode 100644
index 0000000..c46098e
--- /dev/null
+++ b/src/index.html
@@ -0,0 +1,73 @@
+
+
+
+
+ Malhar Angular Dashboard
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/template/widget-default-content.html b/template/widget-default-content.html
deleted file mode 100644
index e69de29..0000000