diff options
Diffstat (limited to 'ecomp-sdk/epsdk-app-overlay/src/main/webapp/app/fusion/scripts/view-models/reportdashboard-page/src/components')
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()">×</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()">×</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()">×</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 |