summaryrefslogtreecommitdiffstats
path: root/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components')
-rw-r--r--ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/angular-markdown-directive/markdown.js36
-rw-r--r--ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboard/WidgetSettingsCtrl.js34
-rw-r--r--ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboard/WidgetSettingsRaptorReportCtrl.js215
-rw-r--r--ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboard/add-raptor-report-template.html26
-rw-r--r--ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboard/add-rcloud-notebook-template.html22
-rw-r--r--ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboard/altDashboard.html49
-rw-r--r--ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboard/dashboard.html74
-rw-r--r--ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboard/dashboard.js427
-rw-r--r--ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboard/dashboard.less88
-rw-r--r--ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboard/dashboard.spec.js878
-rw-r--r--ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboard/widget-settings-raptor-report-template.html58
-rw-r--r--ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboard/widget-settings-template.html22
-rw-r--r--ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboardLayouts/SaveChangesModal.html13
-rw-r--r--ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboardLayouts/SaveChangesModalCtrl.js32
-rw-r--r--ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboardLayouts/dashboardLayouts.html19
-rw-r--r--ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboardLayouts/dashboardLayouts.js151
-rw-r--r--ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboardLayouts/dashboardLayouts.spec.js392
-rw-r--r--ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/widget/DashboardWidgetCtrl.js246
-rw-r--r--ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/widget/DashboardWidgetCtrl.spec.js164
-rw-r--r--ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/widget/widget.js64
-rw-r--r--ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/widget/widget.spec.js104
-rw-r--r--ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/models/DashboardState.js180
-rw-r--r--ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/models/LayoutStorage.js253
-rw-r--r--ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/models/LayoutStorage.spec.js692
-rw-r--r--ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/models/WidgetDataModel.js45
-rw-r--r--ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/models/WidgetDefCollection.js56
-rw-r--r--ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/models/WidgetModel.js112
-rw-r--r--ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/models/WidgetModel.spec.js156
28 files changed, 4608 insertions, 0 deletions
diff --git a/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/angular-markdown-directive/markdown.js b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/angular-markdown-directive/markdown.js
new file mode 100644
index 00000000..989a6bec
--- /dev/null
+++ b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/angular-markdown-directive/markdown.js
@@ -0,0 +1,36 @@
+/*
+ * angular-markdown-directive v0.3.1
+ * (c) 2013-2014 Brian Ford http://briantford.com
+ * License: MIT
+ */
+
+'use strict';
+
+angular.module('btford.markdown', ['ngSanitize']).
+ provider('markdownConverter', function () {
+ var opts = {};
+ return {
+ config: function (newOpts) {
+ opts = newOpts;
+ },
+ $get: function () {
+ return new Showdown.converter(opts);
+ }
+ };
+ }).
+ directive('btfMarkdown', ['$sanitize', 'markdownConverter', function ($sanitize, markdownConverter) {
+ return {
+ restrict: 'AE',
+ link: function (scope, element, attrs) {
+ if (attrs.btfMarkdown) {
+ scope.$watch(attrs.btfMarkdown, function (newVal) {
+ var html = newVal ? $sanitize(markdownConverter.makeHtml(newVal)) : '';
+ element.html(html);
+ });
+ } else {
+ var html = $sanitize(markdownConverter.makeHtml(element.text()));
+ element.html(html);
+ }
+ }
+ };
+ }]);
diff --git a/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboard/WidgetSettingsCtrl.js b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboard/WidgetSettingsCtrl.js
new file mode 100644
index 00000000..4b420a27
--- /dev/null
+++ b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboard/WidgetSettingsCtrl.js
@@ -0,0 +1,34 @@
+/*
+ * 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('WidgetSettingsCtrl', ['$scope', '$uibModalInstance', 'widget', function ($scope, $uibModalInstance, widget) {
+ // add widget to scope
+ $scope.widget = widget;
+
+ // set up result object
+ $scope.result = jQuery.extend(true, {}, widget);
+
+ $scope.ok = function () {
+ $uibModalInstance.close($scope.result);
+ };
+
+ $scope.cancel = function () {
+ $uibModalInstance.dismiss('cancel');
+ };
+ }]); \ No newline at end of file
diff --git a/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboard/WidgetSettingsRaptorReportCtrl.js b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboard/WidgetSettingsRaptorReportCtrl.js
new file mode 100644
index 00000000..fd6a0b02
--- /dev/null
+++ b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboard/WidgetSettingsRaptorReportCtrl.js
@@ -0,0 +1,215 @@
+/*
+ * 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('WidgetSettingsRaptorReportCtrl', ['$http','$scope','$rootScope','$uibModalInstance', 'widget', function ($http,$scope,$rootScope,$uibModalInstance, widget) {
+
+ // add watch function for widget here
+ // leave ajax call to the dashboard.js
+
+ console.log("============= WidgetSettingsRaptorReportCtrl scope =================");
+ console.log($scope);
+
+ var getFormFieldListUrl = "raptor.htm?action=report.run.container&c_master="+widget.report_id + "&refresh=Y"
+ console.log("============= getFormFieldListUrl =============");
+ console.log(getFormFieldListUrl);
+ $http.get(getFormFieldListUrl).then(
+ function(res){
+ $scope.reportData = res.data;
+ // add widget to scope
+ $scope.showFormFieldIds = false;
+ $scope.formFieldSelectedValues = {};
+});
+
+ var parseQueryString = function( queryString ) {
+ var params = {}, queries, temp, i, l;
+ // Split into key/value pairs
+ queries = queryString.split("&");
+ // Convert the array of strings into an object
+ for ( i = 0, l = queries.length; i < l; i++ ) {
+ temp = queries[i].split('=');
+ //console.log(temp[0]);
+ //console.log(temp[0] != "refresh");
+ if(temp[0] && temp[0] != "refresh")
+ params[temp[0]] = temp[1];
+ }
+ return params;
+ };
+
+ var paginationOptions = {
+ pageNumber: 1,
+ pageSize: 5,
+ sort: null
+ };
+
+ $scope.gridOptions = {
+ paginationPageSizes: [5],
+ paginationPageSize: 5,
+ useExternalPagination: true,
+ columnDefs: [],
+ data: [],
+ enableGridMenu: true,
+ enableSelectAll: true,
+ exporterMenuPdf: false,
+ exporterMenuCsv: false,
+ exporterCsvFilename: 'myFile.csv',
+ exporterPdfDefaultStyle: {fontSize: 9},
+ exporterPdfTableStyle: {margin: [30, 30, 30, 30]},
+ exporterPdfTableHeaderStyle: {fontSize: 10, bol$rootScoped: true, italics: true, color: 'red'},
+ exporterPdfHeader: { text: "My Header", style: 'headerStyle' },
+ exporterPdfFooter: function ( currentPage, pageCount ) {
+ return { text: currentPage.toString() + ' of ' + pageCount.toString(), style: 'footerStyle' };
+ },
+ exporterPdfCustomFormatter: function ( docDefinition ) {
+ docDefinition.styles.headerStyle = { fontSize: 22, bold: true };
+ docDefinition.styles.footerStyle = { fontSize: 10, bold: true };
+ return docDefinition;
+ },
+ exporterPdfOrientation: 'portrait',
+ exporterPdfPageSize: 'LETTER',
+ exporterPdfMaxGridWidth: 500,
+ exporterCsvLinkElement: angular.element(document.querySelectorAll(".custom-csv-link-location")),
+ onRegisterApi: function(gridApi) {
+ $scope.gridApi = gridApi;
+ gridApi.pagination.on.paginationChanged($scope, function (newPage, pageSize) {
+ paginationOptions.pageNumber = newPage;
+ paginationOptions.pageSize = pageSize;
+ $scope.runReport();
+ });
+ }
+ };
+
+
+
+ $scope.getFormFieldSelectedValuesAsURL = function(){
+ var formFieldsUrl = '';
+ $scope.widget.reportData.formFieldList.forEach(function(formField) {
+ if(formField.fieldType==='LIST_BOX') {
+ if($scope.formFieldSelectedValues && $scope.formFieldSelectedValues[formField.fieldId] && $scope.formFieldSelectedValues[formField.fieldId].value != '') {
+ formFieldsUrl = formFieldsUrl+formField.fieldId+'='+$scope.formFieldSelectedValues[formField.fieldId].value+'&';
+ }
+ } else if(formField.fieldType==='LIST_MULTI_SELECT') {
+ if($scope.formFieldSelectedValues[formField.fieldId].length >0) {
+ for (var i = 0; i < $scope.formFieldSelectedValues[formField.fieldId].length; i++) {
+ if($scope.formFieldSelectedValues[formField.fieldId][i].defaultValue){
+ formFieldsUrl = formFieldsUrl+formField.fieldId+'='+$scope.formFieldSelectedValues[formField.fieldId][i].value+'&';
+ }
+ }
+ }
+ } else if((formField.fieldType === 'text' || formField.fieldType === 'TEXT') && formField.validationType === 'DATE'){
+ formFieldsUrl = formFieldsUrl+formField.fieldId+'='+dateFilter($scope.formFieldSelectedValues[formField.fieldId],$scope.dateformat)+'&';
+ } else if((formField.fieldType === 'text' || formField.fieldType === 'TEXT') && formField.validationType === 'TIMESTAMP_MIN'){
+ formFieldsUrl = formFieldsUrl+formField.fieldId+'='+dateFilter($scope.formFieldSelectedValues[formField.fieldId],$scope.datetimeformat)+'&';
+ } else if((formField.fieldType === 'text' || formField.fieldType === 'TEXT') && $scope.formFieldSelectedValues[formField.fieldId] && $scope.formFieldSelectedValues[formField.fieldId] != ''){
+ formFieldsUrl = formFieldsUrl+formField.fieldId+'='+$scope.formFieldSelectedValues[formField.fieldId]+'&';
+ }
+ });
+ return formFieldsUrl;
+
+ }
+
+ $scope.triggerOtherFormFields = function(){
+ console.log("report_run");
+ var formFieldsUrl = $scope.getFormFieldSelectedValuesAsURL();
+ $http.get('raptor.htm?action=report.formfields.run.container&c_master='+widget.report_id+'&'+formFieldsUrl).then(
+ function(response){
+ $scope.widget.reportData = response.data;
+ });
+ };
+
+
+ $scope.runReport = function(pagination){
+ var formFieldsUrl = $scope.getFormFieldSelectedValuesAsURL();
+ console.log("pagination");
+ if(!pagination) {
+ console.log("refreshed ...");
+ $scope.gridOptions.pageNumber = 1;
+ $scope.gridOptions.paginationPageSizes= [widget.reportData.pageSize];
+ $scope.gridOptions.paginationPageSize= widget.reportData.pageSize;
+ if(widget.reportData.totalRows<14){
+ $scope.gridHeight = (widget.reportData.totalRows+7)*30+'px';
+ } else{
+ $scope.gridHeight = '400px';
+ }
+ $scope.gridOptions.totalItems = widget.reportData.totalRows;
+ $scope.gridOptions.data= widget.reportData.reportDataRows;
+ $scope.gridOptions.exporterPdfHeader.text= widget.reportData.reportName;
+ }
+/* $scope.currentReportUrlParams = 'c_master='+$scope.urlParams.c_master+'&'+formFieldsUrl+'&display_content=Y&r_page='+(paginationOptions.pageNumber-1);
+ console.log('raptor.htm?action=report.run.container&c_master='+$scope.urlParams.c_master+'&'+formFieldsUrl+'refresh=Y&display_content=Y&r_page='+(paginationOptions.pageNumber-1));
+ $http.get('raptor.htm?action=report.run.container&c_master='+$scope.urlParams.c_master+'&'+formFieldsUrl+'refresh=Y&display_content=Y&r_page='+(paginationOptions.pageNumber-1)).then(
+ */
+ $scope.currentReportUrlParams = 'c_master='+ widget.report_id+'&'+formFieldsUrl+'&display_content=Y&r_page='+(paginationOptions.pageNumber-1);
+ $scope.urlParams = parseQueryString($scope.currentReportUrlParams);
+
+ console.log('raptor.htm?action=report.run.container&c_master='+ widget.report_id +'&'+formFieldsUrl+'refresh=Y&display_content=Y&r_page='+(paginationOptions.pageNumber-1));
+ $http.get('raptor.htm?action=report.run.container&c_master='+widget.report_id+'&'+formFieldsUrl+'refresh=Y&display_content=Y&r_page='+(paginationOptions.pageNumber-1)).then(
+ function(response){
+ widget.reportData = response.data;
+ if(widget.reportData.errormessage) {
+ document.getElementById('errorDiv').innerHTML = widget.reportData.errormessage;
+ console.log(document.getElementById('errorDiv').innerHtml);
+ console.log(widget.reportData.errormessage);
+ }
+ if(!pagination) {
+ if(!$scope.urlParams.hideChart && widget.reportData.chartAvailable && widget.reportData.totalRows>1){
+ console.log('raptor.htm?action=chart.run&c_master='+widget.report_id+'&'+formFieldsUrl+'display_content=Y&r_page='+(paginationOptions.pageNumber-1));
+ $http.get('raptor.htm?action=chart.run&c_master='+widget.report_id +'&'+formFieldsUrl+'display_content=Y&r_page='+(paginationOptions.pageNumber-1)).then(
+ function(response) {
+ console.log(response.data);
+ $scope.showChart = true;
+ document.getElementById('chartiframe').contentWindow.document.write(response.data);
+ document.getElementById('chartiframe').contentWindow.document.close();
+ });
+ } else {
+ $scope.showChart = false;
+ }
+ }
+ if($scope.reportData.displayForm && $scope.reportData.formFieldList && $scope.reportData.formFieldList.length>0 && !$scope.urlParams.hideFormFields){
+ $scope.showFormFields = true;
+ } else {
+ $scope.showFormFields = false;
+ }
+ });
+ $rootScope.gridOptions = $scope.gridOptions;
+ $rootScope.gridHeight = $scope.gridHeight;
+ $rootScope.showdataContainer = true;
+ };
+
+
+
+
+
+ // set up result object
+ $scope.result = jQuery.extend(true, {}, widget);
+
+ $scope.ok = function () {
+ $uibModalInstance.close($scope.result);
+ };
+
+ $scope.okay = function () {
+ console.log("$scope.okay!")
+ console.log($scope);
+ $scope.runReport();
+/* $uibModalInstance.close($scope.result);*/
+ };
+
+ $scope.cancel = function () {
+ $uibModalInstance.dismiss('cancel');
+ };
+ }]); \ No newline at end of file
diff --git a/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboard/add-raptor-report-template.html b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboard/add-raptor-report-template.html
new file mode 100644
index 00000000..00d6c41a
--- /dev/null
+++ b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboard/add-raptor-report-template.html
@@ -0,0 +1,26 @@
+<div>
+ <div class="modal-header">
+ <h3 class="modal-title"> Add a Raptor Report</h3>
+ </div>
+
+ <div >
+ <form name="addRaptorReportForm" class="css-form" novalidate>
+ <div style="width: 100%;">
+ <div style="width: 20%; margin-left: 50px;margin-top:28px; float: left; font-size: 15px;">Report Name: </div>
+ <div style="margin-top:20px;" class="form-field" att-select="raptorReportList" ng-model="selectedRaptorReport" placeholder="Select a Raptor Report" no-filter="true"></div>
+
+ <form action="">
+ <input name="radio" type="radio" ng-model="radioValue" att-radio="chart" title="chart" aria-label="chart radio"> chart
+ <input name="radio" type="radio" ng-model="radioValue" att-radio="data" title="data" aria-label="data radio"> data
+ </form>
+ </div>
+
+ <div class="modal-footer">
+ <a att-button btn-type="primary" ng-click="ok()">OK</a>
+ <a att-button btn-type="primary" ng-click="cancel()">Cancel</a>
+ </div>
+
+ </form>
+ <br />
+ </div>
+</div> \ No newline at end of file
diff --git a/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboard/add-rcloud-notebook-template.html b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboard/add-rcloud-notebook-template.html
new file mode 100644
index 00000000..239497c9
--- /dev/null
+++ b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboard/add-rcloud-notebook-template.html
@@ -0,0 +1,22 @@
+<div>
+ <div class="modal-header">
+ <h3 class="modal-title"> Add a Raptor Report</h3>
+ </div>
+
+ <div >
+ <form name="addRaptorReportForm" class="css-form" novalidate>
+ <div style="width: 100%;">
+ <div style="margin-left: 50px;margin-top:28px; font-size: 15px;">RCloud Notebook URL: </div>
+<!-- <div style="margin-top:20px;" class="form-field" att-select="raptorReportList" ng-model="selectedRaptorReport" placeholder="Select a Raptor Report" no-filter="true"></div> -->
+ <textarea style="margin-left: 50px; width:450px; height: 150px;" ng-model="rcloud_url" name="content">
+ </textarea>
+ </div>
+ <div class="modal-footer">
+ <a att-button btn-type="primary" ng-click="ok()">OK</a>
+ <a att-button btn-type="primary" ng-click="cancel()">Cancel</a>
+ </div>
+
+ </form>
+ <br />
+ </div>
+</div> \ No newline at end of file
diff --git a/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboard/altDashboard.html b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboard/altDashboard.html
new file mode 100644
index 00000000..189bccea
--- /dev/null
+++ b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboard/altDashboard.html
@@ -0,0 +1,49 @@
+<div>
+ <div class="btn-toolbar" ng-if="!options.hideToolbar">
+ <div class="btn-group" ng-if="!options.widgetButtons">
+ <span class="dropdown" on-toggle="toggled(open)">
+ <button type="button" class="btn btn-primary dropdown-toggle" ng-disabled="disabled">
+ Button dropdown <span class="caret"></span>
+ </button>
+ <ul class="dropdown-menu" role="menu">
+ <li ng-repeat="widget in widgetDefs">
+ <a href="#" ng-click="addWidgetInternal($event, widget);" class="dropdown-toggle">{{widget.name}}</a>
+ </li>
+ </ul>
+ </span>
+ </div>
+
+ <div class="btn-group" ng-if="options.widgetButtons">
+ <button ng-repeat="widget in widgetDefs"
+ ng-click="addWidgetInternal($event, widget);" type="button" class="btn btn-primary">
+ {{widget.name}}
+ </button>
+ </div>
+
+ <button class="btn btn-warning" ng-click="resetWidgetsToDefault()">Default Widgets</button>
+
+ <button ng-if="options.storage && options.explicitSave" ng-click="options.saveDashboard()" class="btn btn-success" ng-hide="!options.unsavedChangeCount">{{ !options.unsavedChangeCount ? "Alternative - No Changes" : "Save" }}</button>
+
+ <button ng-click="clear();" ng-hide="!widgets.length" type="button" class="btn btn-info">Clear</button>
+ </div>
+
+ <div ui-sortable="sortableOptions" ng-model="widgets" class="dashboard-widget-area">
+ <div ng-repeat="widget in widgets" ng-style="widget.style" class="widget-container" widget>
+ <div class="widget panel panel-default">
+ <div class="widget-header panel-heading">
+ <h3 class="panel-title">
+ <span class="widget-title" ng-dblclick="editTitle(widget)" ng-hide="widget.editingTitle">{{widget.title}}</span>
+ <form action="" class="widget-title" ng-show="widget.editingTitle" ng-submit="saveTitleEdit(widget)">
+ <input type="text" ng-model="widget.title" class="form-control">
+ </form>
+ <span class="label label-primary" ng-if="!options.hideWidgetName">{{widget.name}}</span>
+ <span ng-click="removeWidget(widget);" class="glyphicon glyphicon-remove icon-erase" ng-if="!options.hideWidgetClose"></span>
+ <span ng-click="openWidgetSettings(widget);" class="glyphicon glyphicon-cog icon-settings" ng-if="!options.hideWidgetSettings"></span>
+ </h3>
+ </div>
+ <div class="panel-body widget-content"></div>
+ <div class="widget-ew-resizer" ng-mousedown="grabResizer($event)"></div>
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboard/dashboard.html b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboard/dashboard.html
new file mode 100644
index 00000000..e891b565
--- /dev/null
+++ b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboard/dashboard.html
@@ -0,0 +1,74 @@
+
+<div>
+ <div class="btn-toolbar" ng-if="!options.hideToolbar">
+ <div class="btn-group" ng-if="!options.widgetButtons">
+
+ <span class="dropdown" on-toggle="toggled(open)">
+ <!--<button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown">
+ Button dropdown<span class="caret"></span>
+ </button>-->
+ <ul class="dropdown-menu" role="menu">
+ <li ng-repeat="widget in widgetDefs">
+ <a href="#" ng-click="addWidgetInternal($event, widget);" class="dropdown-toggle"><span class="label label-primary">{{widget.name}}</span></a>
+ </li>
+ </ul>
+ </span>
+ </div>
+
+ <div class="btn-group" ng-if="options.widgetButtons">
+ <button ng-repeat="widget in widgetDefs"
+ ng-click="addWidgetInternal($event, widget);" type="button" class="btn btn-primary">
+ {{widget.name}}
+ </button>
+ </div>
+
+<!-- <div style="float: left" class="form-field" att-select="reports" ng-model="report1" placeholder="Select a Report"></div>
+ <button class="btn btn-primary" ng-click="addReport(report1)">Add Raptor Report</button> -->
+ <button class="btn btn-primary" ng-click="popupAddReport()">+ Raptor Report</button>
+
+<!-- <div style = "float:left"> <input ng-model = "rcloud_url"> </div>
+ <button class="btn btn-primary" ng-click="addRCloudNotebook(rcloud_url)">Add R Cloud</button> -->
+ <button class="btn btn-primary" ng-click="popupAddRCloudNotebook()">+ RCloud Notebook</button>
+
+<!-- <button class="btn btn-warning" ng-click="resetWidgetsToDefault()">Default Widgets</button> -->
+
+ <button ng-if="options.storage && options.explicitSave" ng-click="options.saveDashboard()" class="btn btn-success" ng-disabled="!options.unsavedChangeCount">{{ !options.unsavedChangeCount ? "All Saved" : "Save Changes (" + options.unsavedChangeCount + ")" }}</button>
+
+ <button ng-click="clear();" type="button" class="btn btn-info">Clear</button>
+
+<!-- <button style="float:right" ng-click="clear();" type="button" class="btn btn-info">Save to Database</button> -->
+ </div>
+
+
+<!--
+ <div id="container">
+ <div id="navi">navi</div>
+ <div id="infoi">
+ <img src="https://appharbor.com/assets/images/stackoverflow-logo.png" height="20" width="32" />infoi
+ </div>
+</div>
+ -->
+
+ <div ui-sortable="sortableOptions" ng-model="widgets" class="dashboard-widget-area">
+ <div ng-mouseover="hoverIn()" ng-mouseleave="hoverOut()" ng-repeat="widget in widgets" ng-style="widget.containerStyle" class="widget-container" widget>
+ <div class="widget panel panel-default">
+ <div style="opacity: 0.8; background-color: #E5E5E5; border: #d3d3d3;" ng-show="hoverEdit" class="widget-header panel-heading">
+ <img style="float:left; margin-right: 10px" src="static/fusion/images/att_angular_gridster/grips.png">
+ <h3 class="panel-title">
+ <span class="widget-title" ng-dblclick="editTitle(widget)" ng-hide="widget.editingTitle"></span>
+ <form action="" class="widget-title" ng-show="widget.editingTitle" ng-submit="saveTitleEdit(widget)">
+ <input type="text" ng-model="widget.title" class="form-control">
+ </form>
+ <span class="label label-primary" ng-if="!options.hideWidgetName">{{widget.name}}</span>
+ <span ng-click="removeWidget(widget);" class="glyphicon glyphicon-remove icon-erase" ng-if="!options.hideWidgetClose"></span>
+ <span ng-click="openWidgetSettings(widget);" class="glyphicon glyphicon-cog icon-settings" ng-if="!options.hideWidgetSettings"></span>
+ <span ng-click="widget.contentStyle.display = widget.contentStyle.display === 'none' ? 'block' : 'none'" class="glyphicon" ng-class="{'glyphicon-plus': widget.contentStyle.display === 'none', 'glyphicon-minus': widget.contentStyle.display !== 'none' }"></span>
+ </h3>
+ </div>
+ <div class="panel-body widget-content" ng-style="widget.contentStyle"></div>
+ <div class="widget-ew-resizer" ng-mousedown="grabResizer($event)"></div>
+ <div style="background-color:#f2f2f2" ng-if="widget.enableVerticalResize" class="widget-s-resizer" ng-mousedown="grabSouthResizer($event)"></div>
+ </div>
+ </div>
+ </div>
+</div> \ No newline at end of file
diff --git a/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboard/dashboard.js b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboard/dashboard.js
new file mode 100644
index 00000000..4062694e
--- /dev/null
+++ b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboard/dashboard.js
@@ -0,0 +1,427 @@
+/*
+ * 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', ['ui.bootstrap', 'ui.sortable']);
+
+angular.module('ui.dashboard')
+
+ .directive('dashboard', ['$http','WidgetModel', 'WidgetDefCollection', '$uibModal', 'DashboardState', '$log', function ($http, WidgetModel, WidgetDefCollection, $uibModal, DashboardState, $log) {
+
+ return {
+ restrict: 'A',
+ templateUrl: function(element, attr) {
+ return attr.templateUrl ? attr.templateUrl : 'app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboard/dashboard.html';
+ },
+ scope: true,
+
+ controller: ['$scope', '$attrs', function (scope, attrs) {
+ // default options
+ var defaults = {
+ stringifyStorage: true,
+ hideWidgetSettings: false,
+ hideWidgetClose: false,
+ settingsModalOptions: {
+ // templateUrl: 'app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboard/widget-settings-template.html',
+ templateUrl: 'app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboard/widget-settings-raptor-report-template.html',
+ // controller: 'WidgetSettingsCtrl'
+ controller: 'WidgetSettingsRaptorReportCtrl'
+ },
+ onSettingsClose: function(result, widget) { // NOTE: dashboard scope is also passed as 3rd argument
+ jQuery.extend(true, widget, result);
+ },
+ onSettingsDismiss: function(reason) { // NOTE: dashboard scope is also passed as 2nd argument
+ $log.info('widget settings were dismissed. Reason: ', reason);
+ }
+ };
+
+ scope.hoverEdit = false;
+
+ scope.hoverIn = function(){
+ this.hoverEdit = true;
+ };
+
+ scope.hoverOut = function(){
+ this.hoverEdit = false;
+ };
+
+ // from dashboard="options"
+ scope.options = scope.$eval(attrs.dashboard);
+
+ // Deep options
+ scope.options.settingsModalOptions = scope.options.settingsModalOptions || {};
+ _.each(['settingsModalOptions'], function(key) {
+ // Ensure it exists on scope.options
+ scope.options[key] = scope.options[key] || {};
+ // Set defaults
+ _.defaults(scope.options[key], defaults[key]);
+ });
+
+ // Shallow options
+ _.defaults(scope.options, defaults);
+
+ // sortable options
+ var sortableDefaults = {
+ stop: function () {
+ scope.saveDashboard();
+ },
+ handle: '.widget-header',
+ distance: 5
+ };
+ scope.sortableOptions = angular.extend({}, sortableDefaults, scope.options.sortableOptions || {});
+
+ }],
+ link: function (scope) {
+
+ // Save default widget config for reset
+ scope.defaultWidgets = scope.options.defaultWidgets;
+
+ scope.widgetDefs = new WidgetDefCollection(scope.options.widgetDefinitions);
+ var count = 1;
+
+ // Instantiate new instance of dashboard state
+ scope.dashboardState = new DashboardState(
+ scope.options.storage,
+ scope.options.storageId,
+ scope.options.storageHash,
+ scope.widgetDefs,
+ scope.options.stringifyStorage
+ );
+
+ /**
+ * Instantiates a new widget on the 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.';
+ }
+
+ // Determine the title for the new widget
+ var title;
+ if (!widgetToInstantiate.title && !defaultWidgetDefinition.title) {
+ widgetToInstantiate.title = 'Widget ' + count++;
+ }
+
+ // Instantiation
+ var widget = new WidgetModel(defaultWidgetDefinition, widgetToInstantiate);
+
+ // Add to the widgets array
+ scope.widgets.push(widget);
+ if (!doNotSave) {
+ scope.saveDashboard();
+ }
+
+ return widget;
+ };
+
+ /**
+ * Removes a widget instance from the dashboard
+ * @param {Object} widget The widget instance object (not a definition object)
+ */
+ scope.removeWidget = function (widget) {
+ scope.widgets.splice(_.indexOf(scope.widgets, widget), 1);
+ scope.saveDashboard();
+ };
+
+ /**
+ * Opens a dialog for setting and changing widget properties
+ * @param {Object} widget The widget instance object
+ */
+ scope.openWidgetSettings = function (widget) {
+/* console.log('======= widgets =======');
+ console.log(widget);
+ console.log('widget.report_id');
+ console.log(widget.report_id);
+*/
+ if (widget.directive.includes("raptor-report")) {
+ var getFormFieldListUrl = "raptor.htm?action=report.run.container&c_master="+widget.report_id + "&refresh=Y";
+ $http.get(getFormFieldListUrl).then(
+ function(res){
+ widget.reportData = res.data;
+ });
+
+ // Set up $uibModal options
+ var options = _.defaults(
+ { scope: scope },
+ widget.settingsModalOptions,
+ scope.options.settingsModalOptions);
+
+/* console.log('======= options =======');
+ console.log(options);
+*/
+ // Ensure widget is resolved
+ options.resolve = {
+ widget: function () {
+ return widget;
+ }
+ };
+
+ // Create the modal
+ var modalInstance = $uibModal.open(options);
+ var onClose = widget.onSettingsClose || scope.options.onSettingsClose;
+ var onDismiss = widget.onSettingsDismiss || scope.options.onSettingsDismiss;
+
+ // Set resolve and reject callbacks for the result promise
+ modalInstance.result.then(
+ function (result) {
+
+ // Call the close callback
+ onClose(result, widget, scope);
+
+ //AW Persist title change from options editor
+ scope.$emit('widgetChanged', widget);
+ },
+ function (reason) {
+
+ // Call the dismiss callback
+ onDismiss(reason, scope);
+
+ }
+ );
+
+ }
+
+ };
+
+ /**
+ * Remove all widget instances from dashboard
+ */
+ scope.clear = function (doNotSave) {
+ scope.widgets = [];
+ if (doNotSave === true) {
+ return;
+ }
+ scope.saveDashboard();
+ };
+
+ /**
+ * Used for preventing default on click event
+ * @param {Object} event A click event
+ * @param {Object} widgetDef A widget definition object
+ */
+ scope.addWidgetInternal = function (event, widgetDef) {
+// event.preventDefault();
+ scope.addWidget(widgetDef);
+ };
+
+ /**
+ * Add report to dashboard
+ */
+ scope.popupAddReport = function () {
+ var modalInstance = $uibModal.open({
+ animation: scope.animationsEnabled,
+ templateUrl: 'app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboard/add-raptor-report-template.html',
+ size:'sm',
+ controller: ['$scope', '$uibModalInstance', '$http', function ($scope, $uibModalInstance, $http) {
+ $scope.radioValue="chart"
+ $http.get('raptor.htm?action=report.search.execute').then(
+ function(result){
+ var data = result.data;
+ var report_id_name = [];
+ for (var i in data.rows[0]) {
+ report_id_name.push({index:i, value: data.rows[0][i][1].searchresultField.displayValue, title: data.rows[0][i][2].searchresultField.displayValue})
+ }
+ $scope.raptorReportList = report_id_name;
+ });
+
+ $scope.ok = function() {
+ scope.addReport($scope.selectedRaptorReport,$scope.radioValue);
+ $uibModalInstance.close();
+ };
+ $scope.cancel = function() {
+ $uibModalInstance.dismiss();
+ };
+ }]
+ });
+ modalInstance.result.then(function () {
+ $scope.$emit('raptorReportWidgetAdded');
+ }, function () {
+ $log.info('Modal dismissed at: ' + new Date());
+ });
+ };
+
+
+ scope.popupAddRCloudNotebook = function () {
+ var modalInstance = $uibModal.open({
+ animation: scope.animationsEnabled,
+ templateUrl: 'app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboard/add-rcloud-notebook-template.html',
+ size:'sm',
+ controller: ['$scope', '$uibModalInstance', '$http', function ($scope, $uibModalInstance, $http) {
+ $scope.rcloud_url = ""
+
+ $scope.ok = function() {
+ scope.addRCloudNotebook($scope.rcloud_url);
+ $uibModalInstance.close();
+ };
+ $scope.cancel = function() {
+ $uibModalInstance.dismiss();
+ };
+ }]
+ });
+ modalInstance.result.then(function () {
+ $scope.$emit('raptorReportWidgetAdded');
+ }, function () {
+ $log.info('Modal dismissed at: ' + new Date());
+ });
+ };
+
+
+ scope.addReport = function (report1,radioValue) {
+ scope.report1 =report1
+ var raptor_report_type = "raptor-report-chart"
+ if (radioValue ==='data') {
+ raptor_report_type = 'raptor-report-data'
+ }
+ console.log("report1")
+ console.log(report1);
+// event.preventDefault();
+ var newreport = {"title":report1.title,"name":raptor_report_type ,"style":{},"size":{"height":"350px","width":"40%"},"attrs":{"value":"randomValue"},"report_id":report1.value};
+ scope.addWidget(newreport, true);
+ console.log("widgets");
+ console.log(scope.widgets);
+ ++scope.options.unsavedChangeCount;
+ return false;
+ };
+
+ /**
+ * Add rcloud notebook to dashboard
+ */
+ scope.addRCloudNotebook = function (rcloud_url) {
+ ++scope.options.unsavedChangeCount;
+ /* open a new prompt window */
+ //event.preventDefault();
+ var newreport = {"title":"R-Cloud","name":"r-cloud","style":{},"size":{"height":"450px","width":"40%"},"attrs":{"value":"randomValue"},"rcloud_url":rcloud_url};
+// console.log("newport");
+ console.log(newreport)
+ scope.addWidget(newreport, true);
+ /* scope.addWidget("raptor-report");*/
+ return false;
+ };
+
+ /**
+ * Uses dashboardState service to save state
+ */
+ scope.saveDashboard = function (force) {
+ if (!scope.options.explicitSave) {
+ scope.dashboardState.save(scope.widgets);
+ } else {
+ if (!angular.isNumber(scope.options.unsavedChangeCount)) {
+ scope.options.unsavedChangeCount = 0;
+ }
+ if (force) {
+ scope.options.unsavedChangeCount = 0;
+ scope.dashboardState.save(scope.widgets);
+
+ } else {
+ ++scope.options.unsavedChangeCount;
+ }
+ }
+ };
+
+ /**
+ * Wraps saveDashboard for external use.
+ */
+ scope.externalSaveDashboard = function(force) {
+ if (angular.isDefined(force)) {
+ scope.saveDashboard(force);
+ } else {
+ scope.saveDashboard(true);
+ }
+ };
+
+ /**
+ * Clears current dash and instantiates widget definitions
+ * @param {Array} widgets Array of definition objects
+ */
+ scope.loadWidgets = function (widgets) {
+ // AW dashboards are continuously saved today (no "save" button).
+ console.log("widgets")
+ scope.defaultWidgets = widgets;
+ widgets =
+ [
+// {"title":"DEMO Bar Chart","name":"raptor-report-chart","style":{},"size":{"height":"450px","width":"40%"},"attrs":{"value":"randomValue"},"report_id":"2"},
+// {"title":"Pie Chart","name":"raptor-report-data","style":{},"size":{"height":"450px","width":"40%"},"attrs":{"value":"randomValue"},"report_id":"5"},
+// {"title":"Pie Chart","name":"raptor-report-chart","style":{},"size":{"height":"450px","width":"40%"},"attrs":{"value":"randomValue"},"report_id":"5"}
+ ];
+ console.log('widgets: ');
+ console.log(JSON.stringify(widgets));
+
+ scope.savedWidgetDefs = widgets;
+ scope.clear(true);
+ _.each(widgets, function (widgetDef) {
+ scope.addWidget(widgetDef, true);
+ });
+ };
+
+ /**
+ * Resets widget instances to default config
+ * @return {[type]} [description]
+ */
+ scope.resetWidgetsToDefault = function () {
+ scope.loadWidgets(scope.defaultWidgets);
+ scope.saveDashboard();
+ };
+
+ // Set default widgets array
+ var savedWidgetDefs = scope.dashboardState.load();
+
+ // Success handler
+ function handleStateLoad(saved) {
+ scope.options.unsavedChangeCount = 0;
+ if (saved && saved.length) {
+ scope.loadWidgets(saved);
+ } else if (scope.defaultWidgets) {
+ scope.loadWidgets(scope.defaultWidgets);
+ } else {
+ scope.clear(true);
+ }
+ }
+
+ if (angular.isArray(savedWidgetDefs)) {
+ handleStateLoad(savedWidgetDefs);
+ } else if (savedWidgetDefs && angular.isObject(savedWidgetDefs) && angular.isFunction(savedWidgetDefs.then)) {
+ savedWidgetDefs.then(handleStateLoad, handleStateLoad);
+ } else {
+ handleStateLoad();
+ }
+
+ // expose functionality externally
+ // functions are appended to the provided dashboard options
+ scope.options.addWidget = scope.addWidget;
+ scope.options.loadWidgets = scope.loadWidgets;
+ scope.options.saveDashboard = scope.externalSaveDashboard;
+ scope.options.removeWidget = scope.removeWidget;
+ scope.options.openWidgetSettings = scope.openWidgetSettings;
+ scope.options.clear = scope.clear;
+ scope.options.resetWidgetsToDefault = scope.resetWidgetsToDefault
+
+ // save state
+ scope.$on('widgetChanged', function (event) {
+ event.stopPropagation();
+ scope.saveDashboard();
+ });
+ }
+ };
+ }]);
diff --git a/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboard/dashboard.less b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboard/dashboard.less
new file mode 100644
index 00000000..6b5b717f
--- /dev/null
+++ b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboard/dashboard.less
@@ -0,0 +1,88 @@
+.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/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboard/dashboard.spec.js b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboard/dashboard.spec.js
new file mode 100644
index 00000000..453de431
--- /dev/null
+++ b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboard/dashboard.spec.js
@@ -0,0 +1,878 @@
+/*
+ * 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';
+
+describe('Directive: dashboard', function () {
+
+ var scope, element, childScope, DashboardState, mockModal, modalOptions, $compile, $q, mockLog;
+
+ // mock UI Sortable
+ beforeEach(function () {
+ angular.module('ui.sortable', []);
+ });
+
+ // load the directive's module
+ beforeEach(module('ui.dashboard', function($provide) {
+ mockModal = {
+ open: function(options) {
+ modalOptions = options;
+ }
+ };
+ mockLog = {
+ info: function() {
+
+ }
+ };
+ $provide.value('$uibModal', mockModal);
+ $provide.value('$log', mockLog);
+ }));
+
+ beforeEach(inject(function (_$compile_, $rootScope, _DashboardState_, _$q_) {
+ // services
+ scope = $rootScope.$new();
+ $compile = _$compile_;
+ DashboardState = _DashboardState_;
+ $q = _$q_;
+
+ // options
+ var widgetDefinitions = [
+ {
+ name: 'wt-one',
+ template: '<div class="wt-one-value">{{2 + 2}}</div>'
+ },
+ {
+ name: 'wt-two',
+ template: '<span class="wt-two-value">{{value}}</span>'
+ }
+ ];
+ var defaultWidgets = _.clone(widgetDefinitions);
+ scope.dashboardOptions = {
+ widgetButtons: true,
+ widgetDefinitions: widgetDefinitions,
+ defaultWidgets: defaultWidgets,
+ sortableOptions: {
+ testProperty: 'foobar'
+ }
+ };
+ scope.value = 10;
+
+ // element setup
+ element = $compile('<div dashboard="dashboardOptions"></div>')(scope);
+ scope.$digest();
+ childScope = element.scope();
+ }));
+
+ it('should have toolbar', function () {
+ var toolbar = element.find('.btn-toolbar');
+ expect(toolbar.length).toEqual(1);
+ });
+
+ it('should have UI.Sortable directive', function () {
+ var widgetArea = element.find('.dashboard-widget-area');
+ expect(widgetArea.attr('ui-sortable')).toBeDefined();
+ });
+
+ it('should render widgets', function () {
+ var widgets = element.find('.widget');
+ expect(widgets.length).toEqual(2);
+ });
+
+ it('should evaluate widget expressions', function () {
+ var divWidget = element.find('.wt-one-value');
+ expect(divWidget.html()).toEqual('4');
+ });
+
+ it('should evaluate scope expressions', function () {
+ var spanWidget = element.find('.wt-two-value');
+ expect(spanWidget.html()).toEqual('10');
+ });
+
+ it('should fill options with defaults', function() {
+ expect(scope.dashboardOptions.stringifyStorage).toEqual(true);
+ });
+
+ it('should not overwrite specified options with defaults', inject(function($compile) {
+ scope.dashboardOptions.stringifyStorage = false;
+ element = $compile('<div dashboard="dashboardOptions"></div>')(scope);
+ $compile(element)(scope);
+ scope.$digest();
+ expect(scope.dashboardOptions.stringifyStorage).toEqual(false);
+ }));
+
+ it('should be able to use a different dashboard template', inject(function($compile, $templateCache) {
+ $templateCache.put(
+ 'myCustomTemplate.html',
+ '<div>' +
+ '<div ui-sortable="sortableOptions" ng-model="widgets">' +
+ '<div ng-repeat="widget in widgets" ng-style="widget.style" class="widget-container custom-widget" widget>' +
+ '<h3 class="widget-header">' +
+ '{{widget.title}}' +
+ '<span ng-click="removeWidget(widget);" class="glyphicon glyphicon-remove icon-erase" ng-if="!options.hideWidgetClose"></span>' +
+ '<span ng-click="openWidgetSettings(widget);" class="glyphicon glyphicon-cog icon-settings" ng-if="!options.hideWidgetSettings"></span>' +
+ '</h3>' +
+ '<div class="widget-content"></div>' +
+ '<div class="widget-ew-resizer" ng-mousedown="grabResizer($event)"></div>' +
+ '</div>' +
+ '</div>' +
+ '</div>'
+ );
+ var customElement = $compile('<div dashboard="dashboardOptions" template-url="myCustomTemplate.html"></div>')(scope);
+ scope.$digest();
+ expect(customElement.find('.custom-widget').length).toEqual(2);
+ }));
+
+ it('should set scope.widgets to an empty array if no defaultWidgets are specified', inject(function($compile) {
+ delete scope.dashboardOptions.defaultWidgets;
+ var element2 = $compile('<div dashboard="dashboardOptions"></div>')(scope);
+ scope.$digest();
+ var childScope2 = element2.scope();
+ expect(childScope2.widgets instanceof Array).toEqual(true);
+ }));
+
+ it('should set options.unsavedChangeCount to 0 upon load', function() {
+ expect(scope.dashboardOptions.unsavedChangeCount).toEqual(0);
+ });
+
+ it('should not call saveDashboard on load', inject(function($compile) {
+ spyOn(DashboardState.prototype, 'save');
+ var s = scope.$new();
+ element = $compile('<div dashboard="dashboardOptions"></div>')(s);
+ scope.$digest();
+ expect(DashboardState.prototype.save).not.toHaveBeenCalled();
+ }));
+
+ describe('the sortableOptions', function() {
+
+ it('should exist', function() {
+ expect(typeof childScope.sortableOptions).toEqual('object');
+ });
+
+ it('should be possible to be extendable from the dashboardOptions', function() {
+ expect(childScope.sortableOptions.testProperty).toEqual('foobar');
+ })
+
+ it('should have a stop function that calls $scope.saveDashboard', function() {
+ expect(typeof childScope.sortableOptions.stop).toEqual('function');
+ spyOn(childScope, 'saveDashboard');
+ childScope.sortableOptions.stop();
+ expect(childScope.saveDashboard).toHaveBeenCalled();
+ });
+ });
+
+ describe('the addWidget function', function() {
+
+ var widgetCreated, widgetPassed, widgetDefault;
+
+ beforeEach(function() {
+ childScope.widgets.push = function(w) {
+ widgetCreated = w;
+ }
+ });
+
+ it('should be a function', function() {
+ expect(typeof childScope.addWidget).toEqual('function');
+ });
+
+ it('should throw if no default widgetDefinition was found', function() {
+ spyOn(childScope.widgetDefs, 'getByName').and.returnValue(false);
+ function fn () {
+ childScope.addWidget({ name: 'notReal' });
+ }
+ expect(fn).toThrow();
+ });
+
+ it('should look to the passed widgetToInstantiate object for the title before anything else', function() {
+ spyOn(childScope.widgetDefs, 'getByName').and.returnValue({ title: 'defaultTitle', name: 'A' });
+ childScope.addWidget({ title: 'highestPrecedence', name: 'A' });
+ expect(widgetCreated.title).toEqual('highestPrecedence');
+ });
+
+ it('should use the defaultWidget\'s title second', function() {
+ spyOn(childScope.widgetDefs, 'getByName').and.returnValue({ title: 'defaultTitle', name: 'A' });
+ childScope.addWidget({ name: 'A' });
+ expect(widgetCreated.title).toEqual('defaultTitle');
+ });
+
+ it('should call the saveDashboard method (internal)', function() {
+ spyOn(childScope.widgetDefs, 'getByName').and.returnValue({ title: 'defaultTitle', name: 'A' });
+ spyOn(childScope, 'saveDashboard');
+ childScope.addWidget({ name: 'A' });
+ 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 = {
+ "name": "nvLineChartAlpha",
+ "directive": "nvd3-line-chart",
+ "dataAttrName": "data",
+ "attrs": {
+ "isArea": true,
+ "height": 400,
+ "showXAxis": true,
+ "showYAxis": true,
+ "xAxisTickFormat": "xAxisTickFormat()",
+ "interactive": true,
+ "useInteractiveGuideline": true,
+ "tooltips": true,
+ "showLegend": true,
+ "noData": "No data for YOU!",
+ "color": "colorFunction()",
+ "forcey": "[0,2]"
+ },
+ "dataModelOptions": {
+ "params": {
+ "from": "-2h",
+ "until": "now"
+ }
+ },
+ "style": {
+ "width": "400px"
+ },
+ });
+ childScope.addWidget(widgetPassed = {
+ "title": "Andy",
+ "name": "nvLineChartAlpha",
+ "style": {
+ "width": "400px"
+ },
+ "dataModelOptions": {
+ "params": {
+ "from": "-1h",
+ "target": [
+ "randomWalk(\"random Andy 1\")",
+ "randomWalk(\"random walk 2\")",
+ "randomWalk(\"random walk 3\")"
+ ]
+ }
+ },
+ "attrs": {
+ "height": 400,
+ "showXAxis": true,
+ "showYAxis": true,
+ "xAxisTickFormat": "xAxisTickFormat()",
+ "interactive": false,
+ "useInteractiveGuideline": true,
+ "tooltips": true,
+ "showLegend": true,
+ "noData": "No data for YOU!",
+ "color": "colorFunction()",
+ "forcey": "[0,2]",
+ "data": "widgetData"
+ }
+ });
+ });
+
+ it('should keep overrides from widgetPassed', function() {
+ expect(widgetCreated.attrs.interactive).toEqual(widgetPassed.attrs.interactive);
+ });
+
+ it('should fill in default attrs', function() {
+ expect(widgetCreated.attrs.isArea).toEqual(widgetDefault.attrs.isArea);
+ });
+
+ it('should override deep options in dataModelOptions', function() {
+ expect(widgetCreated.dataModelOptions.params.from).toEqual(widgetPassed.dataModelOptions.params.from);
+ });
+
+ it('should fill in deep default attrs', function() {
+ expect(widgetCreated.dataModelOptions.params.until).toEqual(widgetDefault.dataModelOptions.params.until);
+ });
+ });
+
+ describe('the doNotSave parameter', function() {
+
+ it('should prevent save from being called if set to true', function() {
+ spyOn(childScope.widgetDefs, 'getByName').and.returnValue({ title: 'defaultTitle', name: 'A' });
+ spyOn(childScope, 'saveDashboard');
+ childScope.addWidget({ name: 'A' }, true);
+ expect(childScope.saveDashboard).not.toHaveBeenCalled();
+ });
+
+ });
+
+ });
+
+ describe('the removeWidget function', function() {
+
+ it('should be a function', function() {
+ expect(typeof childScope.removeWidget).toEqual('function');
+ });
+
+ it('should remove the provided widget from childScope.widgets array', function() {
+ var startingLength = childScope.widgets.length;
+ var expectedLength = startingLength - 1;
+
+ var widgetToRemove = childScope.widgets[0];
+ childScope.removeWidget(widgetToRemove);
+
+ expect(childScope.widgets.length).toEqual(expectedLength);
+ expect(childScope.widgets.indexOf(widgetToRemove)).toEqual(-1);
+ });
+
+ it('should call saveDashboard', function() {
+ spyOn(childScope, 'saveDashboard');
+ var widgetToRemove = childScope.widgets[0];
+ childScope.removeWidget(widgetToRemove);
+ expect(childScope.saveDashboard).toHaveBeenCalled();
+ });
+
+ });
+
+ describe('the saveDashboard function', function() {
+
+ it('should be attached to the options object after initialization', function() {
+ expect(typeof scope.dashboardOptions.saveDashboard).toEqual('function');
+ expect(scope.dashboardOptions.saveDashboard === childScope.externalSaveDashboard).toEqual(true);
+ });
+
+ it('should call scope.dashboardState.save when called internally if explicitSave is falsey', function() {
+ spyOn(childScope.dashboardState, 'save').and.returnValue(true);
+ childScope.saveDashboard();
+ expect(childScope.dashboardState.save).toHaveBeenCalled();
+ });
+
+ it('should not call scope.dashboardState.save when called internally if explicitSave is truthy', function() {
+ scope.dashboardOptions.explicitSave = true;
+ spyOn(childScope.dashboardState, 'save').and.returnValue(true);
+ childScope.saveDashboard();
+ expect(childScope.dashboardState.save).not.toHaveBeenCalled();
+ });
+
+ it('should call scope.dashboardState.save when called externally, no matter what explicitSave value is', function() {
+ spyOn(childScope.dashboardState, 'save').and.returnValue(true);
+
+ scope.dashboardOptions.explicitSave = false;
+ scope.dashboardOptions.saveDashboard();
+ expect(childScope.dashboardState.save.calls.count()).toEqual(1);
+
+ scope.dashboardOptions.explicitSave = true;
+ scope.dashboardOptions.saveDashboard();
+ expect(childScope.dashboardState.save.calls.count()).toEqual(2);
+ });
+
+ it('should keep a count of unsaved changes as unsavedChangeCount', function() {
+ scope.dashboardOptions.explicitSave = true;
+ spyOn(childScope.dashboardState, 'save').and.returnValue(true);
+ childScope.saveDashboard();
+ expect(scope.dashboardOptions.unsavedChangeCount).toEqual(1);
+ childScope.saveDashboard();
+ childScope.saveDashboard();
+ expect(scope.dashboardOptions.unsavedChangeCount).toEqual(3);
+ });
+
+ it('should reset the cound of unsaved changes if a successful force save occurs', function() {
+ scope.dashboardOptions.explicitSave = true;
+ spyOn(childScope.dashboardState, 'save').and.returnValue(true);
+
+ childScope.saveDashboard();
+ childScope.saveDashboard();
+ childScope.saveDashboard();
+
+ childScope.saveDashboard(true);
+
+ expect(scope.dashboardOptions.unsavedChangeCount).toEqual(0);
+ });
+
+ });
+
+ describe('the loadWidgets function', function() {
+
+ it('should be a function', function() {
+ expect(typeof childScope.loadWidgets).toEqual('function');
+ });
+
+ it('should set savedWidgetDefs on scope as passed array', function() {
+ var widgets = [];
+ childScope.loadWidgets(widgets);
+ expect(childScope.savedWidgetDefs === widgets).toEqual(true);
+ });
+
+ it('should call clear on the scope with true as the only argument', function() {
+ spyOn(childScope, 'clear');
+ childScope.loadWidgets([]);
+ expect(childScope.clear).toHaveBeenCalled();
+ expect(childScope.clear.calls.argsFor(0)).toEqual([true]);
+ });
+
+ it('should call addWidget for each widget in the array', function() {
+ spyOn(childScope, 'addWidget').and.returnValue(null);
+ var widgets = [{},{},{}];
+ childScope.loadWidgets(widgets);
+ expect(childScope.addWidget.calls.count()).toEqual(3);
+ });
+
+ it('should call addWidget for each widget with true as the second parameter (doNotSave)', function() {
+ spyOn(childScope, 'addWidget').and.returnValue(null);
+ var widgets = [{},{},{}];
+ childScope.loadWidgets(widgets);
+ expect(childScope.addWidget.calls.argsFor(0)).toEqual( [ widgets[0], true] );
+ expect(childScope.addWidget.calls.argsFor(1)).toEqual( [ widgets[1], true] );
+ expect(childScope.addWidget.calls.argsFor(2)).toEqual( [ widgets[2], true] );
+ });
+
+ });
+
+ describe('the clear function', function() {
+
+ it('should set the scope to an empty array', function() {
+ childScope.clear();
+ expect(childScope.widgets).toEqual([]);
+ });
+
+ it('should not call saveDashboard if first arg is true', function() {
+ spyOn(childScope, 'saveDashboard');
+ childScope.clear(true);
+ expect(childScope.saveDashboard).not.toHaveBeenCalled();
+ });
+
+ it('should call saveDashboard if first arg is not true', function() {
+ spyOn(childScope, 'saveDashboard');
+ childScope.clear();
+ expect(childScope.saveDashboard).toHaveBeenCalled();
+ });
+
+ });
+
+ describe('the openWidgetSettings function', function() {
+
+ it('should be a function', function() {
+ expect(typeof childScope.openWidgetSettings).toEqual('function');
+ });
+
+ it('should call $uibModal.open with default options', function() {
+ var widget = {};
+ spyOn(mockModal, 'open').and.returnValue({
+ result: { then: function(fn) {} }
+ });
+ childScope.openWidgetSettings(widget);
+ expect(mockModal.open).toHaveBeenCalled();
+ });
+
+ it('should have widget in the resolve object', function() {
+ var widget = {};
+ var dfr = $q.defer();
+ spyOn(mockModal, 'open').and.callFake(function(options) {
+ modalOptions = options;
+ return {
+ result: dfr.promise
+ };
+ });
+ childScope.openWidgetSettings(widget);
+ expect(modalOptions.resolve.widget() === widget).toEqual(true);
+ });
+
+ it('should set the templateUrl in modal options to the default ("app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboard/widget-settings-template.html")', function() {
+ var widget = {};
+ var dfr = $q.defer();
+ spyOn(mockModal, 'open').and.callFake(function(options) {
+ modalOptions = options;
+ return {
+ result: dfr.promise
+ };
+ });
+ childScope.openWidgetSettings(widget);
+ expect(modalOptions.templateUrl).toEqual('app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboard/widget-settings-template.html');
+ });
+
+ it('should set the templateUrl in modal options to scope.options.settingsModalOptions.templateUrl', function() {
+ var other;
+ scope.dashboardOptions.settingsModalOptions = {
+ templateUrl: other = 'some/other/url.html'
+ };
+ var widget = {};
+ var dfr = $q.defer();
+ spyOn(mockModal, 'open').and.callFake(function(options) {
+ modalOptions = options;
+ return {
+ result: dfr.promise
+ };
+ });
+ childScope.openWidgetSettings(widget);
+ expect(modalOptions.templateUrl).toEqual(other);
+ });
+
+ it('should set the templateUrl in modal options to widget.settingsModalOptions.templateUrl, if present', function() {
+ var expected;
+ var widget = {
+ settingsModalOptions: {
+ templateUrl: expected = 'specific/template.html'
+ }
+ };
+ var dfr = $q.defer();
+ spyOn(mockModal, 'open').and.callFake(function(options) {
+ modalOptions = options;
+ return {
+ result: dfr.promise
+ };
+ });
+ childScope.openWidgetSettings(widget);
+ expect(modalOptions.templateUrl).toEqual(expected);
+ });
+
+ it('should set the controller in modal options to the default ("WidgetSettingsCtrl")', function() {
+ var widget = {};
+ var dfr = $q.defer();
+ spyOn(mockModal, 'open').and.callFake(function(options) {
+ modalOptions = options;
+ return {
+ result: dfr.promise
+ };
+ });
+ childScope.openWidgetSettings(widget);
+ expect(modalOptions.controller).toEqual('WidgetSettingsCtrl');
+ });
+
+ it('should set the controller in modal options to the default ("WidgetSettingsCtrl"), even when settingsModalOptions is supplied in options', inject(function($rootScope) {
+
+ scope = $rootScope.$new();
+
+ // options
+ var widgetDefinitions = [
+ {
+ name: 'wt-one',
+ template: '<div class="wt-one-value">{{2 + 2}}</div>'
+ },
+ {
+ name: 'wt-two',
+ template: '<span class="wt-two-value">{{value}}</span>'
+ }
+ ];
+ var defaultWidgets = _.clone(widgetDefinitions);
+ scope.dashboardOptions = {
+ widgetButtons: true,
+ widgetDefinitions: widgetDefinitions,
+ defaultWidgets: defaultWidgets,
+ sortableOptions: {
+ testProperty: 'foobar'
+ },
+ settingsModalOptions: {
+ backdrop: false
+ }
+ };
+ scope.value = 10;
+
+ var dfr = $q.defer();
+ spyOn(mockModal, 'open').and.callFake(function(options) {
+ modalOptions = options;
+ return {
+ result: dfr.promise
+ };
+ });
+
+ // element setup
+ element = $compile('<div dashboard="dashboardOptions"></div>')(scope);
+ scope.$digest();
+ childScope = element.scope();
+
+ childScope.openWidgetSettings({});
+ expect(modalOptions.controller).toEqual('WidgetSettingsCtrl');
+
+ }));
+
+ it('should set the controller in modal options to the default ("WidgetSettingsCtrl"), even when settingsModalOptions is supplied in widget', inject(function($rootScope) {
+
+ scope = $rootScope.$new();
+
+ // options
+ var widgetDefinitions = [
+ {
+ name: 'wt-one',
+ template: '<div class="wt-one-value">{{2 + 2}}</div>'
+ },
+ {
+ name: 'wt-two',
+ template: '<span class="wt-two-value">{{value}}</span>'
+ }
+ ];
+ var defaultWidgets = _.clone(widgetDefinitions);
+ scope.dashboardOptions = {
+ widgetButtons: true,
+ widgetDefinitions: widgetDefinitions,
+ defaultWidgets: defaultWidgets,
+ sortableOptions: {
+ testProperty: 'foobar'
+ },
+ settingsModalOptions: {
+ backdrop: false
+ }
+ };
+ scope.value = 10;
+
+ var dfr = $q.defer();
+ spyOn(mockModal, 'open').and.callFake(function(options) {
+ modalOptions = options;
+ return {
+ result: dfr.promise
+ };
+ });
+
+ // element setup
+ element = $compile('<div dashboard="dashboardOptions"></div>')(scope);
+ scope.$digest();
+ childScope = element.scope();
+
+ childScope.openWidgetSettings({
+ settingsModalOptions: {
+ templateUrl: 'custom/widget/template.html'
+ }
+ });
+ expect(modalOptions.controller).toEqual('WidgetSettingsCtrl');
+ expect(modalOptions.backdrop).toEqual(false);
+ expect(modalOptions.templateUrl).toEqual('custom/widget/template.html');
+
+ }));
+
+ it('should set the controller to scope.options.settingsModalOptions.controller if provided', function() {
+ scope.dashboardOptions.settingsModalOptions = {};
+ var expected = scope.dashboardOptions.settingsModalOptions.controller = 'MyCustomCtrl';
+ var widget = {};
+ var dfr = $q.defer();
+ spyOn(mockModal, 'open').and.callFake(function(options) {
+ modalOptions = options;
+ return {
+ result: dfr.promise
+ };
+ });
+ childScope.openWidgetSettings(widget);
+ expect(modalOptions.controller).toEqual(expected);
+ });
+
+ it('should set the controller to widget.settingsModalOptions.controller if provided', function() {
+ var expected;
+ var widget = {
+ settingsModalOptions: {
+ controller: expected = 'MyWidgetCtrl'
+ }
+ };
+ var dfr = $q.defer();
+ spyOn(mockModal, 'open').and.callFake(function(options) {
+ modalOptions = options;
+ return {
+ result: dfr.promise
+ };
+ });
+ childScope.openWidgetSettings(widget);
+ expect(modalOptions.controller).toEqual(expected);
+ });
+
+ it('should pass in other modal options set in scope.options.settingsModalOptions', function() {
+ scope.dashboardOptions.settingsModalOptions = {
+ keyboard: false,
+ windowClass: 'my-extra-class'
+ };
+ var widget = {};
+ var dfr = $q.defer();
+ spyOn(mockModal, 'open').and.callFake(function(options) {
+ modalOptions = options;
+ return {
+ result: dfr.promise
+ };
+ });
+ childScope.openWidgetSettings(widget);
+ expect(modalOptions.keyboard).toEqual(false);
+ expect(modalOptions.windowClass).toEqual('my-extra-class');
+ });
+
+ it('should pass in other modal options set in widget.settingsModalOptions', function() {
+ scope.dashboardOptions.settingsModalOptions = {
+ keyboard: false,
+ windowClass: 'my-extra-class'
+ };
+ var widget = {
+ settingsModalOptions: {
+ keyboard: true,
+ size: 'sm'
+ }
+ };
+ var dfr = $q.defer();
+ spyOn(mockModal, 'open').and.callFake(function(options) {
+ modalOptions = options;
+ return {
+ result: dfr.promise
+ };
+ });
+ childScope.openWidgetSettings(widget);
+ expect(modalOptions.keyboard).toEqual(true);
+ expect(modalOptions.size).toEqual('sm');
+ expect(modalOptions.windowClass).toEqual('my-extra-class');
+ });
+
+ it('should emit a "widgetChanged" event on the childScope when the modal promise is called', function(done) {
+ var widget = {};
+ var result = {};
+ var dfr = $q.defer();
+ spyOn(mockModal, 'open').and.callFake(function(options) {
+ modalOptions = options;
+ return {
+ result: dfr.promise
+ };
+ });
+ spyOn(childScope.options, 'onSettingsClose');
+ childScope.openWidgetSettings(widget);
+ childScope.$on('widgetChanged', done);
+ dfr.resolve(result, widget);
+ childScope.$digest();
+ });
+
+ it('should call scope.options.onSettingsClose when the modal promise is resolved by default', function() {
+ var widget = {};
+ var result = {};
+ var dfr = $q.defer();
+ spyOn(mockModal, 'open').and.callFake(function(options) {
+ modalOptions = options;
+ return {
+ result: dfr.promise
+ };
+ });
+ spyOn(childScope.options, 'onSettingsClose');
+ childScope.openWidgetSettings(widget);
+ dfr.resolve(result);
+ childScope.$digest();
+ expect(scope.dashboardOptions.onSettingsClose).toHaveBeenCalledWith(result, widget, childScope);
+ });
+
+ it('should call scope.options.onSettingsDismiss when the modal promise is rejected by default', function() {
+ var widget = {};
+ var result = {};
+ var dfr = $q.defer();
+ spyOn(mockModal, 'open').and.callFake(function(options) {
+ modalOptions = options;
+ return {
+ result: dfr.promise
+ };
+ });
+ spyOn(childScope.options, 'onSettingsDismiss');
+ childScope.openWidgetSettings(widget);
+ dfr.reject('Testing failure');
+ childScope.$digest();
+ expect(scope.dashboardOptions.onSettingsDismiss).toHaveBeenCalledWith('Testing failure', childScope);
+ });
+
+ it('should call widget.onSettingsClose if provided when the modal promise is resolved', function() {
+ var widget = {
+ onSettingsClose: function(result, widget, scope) {
+
+ }
+ };
+ var result = {};
+ var dfr = $q.defer();
+ spyOn(widget, 'onSettingsClose');
+ spyOn(mockModal, 'open').and.callFake(function(options) {
+ modalOptions = options;
+ return {
+ result: dfr.promise
+ };
+ });
+ spyOn(childScope.options, 'onSettingsClose');
+ childScope.openWidgetSettings(widget);
+ dfr.resolve(result);
+ childScope.$digest();
+ expect(scope.dashboardOptions.onSettingsClose).not.toHaveBeenCalled();
+ expect(widget.onSettingsClose).toHaveBeenCalledWith(result, widget, childScope);
+ });
+
+ it('should call widget.onSettingsDismiss if provided when the modal promise is rejected', function() {
+ var widget = {
+ onSettingsDismiss: function(result, widget, scope) {
+
+ }
+ };
+ var result = {};
+ var dfr = $q.defer();
+ spyOn(widget, 'onSettingsDismiss');
+ spyOn(mockModal, 'open').and.callFake(function(options) {
+ modalOptions = options;
+ return {
+ result: dfr.promise
+ };
+ });
+ spyOn(childScope.options, 'onSettingsDismiss');
+ childScope.openWidgetSettings(widget);
+ dfr.reject('Testing failure');
+ childScope.$digest();
+ expect(scope.dashboardOptions.onSettingsDismiss).not.toHaveBeenCalled();
+ expect(widget.onSettingsDismiss).toHaveBeenCalledWith('Testing failure', childScope);
+ });
+
+ });
+
+ describe('the default onSettingsClose callback', function() {
+
+ var onSettingsClose;
+
+ beforeEach(function() {
+ onSettingsClose = childScope.options.onSettingsClose;
+ });
+
+ it('should exist', function() {
+ expect(typeof onSettingsClose).toEqual('function');
+ });
+
+ it('should deep extend widget with result', function() {
+ var result = {
+ title: 'andy',
+ style: {
+ 'float': 'left'
+ }
+ };
+ var widget = {
+ title: 'scott',
+ style: {
+ width: '100px'
+ }
+ };
+ onSettingsClose(result, widget, {});
+ expect(widget).toEqual({
+ title: 'andy',
+ style: {
+ width: '100px',
+ 'float': 'left'
+ }
+ });
+ });
+
+ });
+
+ describe('the default onSettingsDismiss callback', function() {
+
+ var onSettingsDismiss;
+
+ beforeEach(function() {
+ onSettingsDismiss = childScope.options.onSettingsDismiss;
+ });
+
+ it('should exist', function() {
+ expect(typeof onSettingsDismiss).toEqual('function');
+ });
+
+ it('should call $log.info with the reason', function() {
+ spyOn(mockLog, 'info');
+ onSettingsDismiss('dismiss reason');
+ expect(mockLog.info).toHaveBeenCalled();
+ });
+
+ });
+
+});
diff --git a/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboard/widget-settings-raptor-report-template.html b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboard/widget-settings-raptor-report-template.html
new file mode 100644
index 00000000..7a533c3f
--- /dev/null
+++ b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboard/widget-settings-raptor-report-template.html
@@ -0,0 +1,58 @@
+<div class="modal-header">
+ <button type="button" class="close" data-dismiss="modal" aria-hidden="true" ng-click="cancel()">&times;</button>
+ <h3>Widget Options <small>{{widget.title}}</small></h3>
+</div>
+
+<div class="modal-body">
+ <form name="form" novalidate class="form-horizontal">
+<!-- <div class="form-group">
+ <label for="widgetTitle" class="col-sm-2 control-label">Title</label>
+ <div style="margin-left:120px" class="col-sm-10">
+ <input type="text" class="form-control" name="widgetTitle" ng-model="result.title">
+ </div>
+ </div> -->
+
+<!-- <div>{{showFormFieldIds}}</div>
+<div>{{formFieldSelectedValues}}</div>
+<div>{{JSON.strigify(widget.reportData.formFieldList)}}</div>
+ -->
+<form ng-show="true" class="row section-row" style="margin: 10px">
+ <form-builder ng-form-fields="reportData.formFieldList" ng-show-field-id="showFormFieldIds" ng-num-form-cols="reportData.numFormCols" ng-model="formFieldSelectedValues" ng-trigger-method="triggerOtherFormFields"></form-builder>
+</form>
+
+<!-- <div ng-repeat="formfield in widget.reportData.formFieldList">
+ <div class="form-group">
+ <label for="widgetTitle" class="col-sm-2 control-label">{{formfield.fieldDisplayName}}:</label>
+ <div style="margin-left:120px" class="col-sm-10">
+ <input type="text" class="form-control" name="widgetTitle" ng-model="formfield.title">
+ </div>
+ </div>
+ </div> -->
+<!-- <div ng-repeat="(formfield_key,formfield_value) in widget.reportData.formFieldList[0]" class="form-group">
+ <label for="widgetTitle" class="col-sm-2 control-label">{{formfield_key}}</label>
+ <div style="margin-left:120px" class="col-sm-10">
+ <input type="text" class="form-control" name="widgetTitle" ng-model="formfield_value">
+ </div>
+ </div> -->
+
+
+ <div ng-if="widget.settingsModalOptions.partialTemplateUrl"
+ ng-include="widget.settingsModalOptions.partialTemplateUrl"></div>
+
+<!--
+ <div ng-show="true" id="grid1" ui-grid="gridOptions" ui-grid-pagination ui-grid-pinning ui-grid-resize-columns class="grid" style="height: {{gridHeight}}">
+ <div class="no-rows" ng-show="!gridOptions.data.length">
+ <div class="msg">
+ <span>{{widget.reportData.message}}</span>
+ </div>
+ </div>
+ </div>
+ -->
+ </form>
+
+</div>
+
+<div class="modal-footer">
+ <button type="button" class="btn btn-default" ng-click="cancel()">Cancel</button>
+ <button type="button" class="btn btn-primary" ng-click="ok()">OK</button>
+</div> \ No newline at end of file
diff --git a/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboard/widget-settings-template.html b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboard/widget-settings-template.html
new file mode 100644
index 00000000..a57d4366
--- /dev/null
+++ b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboard/widget-settings-template.html
@@ -0,0 +1,22 @@
+<div class="modal-header">
+ <button type="button" class="close" data-dismiss="modal" aria-hidden="true" ng-click="cancel()">&times;</button>
+ <h3>Widget Options <small>{{widget.title}}</small></h3>
+</div>
+
+<div class="modal-body">
+ <form name="form" novalidate class="form-horizontal">
+ <div class="form-group">
+ <label for="widgetTitle" class="col-sm-2 control-label">Title</label>
+ <div class="col-sm-10">
+ <input type="text" class="form-control" name="widgetTitle" ng-model="result.title">
+ </div>
+ </div>
+ <div ng-if="widget.settingsModalOptions.partialTemplateUrl"
+ ng-include="widget.settingsModalOptions.partialTemplateUrl"></div>
+ </form>
+</div>
+
+<div class="modal-footer">
+ <button type="button" class="btn btn-default" ng-click="cancel()">Cancel</button>
+ <button type="button" class="btn btn-primary" ng-click="ok()">OK</button>
+</div> \ No newline at end of file
diff --git a/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboardLayouts/SaveChangesModal.html b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboardLayouts/SaveChangesModal.html
new file mode 100644
index 00000000..f9f6f361
--- /dev/null
+++ b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboardLayouts/SaveChangesModal.html
@@ -0,0 +1,13 @@
+<div class="modal-header">
+ <button type="button" class="close" data-dismiss="modal" aria-hidden="true" ng-click="cancel()">&times;</button>
+ <h3>Unsaved Changes to "{{layout.title}}"</h3>
+</div>
+
+<div class="modal-body">
+ <p>You have {{layout.dashboard.unsavedChangeCount}} unsaved changes on this dashboard. Would you like to save them?</p>
+</div>
+
+<div class="modal-footer">
+ <button type="button" class="btn btn-default" ng-click="cancel()">Don't Save</button>
+ <button type="button" class="btn btn-primary" ng-click="ok()">Save</button>
+</div> \ No newline at end of file
diff --git a/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboardLayouts/SaveChangesModalCtrl.js b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboardLayouts/SaveChangesModalCtrl.js
new file mode 100644
index 00000000..252f9df4
--- /dev/null
+++ b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboardLayouts/SaveChangesModalCtrl.js
@@ -0,0 +1,32 @@
+/*
+ * 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', '$uibModalInstance', 'layout', function ($scope, $uibModalInstance, layout) {
+
+ // add layout to scope
+ $scope.layout = layout;
+
+ $scope.ok = function () {
+ $uibModalInstance.close();
+ };
+
+ $scope.cancel = function () {
+ $uibModalInstance.dismiss();
+ };
+ }]); \ No newline at end of file
diff --git a/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboardLayouts/dashboardLayouts.html b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboardLayouts/dashboardLayouts.html
new file mode 100644
index 00000000..54aef297
--- /dev/null
+++ b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboardLayouts/dashboardLayouts.html
@@ -0,0 +1,19 @@
+<ul ui-sortable="sortableOptions" ng-model="layouts" class="nav nav-tabs layout-tabs">
+ <li ng-repeat="layout in layouts" ng-class="{ active: layout.active }">
+ <a ng-click="makeLayoutActive(layout)">
+ <span ng-dblclick="editTitle(layout)" ng-show="!layout.editingTitle">{{layout.title}}</span>
+ <form action="" class="layout-title" ng-show="layout.editingTitle" ng-submit="saveTitleEdit(layout)">
+ <input type="text" ng-model="layout.title" class="form-control" data-layout="{{layout.id}}">
+ </form>
+ <span ng-if="!layout.locked" ng-click="removeLayout(layout)" class="glyphicon glyphicon-remove icon-erase remove-layout-icon"></span>
+ <!-- <span class="glyphicon glyphicon-pencil"></span> -->
+ <!-- <span class="glyphicon glyphicon-remove"></span> -->
+ </a>
+ </li>
+ <li>
+ <a ng-click="createNewLayout()">
+ <span class="glyphicon glyphicon-plus"></span>
+ </a>
+ </li>
+</ul>
+<div ng-repeat="layout in layouts | filter:isActive" dashboard="layout.dashboard" template-url="app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboard/dashboard.html"></div> \ No newline at end of file
diff --git a/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboardLayouts/dashboardLayouts.js b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboardLayouts/dashboardLayouts.js
new file mode 100644
index 00000000..bbf107a8
--- /dev/null
+++ b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboardLayouts/dashboardLayouts.js
@@ -0,0 +1,151 @@
+/*
+ * 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', '$uibModal',
+ function(LayoutStorage, $timeout, $uibModal) {
+ return {
+ scope: true,
+ templateUrl: function(element, attr) {
+ return attr.templateUrl ? attr.templateUrl : 'app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboardLayouts/dashboardLayouts.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 = $uibModal.open({
+ templateUrl: 'template/SaveChangesModal.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() {
+ console.log("================= saveDashboard called =================")
+ var layout = layoutStorage.getActiveLayout();
+ console.log("===================== layout ===========================");
+ console.log(layout);
+ if (layout) {
+ layout.dashboard.saveDashboard.apply(layout.dashboard, arguments);
+ }
+ };
+
+ var sortableDefaults = {
+ stop: function() {
+ scope.options.saveLayouts();
+ },
+ distance: 5
+ };
+ scope.sortableOptions = angular.extend({}, sortableDefaults, scope.options.sortableOptions || {});
+ }
+ };
+ }
+ ]); \ No newline at end of file
diff --git a/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboardLayouts/dashboardLayouts.spec.js b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboardLayouts/dashboardLayouts.spec.js
new file mode 100644
index 00000000..8533a211
--- /dev/null
+++ b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/dashboardLayouts/dashboardLayouts.spec.js
@@ -0,0 +1,392 @@
+/*
+ * 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';
+
+describe('Directive: dashboard-layouts', function () {
+
+ var $rootScope, element, options, childScope, DashboardState, LayoutStorage, $mockModal, $mockTimeout, toFn;
+
+ // mock UI Sortable
+ beforeEach(function () {
+ angular.module('ui.sortable', []);
+ });
+
+ // load the directive's module
+ beforeEach(module('ui.dashboard', function($provide) {
+ $mockModal = {
+ open: function() {}
+ };
+ $mockTimeout = function(fn, delay) {
+ toFn = fn;
+ };
+ $provide.value('$uibModal', $mockModal);
+ $provide.value('$timeout', $mockTimeout);
+ }));
+
+ beforeEach(inject(function ($compile, _$rootScope_, _DashboardState_, _LayoutStorage_) {
+ // services
+ $rootScope = _$rootScope_;
+ DashboardState = _DashboardState_;
+ LayoutStorage = _LayoutStorage_;
+
+ // options
+ var widgetDefinitions = [
+ {
+ name: 'wt-one',
+ template: '<div class="wt-one-value">{{2 + 2}}</div>'
+ },
+ {
+ name: 'wt-two',
+ template: '<span class="wt-two-value">{{value}}</span>'
+ }
+ ];
+ var defaultWidgets = _.clone(widgetDefinitions);
+ $rootScope.dashboardOptions = options = {
+ widgetButtons: true,
+ widgetDefinitions: widgetDefinitions,
+ defaultLayouts: [
+ {
+ title: 'first',
+ active: true,
+ defaultWidgets: defaultWidgets
+ },
+ {
+ title: 'second',
+ active: false,
+ defaultWidgets: defaultWidgets
+ }
+ ],
+ defaultWidgets: defaultWidgets,
+ storage: {
+ setItem: function(key, val) {
+
+ },
+ getItem: function(key) {
+
+ },
+ removeItem: function(key) {
+
+ }
+ }
+ };
+ $rootScope.value = 10;
+
+ // element setup
+ element = $compile('<div dashboard-layouts="dashboardOptions"></div>')($rootScope);
+ $rootScope.$digest();
+ childScope = element.scope();
+ }));
+
+ it('should not require storage', inject(function($compile) {
+ delete $rootScope.dashboardOptions.storage;
+ expect(function() {
+ var noStorageEl = $compile('<div dashboard-layouts="dashboardOptions"></div>')($rootScope);
+ $rootScope.$digest();
+ }).not.toThrow();
+
+ }));
+
+ it('should be able to use a different dashboard-layouts template', inject(function($compile, $templateCache) {
+ $templateCache.put(
+ 'myCustomTemplate.html',
+ '<ul class="my-custom-tabs layout-tabs">' +
+ '<li ng-repeat="layout in layouts" ng-class="{ active: layout.active }">' +
+ '<a ng-click="makeLayoutActive(layout)">' +
+ '<span ng-dblclick="editTitle(layout)" ng-show="!layout.editingTitle">{{layout.title}}</span>' +
+ '<form action="" class="layout-title" ng-show="layout.editingTitle" ng-submit="saveTitleEdit(layout)">' +
+ '<input type="text" ng-model="layout.title" class="form-control" data-layout="{{layout.id}}">' +
+ '</form>' +
+ '<span ng-click="removeLayout(layout)" class="glyphicon glyphicon-remove icon-erase"></span>' +
+ '<!-- <span class="glyphicon glyphicon-pencil"></span> -->' +
+ '<!-- <span class="glyphicon glyphicon-remove"></span> -->' +
+ '</a>' +
+ '</li>' +
+ '<li>' +
+ '<a ng-click="createNewLayout()">' +
+ '<span class="glyphicon glyphicon-plus"></span>' +
+ '</a>' +
+ '</li>' +
+ '</ul>' +
+ '<div ng-repeat="layout in layouts | filter:isActive" dashboard="layout.dashboard" templateUrl="template/dashboard.html"></div>'
+ );
+ var customElement = $compile('<div dashboard-layouts="dashboardOptions" template-url="myCustomTemplate.html"></div>')($rootScope);
+ $rootScope.$digest();
+ expect(customElement.find('ul.my-custom-tabs').length).toEqual(1);
+
+ }));
+
+ it('should set the first dashboard to active if there is not one already active', inject(function($compile) {
+ options.defaultLayouts[0].active = options.defaultLayouts[1].active = false;
+ element = $compile('<div dashboard-layouts="dashboardOptions"></div>')($rootScope);
+ $rootScope.$digest();
+ childScope = element.scope();
+
+ var layouts = childScope.layouts;
+ var active;
+ for (var i = 0; i < layouts.length; i++) {
+ if (layouts[i].active) {
+ active = layouts[i];
+ break;
+ }
+ };
+ expect(active).not.toBeUndefined();
+ }));
+
+ describe('the createNewLayout method', function() {
+
+ it('should call the add and save methods of LayoutStorage', function() {
+ spyOn(LayoutStorage.prototype, 'add');
+ spyOn(LayoutStorage.prototype, 'save');
+
+ childScope.createNewLayout();
+ expect(LayoutStorage.prototype.add).toHaveBeenCalled();
+ expect(LayoutStorage.prototype.save).toHaveBeenCalled();
+ });
+
+ it('should return the newly created layout object', function() {
+ var result = childScope.createNewLayout();
+ expect(typeof result).toEqual('object');
+ });
+
+ it('should set active=true on the newly created layout', function() {
+ var result = childScope.createNewLayout();
+ expect(result.active).toEqual(true);
+ });
+
+ it('should set defaultWidgets to dashboardOptions.defaultWidgets if it is present', function() {
+ var result = childScope.createNewLayout();
+ expect(result.defaultWidgets === options.defaultWidgets).toEqual(true);
+ });
+
+ it('should set defaultWidgets to an empty array if dashboardOptions.defaultWidgets is not present', inject(function($compile) {
+ delete options.defaultWidgets;
+ element = $compile('<div dashboard-layouts="dashboardOptions"></div>')($rootScope);
+ $rootScope.$digest();
+ childScope = element.scope();
+ var result = childScope.createNewLayout();
+ expect(result.defaultWidgets).toEqual([]);
+ }));
+
+ });
+
+ describe('the removeLayout method', function() {
+
+ it('should call the remove and save methods of LayoutStorage', function() {
+ spyOn(LayoutStorage.prototype, 'remove');
+ spyOn(LayoutStorage.prototype, 'save');
+
+ childScope.removeLayout(childScope.layouts[0]);
+ expect(LayoutStorage.prototype.remove).toHaveBeenCalled();
+ expect(LayoutStorage.prototype.save).toHaveBeenCalled();
+ });
+
+ it('should call remove with the layout it was passed', function() {
+ spyOn(LayoutStorage.prototype, 'remove');
+ var layout = childScope.layouts[0];
+ childScope.removeLayout(layout);
+ expect(LayoutStorage.prototype.remove.calls.argsFor(0)[0]).toEqual(layout);
+ });
+
+ });
+
+ describe('the makeLayoutActive method', function() {
+
+ it('should call _makeLayoutActive if there is not a currently active dashboard with unsaved changes', function() {
+ spyOn(childScope, '_makeLayoutActive');
+ var layout = childScope.layouts[1];
+ childScope.makeLayoutActive(layout);
+ expect(childScope._makeLayoutActive).toHaveBeenCalled();
+ });
+
+ describe('when there are unsaved changes on the current dashboard', function() {
+
+ var current, options, successCb, errorCb, layout;
+
+ beforeEach(function() {
+ current = childScope.layouts[0];
+ current.dashboard.unsavedChangeCount = 1;
+
+ spyOn($mockModal, 'open').and.callFake(function(arg) {
+ options = arg;
+ return {
+ result: {
+ then: function(success, error) {
+ successCb = success;
+ errorCb = error;
+ }
+ }
+ }
+ });
+
+ layout = childScope.layouts[1];
+ childScope.makeLayoutActive(layout);
+ });
+
+ it('should create a modal', function() {
+ expect($mockModal.open).toHaveBeenCalled();
+ });
+
+ it('should resolve layout to the layout to be made active', function() {
+ expect(options.resolve.layout()).toEqual(layout);
+ });
+
+ it('should provide a success callback that saves the current dashboard and then calls _makeLayoutActive', function() {
+ spyOn(current.dashboard, 'saveDashboard');
+ spyOn(childScope, '_makeLayoutActive');
+ successCb();
+ expect(current.dashboard.saveDashboard).toHaveBeenCalled();
+ expect(childScope._makeLayoutActive).toHaveBeenCalled();
+ expect(childScope._makeLayoutActive.calls.argsFor(0)[0]).toEqual(layout);
+ });
+
+ it('should provide an error callback that only calls _makeLayoutActive', function() {
+ spyOn(current.dashboard, 'saveDashboard');
+ spyOn(childScope, '_makeLayoutActive');
+ errorCb();
+ expect(current.dashboard.saveDashboard).not.toHaveBeenCalled();
+ expect(childScope._makeLayoutActive).toHaveBeenCalled();
+ expect(childScope._makeLayoutActive.calls.argsFor(0)[0]).toEqual(layout);
+ });
+
+ });
+
+ });
+
+ describe('the editTitle method', function() {
+
+ it('should set the editingTitle attribute to true on the layout it is passed', function() {
+ var layout = { id: '1' };
+ childScope.editTitle(layout);
+ $rootScope.$digest();
+ expect(layout.editingTitle).toEqual(true);
+ toFn();
+ });
+
+ });
+
+ describe('the saveTitleEdit method', function() {
+
+ it('should set editingTitle to false', function() {
+ var layout = { id: '1' };
+ childScope.saveTitleEdit(layout);
+ expect(layout.editingTitle).toEqual(false);
+ });
+
+ it('should call layoutStorage.save', function() {
+ var layout = { id: '1' };
+ spyOn(LayoutStorage.prototype, 'save').and.callThrough();
+ childScope.saveTitleEdit(layout);
+ expect(LayoutStorage.prototype.save).toHaveBeenCalled();
+ });
+
+ });
+
+ describe('the saveLayouts method', function() {
+
+ it('should call LayoutStorage.save', function() {
+ spyOn(LayoutStorage.prototype, 'save').and.callThrough();
+ $rootScope.dashboardOptions.saveLayouts();
+ expect(LayoutStorage.prototype.save).toHaveBeenCalled();
+ });
+
+ it('should call LayoutStorage.save with true as the first arg', function() {
+ spyOn(LayoutStorage.prototype, 'save').and.callThrough();
+ $rootScope.dashboardOptions.saveLayouts();
+ expect(LayoutStorage.prototype.save.calls.argsFor(0)[0]).toEqual(true);
+ });
+
+ });
+ describe('the proxy methods to active layout', function() {
+
+ var mockDash, galSpy;
+
+ beforeEach(function() {
+ mockDash = {
+ active: true,
+ dashboard: {
+ addWidget: function() {},
+ loadWidgets: function() {},
+ saveDashboard: function() {}
+ }
+ };
+ spyOn(mockDash.dashboard, 'addWidget');
+ spyOn(mockDash.dashboard, 'loadWidgets');
+ spyOn(mockDash.dashboard, 'saveDashboard');
+ galSpy = spyOn(LayoutStorage.prototype, 'getActiveLayout').and;
+ galSpy.returnValue(mockDash);
+ });
+
+ describe('the addWidget method', function() {
+
+ it('should call dashboard.addWidget method of the active layout', function() {
+ options.addWidget(1,2,3);
+ expect(mockDash.dashboard.addWidget).toHaveBeenCalled();
+ 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() {
+ galSpy.returnValue(null);
+ expect(function() {
+ options.addWidget();
+ }).not.toThrow();
+ });
+
+ });
+
+ describe('the loadWidgets method', function() {
+
+ it('should call dashboard.loadWidgets of the current layout', function() {
+ options.loadWidgets(1,2,3);
+ expect(mockDash.dashboard.loadWidgets).toHaveBeenCalled();
+ 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() {
+ galSpy.returnValue(null);
+ expect(function() {
+ options.loadWidgets();
+ }).not.toThrow();
+ });
+
+ });
+
+ describe('the saveDashboard method', function() {
+
+ it('should call dashboard.saveDashboard of the current layout', function() {
+ options.saveDashboard(1,2,3);
+ expect(mockDash.dashboard.saveDashboard).toHaveBeenCalled();
+ 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() {
+ galSpy.returnValue(null);
+ expect(function() {
+ options.saveDashboard();
+ }).not.toThrow();
+ });
+
+ });
+
+ });
+
+}); \ No newline at end of file
diff --git a/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/widget/DashboardWidgetCtrl.js b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/widget/DashboardWidgetCtrl.js
new file mode 100644
index 00000000..9ac57b19
--- /dev/null
+++ b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/widget/DashboardWidgetCtrl.js
@@ -0,0 +1,246 @@
+/*
+ * 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('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 = '<div ng-include="\'' + widget.templateUrl + '\'"></div>';
+
+ } else if (widget.template) {
+
+ // Direct string template
+ templateString = widget.template;
+
+ } else {
+
+ // Assume attribute directive
+ templateString = '<div ' + widget.directive;
+
+ // Check if data attribute was specified
+ if (widget.dataAttrName) {
+ widget.attrs = widget.attrs || {};
+ widget.attrs[widget.dataAttrName] = 'widgetData';
+ }
+
+ // Check for specified attributes
+ if (widget.attrs) {
+
+ // 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 += '></div>';
+ }
+ return templateString;
+ };
+
+ $scope.grabResizer = function(e) {
+
+ var widget = $scope.widget;
+ var widgetElm = $element.find('.widget');
+
+ // ignore middle- and right-click
+ if (e.which !== 1) {
+ return;
+ }
+
+ 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('<div class="widget-resizer-marquee" style="height: ' + pixelHeight + 'px; width: ' + pixelWidth + 'px; z-index:'+ 200 +';"></div>');
+ widgetElm.append($marquee);
+ // create an overlaying div to block other widgets in order to stop their iframe events from being triggered
+ var $marquee2 = angular.element('<div style=" position: absolute; top: 0; left: 0; height: ' + pixelHeight + 'px; width: ' + (pixelWidth+200) + 'px; z-index:'+ 100 +';"></div>');
+ widgetElm.append($marquee2);
+
+ // determine the unit/pixel ratio
+ var transformMultiplier = unitWidth / pixelWidth;
+
+ // updates marquee with preview of new width
+ var mousemove = function(e) {
+ var curX = e.clientX;
+// console.log(curX);
+// console.log(e);
+ var pixelChange = curX - initX;
+ var newWidth = pixelWidth + pixelChange;
+ $marquee.css('width', newWidth + 'px');
+ $marquee2.css('width', (newWidth + 200) + 'px');
+
+ };
+
+ // sets new widget width on mouseup
+ var mouseup = function(e) {
+ // remove listener and marquee
+ jQuery($window).off('mousemove', mousemove);
+ $marquee.remove();
+ $marquee2.remove();
+
+ // calculate change in units
+ var curX = e.clientX;
+ var pixelChange = curX - initX;
+ var unitChange = Math.round(pixelChange * transformMultiplier * 100) / 100;
+
+ // 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
+ });
+ };
+
+// jQuery($window).on('mousemove', mousemove).one('mouseup', mouseup);
+ 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;
+ }
+
+ 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();
+
+ // create marquee element for resize action
+ var $marquee = angular.element('<div class="widget-resizer-marquee" style="height: ' + pixelHeight + 'px; width: ' + pixelWidth + 'px;"></div>');
+ widgetElm.append($marquee);
+
+ // 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');
+ };
+
+ // sets new widget width on mouseup
+ var mouseup = function(e) {
+ // remove listener and marquee
+ jQuery($window).off('mousemove', mousemove);
+ $marquee.remove();
+
+ // 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');
+
+ var diff = pixelChange;
+ var height = parseInt(widgetContainer.css('height'), 10);
+ var newHeight = (height + diff);
+
+ //$scope.widget.style.height = newHeight + 'px';
+
+ $scope.widget.setHeight(newHeight + 'px');
+
+ $scope.$emit('widgetChanged', $scope.widget);
+ $scope.$apply(); // make AngularJS to apply style changes
+
+ $scope.$broadcast('widgetResized', {
+ height: newHeight
+ });
+ };
+
+ jQuery($window).on('mousemove', mousemove).one('mouseup', mouseup);
+ };
+
+ // 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);
+ });
+ };
+
+ // saves whatever is in the title input as the new title
+ $scope.saveTitleEdit = function(widget) {
+ widget.editingTitle = false;
+ $scope.$emit('widgetChanged', widget);
+ };
+
+ $scope.compileTemplate = function() {
+ var container = $scope.findWidgetContainer($element);
+ var templateString = $scope.makeTemplateString();
+ var widgetElement = angular.element(templateString);
+
+ container.empty();
+ container.append(widgetElement);
+ $compile(widgetElement)($scope);
+ };
+
+ $scope.findWidgetContainer = function(element) {
+ // widget placeholder is the first (and only) child of .widget-content
+ return element.find('.widget-content');
+ };
+ }
+ ]); \ No newline at end of file
diff --git a/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/widget/DashboardWidgetCtrl.spec.js b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/widget/DashboardWidgetCtrl.spec.js
new file mode 100644
index 00000000..55604646
--- /dev/null
+++ b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/widget/DashboardWidgetCtrl.spec.js
@@ -0,0 +1,164 @@
+'use strict';
+
+describe('Controller: DashboardWidgetCtrl', function() {
+
+ var $scope, $element, $timeout, injections;
+
+ beforeEach(module('ui.dashboard'));
+
+ beforeEach(inject(function($rootScope, $controller){
+ $scope = $rootScope.$new();
+ $element = angular.element('<div><div class="widget"></div></div>');
+ $timeout = function timeout(fn) {
+ fn();
+ };
+ injections = {
+ $scope: $scope,
+ $element: $element,
+ $timeout: $timeout
+ };
+ spyOn(injections, '$timeout');
+ $controller('DashboardWidgetCtrl', injections);
+ }));
+
+ describe('the makeTemplateString method', function() {
+
+ it('should return a string', function() {
+ $scope.widget = {
+ templateUrl: 'some/template.html'
+ };
+ expect(typeof $scope.makeTemplateString()).toEqual('string');
+ });
+
+ it('should use ng-include if templateUrl is specified on widget, despite any other options', function() {
+ $scope.widget = {
+ templateUrl: 'some/template.html',
+ template: 'not this one',
+ directive: 'or-this',
+ attrs: {
+ something: 'awesome',
+ other: 'thing'
+ }
+ };
+ expect($scope.makeTemplateString()).toMatch(/ng-include="'some\/template\.html'"/);
+ });
+
+ it('should return widget.template if specified, regardless of presence of directive or attrs', function() {
+ $scope.widget = {
+ template: '<div class="testing"></div>',
+ directive: 'no-good'
+ };
+ expect($scope.makeTemplateString()).toEqual($scope.widget.template);
+ });
+
+ it('should use widget.directive as attribute directive', function() {
+ $scope.widget = {
+ directive: 'ng-awesome'
+ };
+ expect($scope.makeTemplateString()).toEqual('<div ng-awesome></div>');
+ });
+
+ it('should attach attributes if provided', function() {
+ $scope.widget = {
+ directive: 'ng-awesome',
+ attrs: {
+ 'ng-awesome': 'test1',
+ other: 'attr',
+ more: 'stuff'
+ }
+ };
+ expect($scope.makeTemplateString()).toEqual('<div ng-awesome="test1" other="attr" more="stuff"></div>');
+ });
+
+ it('should place widgetData into dataAttrName attribute if specified', function() {
+ $scope.widget = {
+ directive: 'ng-awesome',
+ attrs: {
+ 'ng-awesome': 'test1',
+ other: 'attr',
+ more: 'stuff'
+ },
+ dataAttrName: 'data'
+ };
+ expect($scope.makeTemplateString()).toEqual('<div ng-awesome="test1" other="attr" more="stuff" data="widgetData"></div>');
+ });
+
+ it('should add attrs to the widget object if it does not exist and dataAttrName is specified', function() {
+ $scope.widget = {
+ directive: 'ng-awesome',
+ dataAttrName: 'data'
+ };
+ expect($scope.makeTemplateString()).toEqual('<div ng-awesome data="widgetData"></div>');
+ });
+
+ });
+
+ describe('the grabResizer method', function() {
+
+ var evt, widget, WidgetModel;
+
+ beforeEach(inject(function (_WidgetModel_) {
+ WidgetModel = _WidgetModel_;
+ }));
+
+ beforeEach(function() {
+ evt = {
+ stopPropagation: jasmine.createSpy('stopPropagation'),
+ originalEvent: {
+ preventDefault: jasmine.createSpy('preventDefault')
+ },
+ clientX: 100,
+ which: 1
+ };
+ $scope.widget = widget = new WidgetModel({
+ style: {
+ width: '30%'
+ }
+ });
+ });
+
+ it('should do nothing if event.which is not 1 (left click)', function() {
+ evt.which = 2;
+ $scope.grabResizer(evt);
+ expect(evt.stopPropagation).not.toHaveBeenCalled();
+ });
+
+ it('should call stopPropagation and preventDefault', function() {
+ $scope.grabResizer(evt);
+ expect(evt.stopPropagation).toHaveBeenCalled();
+ expect(evt.originalEvent.preventDefault).toHaveBeenCalled();
+ });
+
+ it('should add a .widget-resizer-marquee element to the .widget element', function() {
+ $scope.grabResizer(evt);
+ expect($element.find('.widget-resizer-marquee').length).toBeGreaterThan(0);
+ });
+
+ });
+
+ describe('the editTitle method', function() {
+
+ it('should set editingTitle=true on the widget object', function() {
+ var widget = {};
+ $scope.editTitle(widget);
+ expect(widget.editingTitle).toEqual(true);
+ });
+
+ it('should call $timeout', function() {
+ var widget = {};
+ $scope.editTitle(widget);
+ expect(injections.$timeout).toHaveBeenCalled();
+ });
+
+ });
+
+ describe('the saveTitleEdit method', function() {
+
+ it('should set editingTitle=false', function() {
+ var widget = { editingTitle: true };
+ $scope.saveTitleEdit(widget);
+ expect(widget.editingTitle).toEqual(false);
+ });
+ });
+
+}); \ No newline at end of file
diff --git a/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/widget/widget.js b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/widget/widget.js
new file mode 100644
index 00000000..f5a6ebef
--- /dev/null
+++ b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/widget/widget.js
@@ -0,0 +1,64 @@
+/*
+ * 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('widget', ['$injector', function ($injector) {
+
+ return {
+
+ controller: 'DashboardWidgetCtrl',
+
+ link: function (scope) {
+
+ var widget = scope.widget;
+ var dataModelType = widget.dataModelType;
+
+ // set up data source
+ if (dataModelType) {
+ var DataModelConstructor; // data model constructor function
+
+ if (angular.isFunction(dataModelType)) {
+ DataModelConstructor = dataModelType;
+ } else if (angular.isString(dataModelType)) {
+ $injector.invoke([dataModelType, function (DataModelType) {
+ DataModelConstructor = DataModelType;
+ }]);
+ } else {
+ throw new Error('widget dataModelType should be function or string');
+ }
+
+ var ds;
+ if (widget.dataModelArgs) {
+ ds = new DataModelConstructor(widget.dataModelArgs);
+ } else {
+ ds = new DataModelConstructor();
+ }
+ widget.dataModel = ds;
+ ds.setup(widget, scope);
+ ds.init();
+ scope.$on('$destroy', _.bind(ds.destroy,ds));
+ }
+
+ // Compile the widget template, emit add event
+ scope.compileTemplate();
+ scope.$emit('widgetAdded', widget);
+
+ }
+
+ };
+ }]);
diff --git a/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/widget/widget.spec.js b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/widget/widget.spec.js
new file mode 100644
index 00000000..0997e071
--- /dev/null
+++ b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/directives/widget/widget.spec.js
@@ -0,0 +1,104 @@
+// 'use strict';
+
+describe('Directive: widget', function () {
+
+ var element, scope, rootScope, isoScope, compile, provide;
+
+ function Type() {
+ }
+
+ Type.prototype = {
+ setup: function () {
+ },
+ init: function () {
+ },
+ destroy: function () {
+ }
+ };
+
+ beforeEach(function () {
+ spyOn(Type.prototype, 'setup');
+ spyOn(Type.prototype, 'init');
+ spyOn(Type.prototype, 'destroy');
+ // define mock objects here
+ });
+
+ // load the directive's module
+ beforeEach(module('ui.dashboard', function ($provide, $controllerProvider) {
+ provide = $provide;
+ // Inject dependencies like this:
+ $controllerProvider.register('DashboardWidgetCtrl', function ($scope) {
+
+ });
+
+ }));
+
+ beforeEach(inject(function ($compile, $rootScope) {
+ // Cache these for reuse
+ rootScope = $rootScope;
+ compile = $compile;
+
+ // Other setup, e.g. helper functions, etc.
+
+ // Set up the outer scope
+ scope = $rootScope.$new();
+ scope.widget = {
+ dataModelType: Type
+ };
+
+ compileTemplate = jasmine.createSpy('compileTemplate');
+ scope.compileTemplate = compileTemplate;
+ }));
+
+ function compileWidget() {
+ // Define and compile the element
+ element = angular.element('<div widget><div class="widget-content"></div></div>');
+ element = compile(element)(scope);
+ scope.$digest();
+ isoScope = element.isolateScope();
+ }
+
+ it('should create a new instance of dataModelType if provided in scope.widget', function () {
+ compileWidget();
+ expect(scope.widget.dataModel instanceof Type).toBe(true);
+ });
+
+ it('should call setup and init on the new dataModel', function () {
+ compileWidget();
+ expect(Type.prototype.setup).toHaveBeenCalled();
+ expect(Type.prototype.init).toHaveBeenCalled();
+ });
+
+ it('should call compile template', function () {
+ compileWidget();
+ expect(scope.compileTemplate).toHaveBeenCalled();
+ });
+
+ it('should create a new instance of dataModelType from string name', function () {
+ // register data model with $injector
+ provide.factory('StringNameDataModel', function () {
+ return Type;
+ });
+
+ scope.widget = {
+ dataModelType: 'StringNameDataModel'
+ };
+
+ compileWidget();
+
+ expect(scope.widget.dataModel instanceof Type).toBe(true);
+ expect(Type.prototype.setup).toHaveBeenCalled();
+ expect(Type.prototype.init).toHaveBeenCalled();
+ });
+
+ it('should validate data model type', function () {
+ scope.widget = {
+ dataModelType: {}
+ };
+
+ expect(function () {
+ compileWidget()
+ }).toThrowError();
+ });
+
+}); \ No newline at end of file
diff --git a/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/models/DashboardState.js b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/models/DashboardState.js
new file mode 100644
index 00000000..67948ead
--- /dev/null
+++ b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/models/DashboardState.js
@@ -0,0 +1,180 @@
+/*
+ * 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('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;
+ }
+
+ 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;
+ }
+
+ var serialized = _.map(widgets, function (widget) {
+ return widget.serialize();
+ });
+
+ var item = { widgets: serialized, hash: this.hash };
+
+ if (this.stringify) {
+ item = JSON.stringify(item);
+ }
+
+ this.storage.setItem(this.id, item);
+ return true;
+ },
+
+ /**
+ * 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 () {
+
+ if (!this.storage) {
+ return null;
+ }
+
+ var serialized;
+
+ // try loading storage item
+ serialized = this.storage.getItem( this.id );
+
+ 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;
+ }
+ },
+
+ _handleSyncLoad: function(serialized) {
+
+ var deserialized, result = [];
+
+ if (!serialized) {
+ return null;
+ }
+
+ if (this.stringify) {
+ try { // to deserialize the string
+
+ deserialized = JSON.parse(serialized);
+
+ } catch (e) {
+
+ // bad JSON, log a warning and return
+ $log.warn('Serialized dashboard state was malformed and could not be parsed: ', serialized);
+ return null;
+
+ }
+ }
+ else {
+ deserialized = serialized;
+ }
+
+ // check hash against current hash
+ if (deserialized.hash !== this.hash) {
+
+ $log.info('Serialized dashboard from storage was stale (old hash: ' + deserialized.hash + ', new hash: ' + this.hash + ')');
+ this.storage.removeItem(this.id);
+ return null;
+
+ }
+
+ // Cache widgets
+ var savedWidgetDefs = deserialized.widgets;
+
+ // instantiate widgets from stored data
+ for (var i = 0; i < savedWidgetDefs.length; i++) {
+
+ // deserialized object
+ var savedWidgetDef = savedWidgetDefs[i];
+
+ // widget definition to use
+ var widgetDefinition = this.widgetDefinitions.getByName(savedWidgetDef.name);
+
+ // 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;
+ }
+
+ // 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;
+ }
+
+ // push instantiated widget to result array
+ result.push(savedWidgetDef);
+ }
+
+ return result;
+ },
+
+ _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);
+ }
+ );
+
+ return deferred.promise;
+ }
+
+ };
+ return DashboardState;
+ }]); \ No newline at end of file
diff --git a/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/models/LayoutStorage.js b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/models/LayoutStorage.js
new file mode 100644
index 00000000..3685fd3f
--- /dev/null
+++ b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/models/LayoutStorage.js
@@ -0,0 +1,253 @@
+/*
+ * 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 = {};
+ },
+
+ setItem: function(id, value) {
+ this.states[id] = value;
+ this.save();
+ },
+
+ getItem: function(id) {
+ return this.states[id];
+ },
+
+ removeItem: function(id) {
+ delete this.states[id];
+ this.save();
+ },
+
+ 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;
+ },
+
+ _addDefaultLayouts: function() {
+ var self = this;
+ var defaults = this.lockDefaultLayouts ? { locked: true } : {};
+ angular.forEach(this.defaultLayouts, function(layout) {
+ self.add(angular.extend(_.clone(defaults), layout));
+ });
+ },
+
+ _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;
+ },
+
+ _handleSyncLoad: function(serialized) {
+
+ var deserialized;
+
+ if (this.stringifyStorage) {
+ try {
+
+ deserialized = JSON.parse(serialized);
+
+ } catch (e) {
+ this._addDefaultLayouts();
+ return;
+ }
+ } else {
+
+ deserialized = serialized;
+
+ }
+
+ if (this.storageHash !== deserialized.storageHash) {
+ this._addDefaultLayouts();
+ return;
+ }
+ this.states = deserialized.states;
+ this.add(deserialized.layouts);
+ },
+
+ _handleAsyncLoad: function(promise) {
+ var self = this;
+ promise.then(
+ angular.bind(self, this._handleSyncLoad),
+ angular.bind(self, this._addDefaultLayouts)
+ );
+ },
+
+ _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;
+ }
+ },
+
+ _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 LayoutStorage;
+ }); \ No newline at end of file
diff --git a/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/models/LayoutStorage.spec.js b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/models/LayoutStorage.spec.js
new file mode 100644
index 00000000..3310cad9
--- /dev/null
+++ b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/models/LayoutStorage.spec.js
@@ -0,0 +1,692 @@
+'use strict';
+
+describe('Factory: LayoutStorage', function () {
+
+ // mock UI Sortable
+ beforeEach(function () {
+ angular.module('ui.sortable', []);
+ });
+
+ // load the service's module
+ beforeEach(module('ui.dashboard'));
+
+ // instantiate service
+ var LayoutStorage;
+ beforeEach(inject(function (_LayoutStorage_) {
+ LayoutStorage = _LayoutStorage_;
+ }));
+
+ describe('the constructor', function() {
+
+ var storage, options;
+
+ beforeEach(function() {
+ options = {
+ storageId: 'testingStorage',
+ storage: {
+ setItem: function(key, value) {
+
+ },
+ getItem: function(key) {
+
+ },
+ removeItem: function(key) {
+
+ }
+ },
+ storageHash: 'ds5f9d1f',
+ stringifyStorage: true,
+ widgetDefinitions: [
+
+ ],
+ defaultLayouts: [
+ {title: 'something'},
+ {title: 'something'},
+ {title: 'something'}
+ ],
+ widgetButtons: false,
+ explicitSave: false,
+ settingsModalOptions: {},
+ onSettingsClose: function() {
+
+ },
+ onSettingsDismiss: function() {
+
+ }
+ };
+ storage = new LayoutStorage(options);
+ });
+
+ it('should provide an empty implementation of storage if it is not provided', function() {
+ delete options.storage;
+ var stateless = new LayoutStorage(options);
+ var noop = stateless.storage;
+ angular.forEach(['setItem', 'getItem', 'removeItem'], function(method) {
+ expect(typeof noop[method]).toEqual('function');
+ expect(noop[method]).not.toThrow();
+ noop[method]();
+ });
+ });
+
+ it('should set a subset of the options directly on the LayoutStorage instance itself', function() {
+ var properties = {
+ id: 'storageId',
+ storage: 'storage',
+ storageHash: 'storageHash',
+ stringifyStorage: 'stringifyStorage',
+ widgetDefinitions: 'widgetDefinitions',
+ defaultLayouts: 'defaultLayouts',
+ widgetButtons: 'widgetButtons',
+ explicitSave: 'explicitSave',
+ settingsModalOptions: 'settingsModalOptions',
+ onSettingsClose: 'onSettingsClose',
+ onSettingsDismiss: 'onSettingsDismiss'
+ };
+
+ angular.forEach(properties, function(val, key) {
+ expect( storage[key] ).toEqual( options[val] );
+ });
+
+ });
+
+ it('should set stringify as true by default', function() {
+ delete options.stringifyStorage;
+ storage = new LayoutStorage(options);
+ expect(storage.stringifyStorage).toEqual(true);
+ });
+
+ it('should allow stringify to be overridden by option', function() {
+ options.stringifyStorage = false;
+ storage = new LayoutStorage(options);
+ expect(storage.stringifyStorage).toEqual(false);
+ });
+
+ it('should create a layouts array and states object', function() {
+ expect(storage.layouts instanceof Array).toEqual(true);
+ expect(typeof storage.states).toEqual('object');
+ });
+
+ it('should call load', function() {
+ spyOn(LayoutStorage.prototype, 'load');
+ storage = new LayoutStorage(options);
+ expect(LayoutStorage.prototype.load).toHaveBeenCalled();
+ });
+
+ });
+
+ describe('the load method', function() {
+
+ var options, storage;
+
+ beforeEach(function() {
+ options = {
+ storageId: 'testingStorage',
+ storage: {
+ setItem: function(key, value) {
+
+ },
+ getItem: function(key) {
+
+ },
+ removeItem: function(key) {
+
+ }
+ },
+ storageHash: 'ds5f9d1f',
+ stringifyStorage: true,
+ widgetDefinitions: [
+
+ ],
+ defaultLayouts: [
+ {title: 'something'},
+ {title: 'something'},
+ {title: 'something'}
+ ],
+ widgetButtons: false,
+ explicitSave: false
+ }
+ storage = new LayoutStorage(options);
+ });
+
+ it('should use the default layouts if no stored info was found', function() {
+ expect(storage.layouts.length).toEqual(options.defaultLayouts.length);
+ });
+
+ it('should clone default layouts rather than use them directly', function() {
+ expect(storage.layouts.indexOf(options.defaultLayouts[0])).toEqual(-1);
+ });
+
+ it('should use the result from getItem for layouts.', function() {
+ spyOn(options.storage, 'getItem').and.returnValue(JSON.stringify({
+ storageHash: 'ds5f9d1f',
+ layouts: [
+ { id: 0, title: 'title', defaultWidgets: [], active: true },
+ { id: 1, title: 'title2', defaultWidgets: [], active: false },
+ { id: 2, title: 'title3', defaultWidgets: [], active: false },
+ { id: 3, title: 'custom', defaultWidgets: [], active: false }
+ ],
+ states: {
+ 0: {},
+ 1: {},
+ 2: {}
+ }
+ }));
+ storage.load();
+ expect(storage.layouts.map(function(l) {return l.title})).toEqual(['title', 'title2', 'title3', 'custom']);
+ });
+
+ it('should NOT use result from getItem for layouts if the storageHash doesnt match', function() {
+ spyOn(options.storage, 'getItem').and.returnValue(JSON.stringify({
+ storageHash: 'alskdjf02iej',
+ layouts: [
+ { id: 0, title: 'title', defaultWidgets: [], active: true },
+ { id: 1, title: 'title2', defaultWidgets: [], active: false },
+ { id: 2, title: 'title3', defaultWidgets: [], active: false },
+ { id: 3, title: 'custom', defaultWidgets: [], active: false }
+ ],
+ states: {
+ 0: {},
+ 1: {},
+ 2: {}
+ }
+ }));
+ storage.load();
+ expect(storage.layouts.map(function(l) {return l.title})).toEqual(['something', 'something', 'something']);
+ });
+
+ it('should be able to handle async loading via promise', inject(function($rootScope,$q) {
+ var deferred = $q.defer();
+ spyOn(options.storage, 'getItem').and.returnValue(deferred.promise);
+ storage.load();
+ expect(storage.layouts).toEqual([]);
+ deferred.resolve(JSON.stringify({
+ storageHash: 'ds5f9d1f',
+ layouts: [
+ { id: 0, title: 'title', defaultWidgets: [], active: true },
+ { id: 1, title: 'title2', defaultWidgets: [], active: false },
+ { id: 2, title: 'title3', defaultWidgets: [], active: false },
+ { id: 3, title: 'custom', defaultWidgets: [], active: false }
+ ],
+ states: {
+ 0: {},
+ 1: {},
+ 2: {}
+ }
+ }));
+ $rootScope.$apply();
+ expect(storage.layouts.map(function(l) {return l.title})).toEqual(['title', 'title2', 'title3', 'custom']);
+ }));
+
+ it('should load defaults if the deferred is rejected', inject(function($rootScope,$q) {
+ var deferred = $q.defer();
+ spyOn(options.storage, 'getItem').and.returnValue(deferred.promise);
+ storage.load();
+ deferred.reject();
+ $rootScope.$apply();
+ expect(storage.layouts.map(function(l) {return l.title})).toEqual(['something', 'something', 'something']);
+ }));
+
+ it('should load defaults if the json is malformed', inject(function($rootScope,$q) {
+ var deferred = $q.defer();
+ spyOn(options.storage, 'getItem').and.returnValue(deferred.promise);
+ storage.load();
+ expect(storage.layouts).toEqual([]);
+ deferred.resolve(JSON.stringify({
+ storageHash: 'ds5f9d1f',
+ layouts: [
+ { id: 0, title: 'title', defaultWidgets: [], active: true },
+ { id: 1, title: 'title2', defaultWidgets: [], active: false },
+ { id: 2, title: 'title3', defaultWidgets: [], active: false },
+ { id: 3, title: 'custom', defaultWidgets: [], active: false }
+ ],
+ states: {
+ 0: {},
+ 1: {},
+ 2: {}
+ }
+ }).replace('{','{{'));
+ $rootScope.$apply();
+ expect(storage.layouts.map(function(l) {return l.title})).toEqual(['something', 'something', 'something']);
+ }));
+
+ it('should not try to JSON.parse the result if stringifyStorage is false.', function() {
+ options.stringifyStorage = false;
+ storage = new LayoutStorage(options);
+ spyOn(options.storage, 'getItem').and.returnValue({
+ storageHash: 'ds5f9d1f',
+ layouts: [
+ { id: 0, title: 'title', defaultWidgets: [], active: true },
+ { id: 1, title: 'title2', defaultWidgets: [], active: false },
+ { id: 2, title: 'title3', defaultWidgets: [], active: false },
+ { id: 3, title: 'custom', defaultWidgets: [], active: false }
+ ],
+ states: {
+ 0: {},
+ 1: {},
+ 2: {}
+ }
+ });
+ storage.load();
+ expect(storage.layouts.map(function(l) {return l.title})).toEqual(['title', 'title2', 'title3', 'custom']);
+ });
+
+ });
+
+ describe('the add method', function() {
+
+ var storage, options;
+
+ beforeEach(function() {
+ options = {
+ storageId: 'testingStorage',
+ storage: {
+ setItem: function(key, value) {
+
+ },
+ getItem: function(key) {
+
+ },
+ removeItem: function(key) {
+
+ }
+ },
+ storageHash: 'ds5f9d1f',
+ stringifyStorage: true,
+ widgetDefinitions: [
+
+ ],
+ defaultLayouts: [],
+ widgetButtons: false,
+ explicitSave: false
+ }
+
+ spyOn(LayoutStorage.prototype, 'load' );
+
+ storage = new LayoutStorage(options);
+
+ });
+
+ it('should add to storage.layouts', function() {
+ var newLayout = { title: 'my-layout' };
+ storage.add(newLayout);
+ expect(storage.layouts[0]).toEqual(newLayout);
+ });
+
+ it('should be able to take an array of new layouts', function() {
+ var newLayouts = [ { title: 'my-layout' }, { title: 'my-layout-2' } ];
+ storage.add(newLayouts);
+ expect(storage.layouts.length).toEqual(2);
+ expect(storage.layouts.indexOf(newLayouts[0])).not.toEqual(-1);
+ expect(storage.layouts.indexOf(newLayouts[1])).not.toEqual(-1);
+ });
+
+ it('should look for defaultWidgets on storage options if not supplied on layout definition', function() {
+ options.defaultWidgets = [{name: 'a'}, {name: 'b'}, {name: 'c'}];
+ storage = new LayoutStorage(options);
+
+ var newLayouts = [ { title: 'my-layout', defaultWidgets: [] }, { title: 'my-layout-2' } ];
+ storage.add(newLayouts);
+ expect(newLayouts[0].dashboard.defaultWidgets === newLayouts[0].defaultWidgets).toEqual(true);
+ expect(newLayouts[1].dashboard.defaultWidgets === options.defaultWidgets).toEqual(true);
+ });
+
+ it('should use defaultWidgets if supplied in the layout definition', function() {
+ options.defaultWidgets = [{name: 'a'}, {name: 'b'}, {name: 'c'}];
+ storage = new LayoutStorage(options);
+
+ var newLayouts = [ { title: 'my-layout', defaultWidgets: [] }, { title: 'my-layout-2' } ];
+ storage.add(newLayouts);
+ expect(newLayouts[0].dashboard.defaultWidgets).toEqual([]);
+ 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() {
+
+ var storage, options;
+
+ beforeEach(function() {
+ options = {
+ storageId: 'testingStorage',
+ storageHash: 'ds5f9d1f',
+ stringifyStorage: true,
+ widgetDefinitions: [
+ { name: 'A' },
+ { name: 'B' },
+ { name: 'C' }
+ ],
+ defaultLayouts: [
+ { title: '1' },
+ { title: '2', active: true },
+ { title: '3' }
+ ],
+ widgetButtons: false,
+ explicitSave: false
+ }
+
+ storage = new LayoutStorage(options);
+ });
+
+ it('should remove the supplied layout', function() {
+ var layout = storage.layouts[1];
+ storage.remove(layout);
+ expect(storage.layouts.indexOf(layout)).toEqual(-1);
+ });
+
+ it('should delete the state', function() {
+ var layout = storage.layouts[1];
+ storage.setItem(layout.id, {});
+ storage.remove(layout);
+ expect(storage.states[layout.id]).toBeUndefined();
+ });
+
+ it('should do nothing if layout is not in layouts', function() {
+ var layout = {};
+ var before = storage.layouts.length;
+ storage.remove(layout);
+ var after = storage.layouts.length;
+ expect(before).toEqual(after);
+ });
+
+ it('should set another dashboard to active if the layout removed was active', function() {
+ var layout = storage.layouts[1];
+ storage.remove(layout);
+ expect(storage.layouts[0].active || storage.layouts[1].active).toEqual(true);
+ });
+
+ it('should set the layout at index 0 to active if the removed layout was 0', function() {
+ storage.layouts[1].active = false;
+ storage.layouts[0].active = true;
+ storage.remove(storage.layouts[0]);
+ expect(storage.layouts[0].active).toEqual(true);
+ });
+
+ it('should not change the active layout if it was not the one that got removed', function() {
+ var active = storage.layouts[1];
+ var layout = storage.layouts[0];
+ storage.remove(layout);
+ expect(active.active).toEqual(true);
+ });
+
+ });
+
+ describe('the save method', function() {
+
+ var options, storage;
+
+ beforeEach(function() {
+ options = {
+ storageId: 'testingStorage',
+ storage: {
+ setItem: function(key, value) {
+
+ },
+ getItem: function(key) {
+
+ },
+ removeItem: function(key) {
+
+ }
+ },
+ storageHash: 'ds5f9d1f',
+ stringifyStorage: true,
+ widgetDefinitions: [
+
+ ],
+ defaultLayouts: [
+ {title: 'something'},
+ {title: 'something'},
+ {title: 'something'}
+ ],
+ widgetButtons: false,
+ explicitSave: false
+ }
+ storage = new LayoutStorage(options);
+ });
+
+ it('should call options.storage.setItem with a stringified object', function() {
+ spyOn(options.storage, 'setItem' );
+ storage.save();
+ expect(options.storage.setItem).toHaveBeenCalled();
+ expect(options.storage.setItem.calls.argsFor(0)[0]).toEqual(storage.id);
+ expect(typeof options.storage.setItem.calls.argsFor(0)[1]).toEqual('string');
+ expect(function(){
+ JSON.parse(options.storage.setItem.calls.argsFor(0)[1]);
+ }).not.toThrow();
+ });
+
+ it('should save an object that has layouts, states, and storageHash', function() {
+ spyOn(options.storage, 'setItem' );
+ storage.save();
+ var obj = JSON.parse(options.storage.setItem.calls.argsFor(0)[1]);
+ expect(obj.hasOwnProperty('layouts')).toEqual(true);
+ expect(obj.layouts instanceof Array).toEqual(true);
+ expect(obj.hasOwnProperty('states')).toEqual(true);
+ expect(typeof obj.states).toEqual('object');
+ expect(obj.hasOwnProperty('storageHash')).toEqual(true);
+ expect(typeof obj.storageHash).toEqual('string');
+ });
+
+ it('should call options.storage.setItem with an object when stringifyStorage is false', function() {
+ options.stringifyStorage = false;
+ storage = new LayoutStorage(options);
+ spyOn(options.storage, 'setItem' );
+ storage.save();
+ expect(options.storage.setItem).toHaveBeenCalled();
+ expect(options.storage.setItem.calls.argsFor(0)[0]).toEqual(storage.id);
+ expect(typeof options.storage.setItem.calls.argsFor(0)[1]).toEqual('object');
+ });
+
+ });
+
+ describe('the setItem method', function() {
+
+ var options, storage;
+
+ beforeEach(function() {
+ options = {
+ storageId: 'testingStorage',
+ storage: {
+ setItem: function(key, value) {
+
+ },
+ getItem: function(key) {
+
+ },
+ removeItem: function(key) {
+
+ }
+ },
+ storageHash: 'ds5f9d1f',
+ stringifyStorage: true,
+ widgetDefinitions: [
+
+ ],
+ defaultLayouts: [
+ {title: 'something'},
+ {title: 'something'},
+ {title: 'something'}
+ ],
+ widgetButtons: false,
+ explicitSave: false
+ }
+ storage = new LayoutStorage(options);
+ });
+
+ it('should set storage.states[id] to the second argument', function() {
+ var state = { some: 'thing'};
+ storage.setItem('id', state);
+ expect(storage.states.id).toEqual(state);
+ });
+
+ it('should call save', function() {
+ spyOn(storage, 'save');
+ var state = { some: 'thing'};
+ storage.setItem('id', state);
+ expect(storage.save).toHaveBeenCalled();
+ });
+
+ });
+
+ describe('the getItem method', function() {
+
+ var options, storage;
+
+ beforeEach(function() {
+ options = {
+ storageId: 'testingStorage',
+ storage: {
+ setItem: function(key, value) {
+
+ },
+ getItem: function(key) {
+
+ },
+ removeItem: function(key) {
+
+ }
+ },
+ storageHash: 'ds5f9d1f',
+ stringifyStorage: true,
+ widgetDefinitions: [
+
+ ],
+ defaultLayouts: [
+ {title: 'something'},
+ {title: 'something'},
+ {title: 'something'}
+ ],
+ widgetButtons: false,
+ explicitSave: false
+ }
+ storage = new LayoutStorage(options);
+ });
+
+ it('should return states[id]', function() {
+ storage.states['myId'] = {};
+ var result = storage.getItem('myId');
+ expect(result === storage.states['myId']).toEqual(true);
+ });
+
+ });
+
+ describe('the getActiveLayout method', function() {
+ var options, storage;
+
+ beforeEach(function() {
+ options = {
+ storageId: 'testingStorage',
+ storage: {
+ setItem: function(key, value) {
+
+ },
+ getItem: function(key) {
+
+ },
+ removeItem: function(key) {
+
+ }
+ },
+ storageHash: 'ds5f9d1f',
+ stringifyStorage: true,
+ widgetDefinitions: [
+
+ ],
+ defaultLayouts: [
+ {title: 'i am active', active: true},
+ {title: 'i am not'},
+ {title: 'me neither'}
+ ],
+ widgetButtons: false,
+ explicitSave: false
+ }
+ storage = new LayoutStorage(options);
+ });
+
+ it('should return the layout with active:true', function() {
+ var layout = storage.getActiveLayout();
+ expect(layout.title).toEqual('i am active');
+ });
+
+ it('should return false if no layout is active', function() {
+ var layout = storage.getActiveLayout();
+ layout.active = false;
+ var result = storage.getActiveLayout();
+ expect(result).toEqual(false);
+ });
+
+ });
+
+ describe('the removeItem', function() {
+
+ var options, storage;
+
+ beforeEach(function() {
+ options = {
+ storageId: 'testingStorage',
+ storage: {
+ setItem: function(key, value) {
+
+ },
+ getItem: function(key) {
+
+ },
+ removeItem: function(key) {
+
+ }
+ },
+ storageHash: 'ds5f9d1f',
+ stringifyStorage: true,
+ widgetDefinitions: [
+
+ ],
+ defaultLayouts: [
+ {title: 'i am active', active: true},
+ {title: 'i am not'},
+ {title: 'me neither'}
+ ],
+ widgetButtons: false,
+ explicitSave: false
+ }
+ storage = new LayoutStorage(options);
+ });
+
+ it('should remove states[id]', function() {
+ var state = {};
+ storage.setItem('1', state);
+ storage.removeItem('1');
+ expect(storage.states['1']).toBeUndefined();
+ });
+
+ it('should call save', function() {
+ spyOn(storage, 'save');
+ var state = {};
+ storage.setItem('1', state);
+ storage.removeItem('1');
+ expect(storage.save).toHaveBeenCalled();
+ });
+
+ });
+
+}); \ No newline at end of file
diff --git a/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/models/WidgetDataModel.js b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/models/WidgetDataModel.js
new file mode 100644
index 00000000..547f2e96
--- /dev/null
+++ b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/models/WidgetDataModel.js
@@ -0,0 +1,45 @@
+/*
+ * 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('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;
+ }); \ No newline at end of file
diff --git a/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/models/WidgetDefCollection.js b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/models/WidgetDefCollection.js
new file mode 100644
index 00000000..27765440
--- /dev/null
+++ b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/models/WidgetDefCollection.js
@@ -0,0 +1,56 @@
+/*
+ * 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('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
+ var map = {};
+ _.each(widgetDefs, function (widgetDef) {
+ map[widgetDef.name] = widgetDef;
+ });
+ this.map = map;
+ }
+
+ WidgetDefCollection.prototype = Object.create(Array.prototype);
+
+ WidgetDefCollection.prototype.getByName = function (name) {
+ return this.map[name];
+ };
+
+ WidgetDefCollection.prototype.add = function(def) {
+ def = convertToDefinition(def);
+ this.push(def);
+ this.map[def.name] = def;
+ };
+
+ return WidgetDefCollection;
+ });
diff --git a/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/models/WidgetModel.js b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/models/WidgetModel.js
new file mode 100644
index 00000000..c378d3b6
--- /dev/null
+++ b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/models/WidgetModel.js
@@ -0,0 +1,112 @@
+/*
+ * 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('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(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);
+ }
+ }
+
+ 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);
+
+ // check with min width if set, unit refer to width's unit
+ if (this.size && _.has(this.size, 'minWidth')) {
+ width = _.max([parseFloat(this.size.minWidth), 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;
+ },
+
+ setHeight: function (height) {
+ this.contentStyle.height = height;
+ this.updateSize(this.contentStyle);
+ },
+
+ setStyle: function (style) {
+ this.style = style;
+ this.updateContainerStyle(style);
+ },
+
+ updateSize: function (size) {
+ angular.extend(this.size, size);
+ },
+
+ updateContainerStyle: function (style) {
+ angular.extend(this.containerStyle, style);
+ },
+ serialize: function() {
+ return _.pick(this, ['title', 'name', 'report_id', 'hideGrid', 'showChart' ,'rcloud_url','reportData','style', 'size', 'dataModelOptions', 'attrs', 'storageHash']);
+ }
+ };
+
+ return WidgetModel;
+ }); \ No newline at end of file
diff --git a/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/models/WidgetModel.spec.js b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/models/WidgetModel.spec.js
new file mode 100644
index 00000000..151e560a
--- /dev/null
+++ b/ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components/models/WidgetModel.spec.js
@@ -0,0 +1,156 @@
+'use strict';
+
+describe('Factory: WidgetModel', function () {
+
+ // load the service's module
+ beforeEach(module('ui.dashboard'));
+
+ // instantiate service
+ var WidgetModel;
+ beforeEach(inject(function (_WidgetModel_) {
+ WidgetModel = _WidgetModel_;
+ }));
+
+ it('should be a function', function() {
+ expect(typeof WidgetModel).toEqual('function');
+ });
+
+ describe('the constructor', function() {
+ var m, Class, Class2, overrides;
+
+ beforeEach(function() {
+ Class = {
+ name: 'TestWidget',
+ attrs: {},
+ dataAttrName: 'attr-name',
+ dataModelType: function TestType() {},
+ dataModelOptions: {},
+ style: { width: '10em' },
+ settingsModalOptions: {},
+ onSettingsClose: function() {},
+ onSettingsDismiss: function() {},
+ funkyChicken: {
+ cool: false,
+ fun: true
+ }
+ };
+
+ Class2 = {
+ name: 'TestWidget2',
+ attrs: {},
+ dataAttrName: 'attr-name',
+ dataModelType: function TestType() {},
+ dataModelOptions: {},
+ style: { width: '10em' },
+ templateUrl: 'my/url.html',
+ template: '<div>some template</div>'
+ };
+
+ overrides = {
+ style: {
+ width: '15em'
+ }
+ };
+ spyOn(WidgetModel.prototype, 'setWidth');
+ m = new WidgetModel(Class, overrides);
+ });
+
+ it('should copy class defaults, so that changes on an instance do not change the Class', function() {
+ m.style.width = '20em';
+ expect(Class.style.width).toEqual('10em');
+ });
+
+ it('should call setWidth', function() {
+ expect(WidgetModel.prototype.setWidth).toHaveBeenCalled();
+ });
+
+ it('should take overrides as precedent over Class defaults', function() {
+ expect(m.style.width).toEqual('15em');
+ });
+
+ 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 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 set template if and only if it is present on Class', function() {
+ delete Class2.templateUrl;
+ var m2 = new WidgetModel(Class2, overrides);
+ expect(m2.template).toEqual('<div>some template</div>');
+ });
+
+ it('should look for directive if neither templateUrl nor template is found on Class', function() {
+ delete Class2.templateUrl;
+ delete Class2.template;
+ Class2.directive = 'ng-bind';
+ var m2 = new WidgetModel(Class2, overrides);
+ expect(m2.directive).toEqual('ng-bind');
+ });
+
+ it('should set the name as directive if templateUrl, template, and directive are not defined', function() {
+ delete Class2.templateUrl;
+ delete Class2.template;
+ var m2 = new WidgetModel(Class2, overrides);
+ expect(m2.directive).toEqual('TestWidget2');
+ });
+
+ it('should not require overrides', function() {
+ var fn = function() {
+ var m2 = new WidgetModel(Class);
+ }
+ expect(fn).not.toThrow();
+ });
+
+ it('should copy references to settingsModalOptions, onSettingsClose, onSettingsDismiss', function() {
+ var m = new WidgetModel(Class);
+ expect(m.settingsModalOptions).toEqual(Class.settingsModalOptions);
+ expect(m.onSettingsClose).toEqual(Class.onSettingsClose);
+ expect(m.onSettingsDismiss).toEqual(Class.onSettingsDismiss);
+ });
+
+ });
+
+ describe('setWidth method', function() {
+
+ var context, setWidth;
+
+ beforeEach(function() {
+ context = new WidgetModel({});
+ setWidth = WidgetModel.prototype.setWidth;
+ });
+
+ it('should take one argument as a string with units', function() {
+ setWidth.call(context, '100px');
+ expect(context.containerStyle.width).toEqual('100px');
+ });
+
+ it('should take two args as a number and string as units', function() {
+ setWidth.call(context, 100, 'px');
+ expect(context.containerStyle.width).toEqual('100px');
+ });
+
+ it('should return false and not set anything if width is less than 0', function() {
+ var result = setWidth.call(context, -100, 'em');
+ expect(result).toEqual(false);
+ expect(context.containerStyle.width).not.toEqual('-100em');
+ });
+
+ it('should assume % if no unit is given', function() {
+ setWidth.call(context, 50);
+ expect(context.containerStyle.width).toEqual('50%');
+ });
+
+ it('should force greater than 0% and less than or equal 100%', function() {
+ setWidth.call(context, '110%');
+ expect(context.containerStyle.width).toEqual('100%');
+ });
+
+ });
+
+}); \ No newline at end of file