diff options
Diffstat (limited to 'catalog-ui/app/scripts/directives')
121 files changed, 10886 insertions, 0 deletions
diff --git a/catalog-ui/app/scripts/directives/clicked-outside/clicked-outside-directive.ts b/catalog-ui/app/scripts/directives/clicked-outside/clicked-outside-directive.ts new file mode 100644 index 0000000000..1b0af4ef99 --- /dev/null +++ b/catalog-ui/app/scripts/directives/clicked-outside/clicked-outside-directive.ts @@ -0,0 +1,131 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ +/// <reference path="../../references"/> +module Sdc.Directives { + + class ClickedOutsideModel{ + + private clickedOutsideContainerSelector: string; + private onClickedOutsideGetter: Function; + private clickedOutsideEnableGetter: Function; + + constructor(clickedOutsideData: any) { + this.clickedOutsideContainerSelector = clickedOutsideData.clickedOutsideContainerSelector; + this.onClickedOutsideGetter = clickedOutsideData.onClickedOutsideGetter; + this.clickedOutsideEnableGetter = clickedOutsideData.clickedOutsideEnableGetter; + } + + public getClickedOutsideContainerSelector = (): string => { + return this.clickedOutsideContainerSelector; + } + + public getOnClickedOutsideGetter = (): Function => { + return this.onClickedOutsideGetter; + } + + public getClickedOutsideEnableGetter = (): Function => { + return this.clickedOutsideEnableGetter; + } + } + + export interface IClickedOutsideDirectiveScope extends ng.IScope{} + + export class ClickedOutsideDirective implements ng.IDirective { + + constructor(private $document: JQuery, private $parse: ng.IParseService) {} + + restrict = 'A'; + + link = (scope:IClickedOutsideDirectiveScope, element: JQuery, attrs) => { + + let container: HTMLElement; + let attrsAfterEval = scope.$eval(attrs.clickedOutside); + attrsAfterEval.onClickedOutsideGetter = this.$parse(attrsAfterEval.onClickedOutside); + attrsAfterEval.clickedOutsideEnableGetter = this.$parse(attrsAfterEval.clickedOutsideEnable); + + let clickedOutsideModel: ClickedOutsideModel = new ClickedOutsideModel(attrsAfterEval); + + + let getContainer: Function = ():HTMLElement => { + if(!container){ + let clickedOutsideContainerSelector: string = clickedOutsideModel.getClickedOutsideContainerSelector(); + if(!angular.isUndefined(clickedOutsideContainerSelector) && clickedOutsideContainerSelector !== ''){ + container = element.parents(clickedOutsideContainerSelector+':first')[0]; + if(!container){ + container = element[0]; + } + }else{ + container = element[0]; + } + } + return container; + }; + + + let onClickedOutside = (event: JQueryEventObject) => { + let containerDomElement: HTMLElement = getContainer(); + let targetDomElementJq: JQuery = angular.element(event.target); + if(targetDomElementJq.hasClass('tooltip') || targetDomElementJq.parents('.tooltip:first').length){ + return; + } + let targetDomElement: HTMLElement = targetDomElementJq[0]; + if (!containerDomElement.contains(targetDomElement)){ + scope.$apply(() => { + let onClickedOutsideGetter:Function = clickedOutsideModel.getOnClickedOutsideGetter(); + onClickedOutsideGetter(scope); + }); + } + }; + + let attachDomEvents: Function = () => { + this.$document.on('mousedown', onClickedOutside); + }; + + let detachDomEvents: Function = () => { + this.$document.off('mousedown', onClickedOutside); + }; + + // + scope.$on('$destroy', () => { + detachDomEvents(); + }); + + + scope.$watch(() => { + let clickedOutsideEnableGetter: Function = clickedOutsideModel.getClickedOutsideEnableGetter(); + return clickedOutsideEnableGetter(scope); + }, (newValue: boolean) => { + if(newValue){ + attachDomEvents(); + return; + } + detachDomEvents(); + }); + + + } + + public static factory = ($document: JQuery, $parse: ng.IParseService) => { + return new ClickedOutsideDirective($document, $parse); + } + } + + ClickedOutsideDirective.factory.$inject = ['$document', '$parse']; +} diff --git a/catalog-ui/app/scripts/directives/custom-validation/custom-validation.ts b/catalog-ui/app/scripts/directives/custom-validation/custom-validation.ts new file mode 100644 index 0000000000..e2f831ed53 --- /dev/null +++ b/catalog-ui/app/scripts/directives/custom-validation/custom-validation.ts @@ -0,0 +1,55 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ +/// <reference path="../../references"/> +module Sdc.Directives { + 'use strict'; + + + export interface ICustomValidationScope extends ng.IScope { + validationFunc: Function; + } + + export class CustomValidationDirective implements ng.IDirective { + + constructor() {} + + require = 'ngModel'; + restrict = 'A'; + + scope = { + validationFunc: '=' + }; + + link = (scope:ICustomValidationScope, elem, attrs, ngModel) => { + + ngModel.$validators.customValidation = (modelValue, viewValue) :boolean => { + return scope.validationFunc(viewValue); + }; + + }; + + public static factory = ()=> { + return new CustomValidationDirective(); + }; + + } + + CustomValidationDirective.factory.$inject = []; +} diff --git a/catalog-ui/app/scripts/directives/download-artifact/download-artifact.ts b/catalog-ui/app/scripts/directives/download-artifact/download-artifact.ts new file mode 100644 index 0000000000..49bf14618c --- /dev/null +++ b/catalog-ui/app/scripts/directives/download-artifact/download-artifact.ts @@ -0,0 +1,141 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ +/// <reference path="../../references"/> +module Sdc.Directives { + 'use strict'; + + export class DOWNLOAD_CSS_CLASSES { + static DOWNLOAD_ICON = "table-download-btn tosca"; + static LOADER_ICON = "tlv-loader small loader"; + } + + export interface IDownloadArtifactScope extends ng.IScope { + $window:any; + artifact: Models.ArtifactModel; + component: Models.Components.Component; + instance:boolean; + download: Function; + showLoader:boolean; + updateDownloadIcon:Function; + } + + export class DownloadArtifactDirective implements ng.IDirective { + + constructor(private $window:any,private cacheService:Services.CacheService, private EventListenerService:Services.EventListenerService, private fileUtils:Sdc.Utils.FileUtils) {} + + scope = { + artifact: '=', + component: '=', + instance:'=', + showLoader:'=' + }; + restrict = 'EA'; + + link = (scope:IDownloadArtifactScope, element:any) => { + scope.$window = this.$window; + + element.on("click", function() { + scope.download(scope.artifact); + }); + + + let initDownloadLoader = ()=>{ + //if the artifact is in a middle of download progress register form callBack & change icon from download to loader + if(scope.showLoader && this.cacheService.get(scope.artifact.uniqueId)){ + this.EventListenerService.registerObserverCallback(Utils.Constants.EVENTS.DOWNLOAD_ARTIFACT_FINISH_EVENT + scope.artifact.uniqueId, scope.updateDownloadIcon); + window.setTimeout(():void => { + if(this.cacheService.get(scope.artifact.uniqueId)){ + element[0].className = DOWNLOAD_CSS_CLASSES.LOADER_ICON; + } + },1000); + + } + }; + + let setDownloadedFileLoader = ()=> { + if(scope.showLoader){ + //set in cache service thet the artifact is in download progress + this.cacheService.set(scope.artifact.uniqueId,true); + initDownloadLoader(); + } + }; + + let removeDownloadedFileLoader = ()=> { + if (scope.showLoader) { + this.cacheService.set(scope.artifact.uniqueId, false); + this.EventListenerService.notifyObservers(Utils.Constants.EVENTS.DOWNLOAD_ARTIFACT_FINISH_EVENT + scope.artifact.uniqueId); + } + }; + + + //replace the loader to download icon + scope.updateDownloadIcon = () =>{ + element[0].className = DOWNLOAD_CSS_CLASSES.DOWNLOAD_ICON; + }; + + + initDownloadLoader(); + + scope.download = (artifact:Models.ArtifactModel):void => { + + let onFaild = (response):void => { + console.info('onFaild', response); + removeDownloadedFileLoader(); + }; + + let onSuccess = (data:Models.IFileDownload):void => { + downloadFile(data); + removeDownloadedFileLoader(); + }; + + setDownloadedFileLoader(); + + if(scope.instance){ + scope.component.downloadInstanceArtifact(artifact.uniqueId).then(onSuccess, onFaild); + }else { + scope.component.downloadArtifact(artifact.uniqueId).then(onSuccess, onFaild); + } + }; + + let downloadFile = (file:Models.IFileDownload):void => { + if (file){ + let blob = this.fileUtils.base64toBlob(file.base64Contents,''); + let fileName = file.artifactName; + this.fileUtils.downloadFile(blob, fileName); + } + }; + + element.on('$destroy', ()=>{ + //remove listener of download event + if(scope.artifact && scope.artifact.uniqueId){ + this.EventListenerService.unRegisterObserver(Utils.Constants.EVENTS.DOWNLOAD_ARTIFACT_FINISH_EVENT + scope.artifact.uniqueId); + } + }); + + }; + + public static factory = ($window:any,cacheService:Sdc.Services.CacheService,EventListenerService:Services.EventListenerService, fileUtils:Sdc.Utils.FileUtils)=> { + return new DownloadArtifactDirective($window,cacheService,EventListenerService, fileUtils); + }; + + } + + DownloadArtifactDirective.factory.$inject = ['$window', 'Sdc.Services.CacheService', 'EventListenerService', 'FileUtils']; +} diff --git a/catalog-ui/app/scripts/directives/ecomp-header/ecomp-header.html b/catalog-ui/app/scripts/directives/ecomp-header/ecomp-header.html new file mode 100644 index 0000000000..e86f9df8b0 --- /dev/null +++ b/catalog-ui/app/scripts/directives/ecomp-header/ecomp-header.html @@ -0,0 +1,73 @@ +<div class="sdc-ecomp-header-wrapper"> + + <div class="sdc-ecomp-header"> + + <div class="sdc-ecomp-logo-wrapper"> + <a class="sdc-ecomp-header-title" data-ng-if="clickableLogo==='true'" data-ui-sref="dashboard"><span class="sdc-ecomp-logo"></span>ASDC</a> + <a class="sdc-ecomp-header-title" data-ng-if="clickableLogo==='false'"><span class="sdc-ecomp-logo"></span>ASDC</a> + <div class="sdc-ecomp-header-version"> v.{{version}}</div> + </div> + + <div class="sdc-ecomp-menu"> + + <!-- Top level menu --> + <ul class="sdc-ecomp-menu-top-level"> + <li class="sdc-ecomp-menu-top-level-item" + ng-repeat="item in megaMenuDataObject" + data-ng-if="item.children && item.children.length>0" + > + <span class="selected" data-ng-if="item.menuId === selectedTopMenu.menuId"></span> + <a href data-ng-click="firstMenuLevelClick(item.menuId)">{{item.text}}</a> + + <!-- Second level menu --> + <div class="sdc-ecomp-menu-second-level" data-ng-if="item.menuId === selectedTopMenu.menuId" data-ng-mouseleave="subMenuLeaveAction(item.menuId)"> + <ul> + <li class="sdc-ecomp-menu-second-level-item" + ng-repeat="subItem in selectedTopMenu.children | orderBy : 'column'" + aria-label="{{subItem.text}}" + data-ng-class="{'sdc-ecomp-menu-item-hover': menuItemHover===true}" + ng-mouseover="subMenuEnterAction(subItem.menuId)" + ng-mouseenter="menuItemHover=true" + ng-mouseleave="menuItemHover=false"> + + <!--<i ng-if="subItem.text=='Favorites'" id="favorite-star" class="icon-star favorites-icon-active"></i>--> + + <a href title="{{subItem.text}}" data-ng-click="memuItemClick(subItem)">{{subItem.text}}</a> + + <!-- Third and Four menu panel --> + <ul class="sdc-ecomp-menu-third-level" data-ng-if="subItem.menuId === selectedSubMenu.menuId && (selectedSubMenu.children && selectedSubMenu.children.length>0)"> + <li class="sdc-ecomp-menu-third-level-item" + ng-repeat="thirdItem in selectedSubMenu.children | orderBy : 'column'" + aria-label="{{thirdItem.text}}"> + <a class="sdc-ecomp-menu-third-level-title" href title="{{thirdItem.text}}" data-ng-click="memuItemClick(thirdItem)">{{thirdItem.text}}</a> + <span class="sdc-ecomp-menu-four-level-seperator"></span> + <ul class="sdc-ecomp-menu-four-level"> + <li class="sdc-ecomp-menu-four-level-item" data-ng-repeat="fourItem in thirdItem.children | orderBy : 'column'"> + <a href title="{{fourItem.text}}" data-ng-click="memuItemClick(fourItem)">{{fourItem.text}}</a> + </li> + </ul> + </li> + </ul> + + </li> + </ul> + </div> + + </li> + + </ul> + + </div> + <div class="sdc-ecomp-user-wrapper"> + <span class="sdc-ecomp-user-icon"></span> + <div class="sdc-ecomp-user-details"> + <div class="sdc-ecomp-user-details-name-role"> + <div sdc-smart-tooltip class="sdc-ecomp-user-details-name" data-ng-bind="user.getName()"></div> + <div class="sdc-ecomp-user-details-role" data-ng-bind="user.getRoleToView()"></div> + </div> + <div class="sdc-ecomp-user-details-last-login" data-ng-show="user.getLastLogin()!==''">Last Login: {{user.getLastLogin() | date: 'MMM dd hh:mm a' : 'UTC'}} UTC</div> + </div> + </div> + </div> + +</div> diff --git a/catalog-ui/app/scripts/directives/ecomp-header/ecomp-header.less b/catalog-ui/app/scripts/directives/ecomp-header/ecomp-header.less new file mode 100644 index 0000000000..a6f7e9b5a2 --- /dev/null +++ b/catalog-ui/app/scripts/directives/ecomp-header/ecomp-header.less @@ -0,0 +1,296 @@ +@first-level-color: #067ab4; +@second-level-color: #f8f8f8; + +@first-level-height: 60px; +@second-level-height: 60px; +@third-four-level-height: 370px; + +@max-item-width: 250px; + +.sdc-ecomp-header-wrapper { + + position: absolute; + top: 0; + right: 0; + left: 0; + z-index: 999; + + ul { + margin: 0; + padding: 0; + } + + .sdc-ecomp-header { + + background-color: @first-level-color; + height: @first-level-height; + line-height: @first-level-height; + vertical-align: middle; + display: flex; + flex-direction: row; + padding: 0 20px; + + .sdc-ecomp-menu { + flex-grow: 98; + } + + } + + /* LEVEL 1 */ + .sdc-ecomp-menu-top-level { + list-style: none; + + .sdc-ecomp-menu-top-level-item:first-child { + margin-left: 0; + } + + .sdc-ecomp-menu-top-level-item { + display: inline-block; + margin: 0 20px; + position: relative; + a { + .p_14_m; + text-decoration: none; + } + + span { + &.selected { + position: absolute; + bottom: 0; + width: 0; + height: 0; + border-left: 15px solid transparent; + border-right: 15px solid transparent; + border-bottom: 15px solid @second-level-color; + } + } + } + } + + /* LEVEL 2 */ + .sdc-ecomp-menu-second-level { + background-color: @second-level-color; + box-shadow: 0px 1px 0px 0px rgba(0, 0, 0, 0.33); + height: @second-level-height; + list-style: none; + line-height: @second-level-height; + vertical-align: middle; + display: flex; + flex-direction: row; + padding: 0 20px; + position: fixed; + left: 0; + right: 0; + + .sdc-ecomp-menu-second-level-item:first-child { + margin-left: 0; + } + + .sdc-ecomp-menu-second-level-item { + display: inline-block; + height: @second-level-height; + line-height: @second-level-height; + + &.sdc-ecomp-menu-item-hover { + border-bottom: 4px solid #067ab4; + } + + a { + .m_14_r; + text-decoration: none; + text-align:center; + padding: 0 20px; + display: block; + + &:hover { + .s_14_r; + font-weight:bold; + } + + &:active { + font-weight: bold; + } + + /* fix jump when hovering text */ + &:after { + display:block; + content:attr(title); + font-weight:bold; + height:1px; + color:transparent; + overflow:hidden; + visibility:hidden; + } + } + + } + + } + + /* LEVEL 3 */ + ul.sdc-ecomp-menu-third-level { + background-color: #ffffff; + box-shadow: 0px 0px 0px 1px rgba(0, 0, 0, 0.1); + min-height: @third-four-level-height; + list-style: none; + + display: flex; + flex-direction: row; + /*flex-wrap: wrap;*/ + position: fixed; + top: @first-level-height + @second-level-height; + left: 0; + right: 0; + padding: 0 40px; + + li.sdc-ecomp-menu-third-level-item { + height: 40px; + line-height: 40px; + position: relative; + /*width: @max-item-width;*/ + max-width: @max-item-width; + + .sdc-ecomp-menu-third-level-title { + .m_14_m; + text-decoration: none; + text-align: left; + display: block; + padding: 0 20px; + line-height: 20px; + margin-top: 20px; + font-weight: bold; + cursor: pointer; + } + + .sdc-ecomp-menu-four-level-seperator { + position: absolute; + border-right: solid 1px #e5e5e5; + height: @third-four-level-height - 20; + top: 10px; + } + } + + li.sdc-ecomp-menu-third-level-item:first-child { + .sdc-ecomp-menu-four-level-seperator { + border: none; + } + } + } + + /* LEVEL 4 */ + ul.sdc-ecomp-menu-four-level { + display: flex; + flex-direction: column; + margin-top: 10px; + + li.sdc-ecomp-menu-four-level-item { + display: block; + /*width: @max-item-width;*/ + max-width: @max-item-width; + line-height: 20px; + + a { + .m_14_r; + text-decoration: none; + text-align: left; + display: block; + + &:hover { + .s_14_r; + font-weight: bold; + } + } + } + } + +} + +.sdc-ecomp-logo-wrapper { + flex-grow: 1; + .p_18_m; + white-space: nowrap; + min-width: 210px; + text-align: left; + position: relative; + + .sdc-ecomp-logo { + background-image: url("images/att_logo_white.png"); + background-repeat: no-repeat; + display: inline-block; + vertical-align: middle; + width: 55px; + height: 55px; + } + + .sdc-ecomp-header-version { + .c_16; + .opacity(0.8); + position: absolute; + top: 34px; + line-height: 20px; + left: 56px; + } + + a.sdc-ecomp-header-title { + .p_24; + text-decoration: none; + } +} + + +.sdc-ecomp-user-wrapper { + + flex-grow: 1; + .p_14_m; + white-space: nowrap; + display: flex; + flex-direction: row; + align-items: center; + height: @first-level-height; + + .sdc-ecomp-user-icon { + margin-right: 20px; + .tlv-sprite; + .tlv-sprite.user; + } + + .sdc-ecomp-user-details { + display: flex; + flex-direction: column; + } + + .sdc-ecomp-user-details-name-role { + line-height: 20px; + + .sdc-ecomp-user-details-name { + max-width: 160px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + vertical-align: bottom; + + .bold; + display: inline-block; + } + + .sdc-ecomp-user-details-role { + .bold; + display: inline-block; + margin-left: 6px; + + &:before { + content: ''; + margin-right: 8px; + border-left: 1px solid @color_m; + } + } + } + + .sdc-ecomp-user-details-last-login { + .font-type._3; + display: block; + line-height: 20px; + height: 20px; + } + +} diff --git a/catalog-ui/app/scripts/directives/ecomp-header/ecomp-header.ts b/catalog-ui/app/scripts/directives/ecomp-header/ecomp-header.ts new file mode 100644 index 0000000000..7102c810ba --- /dev/null +++ b/catalog-ui/app/scripts/directives/ecomp-header/ecomp-header.ts @@ -0,0 +1,235 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ +/// <reference path="../../references"/> +module Sdc.Directives { + 'use strict'; + + export class MenuItem { + menuId:number; + column:number; + text:string; + parentMenuId:number; + url:string; + children:Array<MenuItem> + } + + export interface IEcompHeaderDirectiveScope extends ng.IScope { + menuData:Array<MenuItem>; + version:string; + clickableLogo:string; + contactUsUrl:string; + getAccessUrl:string; + megaMenuDataObjectTemp:Array<any>; + megaMenuDataObject:Array<any>; + + selectedTopMenu:MenuItem; + selectedSubMenu:MenuItem; + + firstMenuLevelClick:Function; + subMenuEnterAction:Function; + subMenuLeaveAction:Function; + + memuItemClick:Function; + user: Models.IUser; + } + + export class EcompHeaderDirective implements ng.IDirective { + + constructor(private $templateCache:ng.ITemplateCacheService, + private $http:ng.IHttpService, + private sdcConfig:Models.IAppConfigurtaion, + private UserResourceClass:Services.IUserResourceClass) { + + } + + scope = { + menuData: '=', + version: '@', + clickableLogo: '@?' + }; + + public replace = true; + public restrict = 'E'; + public controller = EcompHeaderController; + + template = ():string => { + return this.$templateCache.get('/app/scripts/directives/ecomp-header/ecomp-header.html'); + }; + + link = ($scope:IEcompHeaderDirectiveScope, $elem:JQuery, attr:any) => { + + if (!$scope.clickableLogo){ + $scope.clickableLogo="true"; + } + + let findMenuItemById = (menuId):MenuItem => { + let selectedMenuItem:MenuItem = _.find($scope.menuData, (item:MenuItem)=>{ + if (item.menuId === menuId){ + return item; + } + }); + return selectedMenuItem; + }; + + let initUser = ():void => { + let defaultUserId:string; + let user:Services.IUserResource = this.UserResourceClass.getLoggedinUser(); + if (!user) { + defaultUserId = this.$http.defaults.headers.common[this.sdcConfig.cookie.userIdSuffix]; + user = this.UserResourceClass.get({id: defaultUserId}, ():void => { + $scope.user = new Models.User(user); + }); + } else { + $scope.user = new Models.User(user); + } + }; + + $scope.firstMenuLevelClick = (menuId:number):void => { + let selectedMenuItem:MenuItem = _.find($scope.megaMenuDataObjectTemp, (item:MenuItem)=>{ + if (item.menuId === menuId){ + return item; + } + }); + if (selectedMenuItem) { + $scope.selectedTopMenu = selectedMenuItem; + //console.log("Selected menu item: " + selectedMenuItem.text); + } + }; + + $scope.subMenuEnterAction = (menuId:number):void => { + $scope.selectedSubMenu = findMenuItemById(menuId); + }; + + $scope.subMenuLeaveAction = (menuId:number):void => { + $scope.selectedTopMenu = undefined; + }; + + $scope.memuItemClick = (menuItem:MenuItem):void => { + if (menuItem.url){ + window.location.href=menuItem.url; + } else { + console.log("Menu item: " + menuItem.text + " does not have defined URL!"); + } + }; + + initUser(); + + }; + + public static factory = ($templateCache:ng.ITemplateCacheService, + $http:ng.IHttpService, + sdcConfig:Models.IAppConfigurtaion, + UserResourceClass:Services.IUserResourceClass)=> { + return new EcompHeaderDirective($templateCache, $http, sdcConfig, UserResourceClass); + }; + + } + + export class EcompHeaderController { + + messages:any; + getAttachId:Function; + render:any; + reRender:Function; + register:Function; + deregister:Function; + head:any; + + static '$inject' = [ + '$element', + '$scope', + '$attrs', + '$animate' + ]; + + constructor(private $element:JQuery, + private $scope:IEcompHeaderDirectiveScope, + private $attrs:ng.IAttributes, + private $animate:any) { + + this.$scope = $scope; + + this.$scope.$watch('menuData', (newVal, oldVal) => { + if (newVal){ + this.init(); + } + }); + + } + + init = ():void => { + + this.$scope.contactUsUrl = "https://wiki.web.att.com/display/EcompPortal/ECOMP+Portal+Home"; + this.$scope.getAccessUrl = "http://ecomp-tlv-dev2.uccentral.att.com:8080/ecompportal/get_access"; + + let unflatten = ( array, parent?, tree? ) => { + tree = typeof tree !== 'undefined' ? tree : []; + parent = typeof parent !== 'undefined' ? parent : { menuId: null }; + let children = _.filter( array, function(child){ return child["parentMenuId"] == parent.menuId; }); + if( !_.isEmpty( children ) ){ + if( parent.menuId === null ){ + tree = children; + }else{ + parent['children'] = children + } + _.each( children, function( child ){ unflatten( array, child ) } ); + } + return tree; + }; + + let menuStructureConvert = (menuItems) => { + console.log(menuItems); + this.$scope.megaMenuDataObjectTemp = [ + { + menuId: 1001, + text: "ECOMP", + children: menuItems + }, + { + menuId: 1002, + text: "Help", + children: [ + { + text:"Contact Us", + url: this.$scope.contactUsUrl + }] + } + ]; + + /*{ + text:"Get Access", + url: this.$scope.getAccessUrl + }*/ + return this.$scope.megaMenuDataObjectTemp; + }; + + let a = unflatten(this.$scope.menuData); + this.$scope.megaMenuDataObject = menuStructureConvert(a); + //console.log(this.$scope.megaMenuDataObject); + }; + } + + EcompHeaderDirective.factory.$inject = ['$templateCache', '$http', 'sdcConfig', 'Sdc.Services.UserResourceService']; + +} + + + + diff --git a/catalog-ui/app/scripts/directives/edit-name-popover/edit-module-name-popover.html b/catalog-ui/app/scripts/directives/edit-name-popover/edit-module-name-popover.html new file mode 100644 index 0000000000..d90c52d9a6 --- /dev/null +++ b/catalog-ui/app/scripts/directives/edit-name-popover/edit-module-name-popover.html @@ -0,0 +1,31 @@ +<div> + <form name="popoverForm" class="w-sdc-form" data-tests-id="popover-form"> + <span class="tlv-sprite tlv-x-btn close-popover-btn" data-tests-id="popover-x-button" ng-click="closePopover()"></span> + <div class="form-group"> + <span class="popover-label" data-tests-id="popover-vfinstance-name" tooltips tooltip-content="{{module.vfInstanceName}}">{{module.vfInstanceName}}</span> + <div class="popover-input"> + <div class="i-sdc-form-item" data-ng-class="{'error': validateField(popoverForm.heatName)}"> + <input type="text" + data-ng-model="module.heatName" + data-ng-maxlength="50" + maxlength="50" + name="heatName" + placeholder="Enter Name" + autocomplete="off" + data-ng-init="onInit()" + data-ng-pattern="heatNameValidationPattern" + data-tests-id="popover-heat-name" + class="form-control" + /> + </div> + <div class="input-error" data-ng-show="validateField(popoverForm.heatName)"> + <span ng-show="popoverForm.heatName.$error.maxlength" translate="VALIDATION_ERROR_MAX_LENGTH" translate-values="{'max': '50' }"></span> + <span ng-show="popoverForm.heatName.$error.pattern" translate="VALIDATION_ERROR_SPECIAL_CHARS_NOT_ALLOWED"></span> + </div> + </div> + <span class="popover-label" data-tests-id="popover-module-name" tooltips tooltip-content="{{module.moduleName}}">{{module.moduleName}}</span> + <button class="tlv-btn grey popover-btn" data-tests-id="popover-close-button" data-ng-click="closePopover()">Cancel</button> + <button class="tlv-btn blue popover-btn" data-tests-id="popover-save-button" data-ng-class="{'disabled': (validateField(popoverForm.heatName) || popoverForm.heatName.$viewValue === originalName)}" data-ng-click="updateHeatName()">Save</button> + </div> + </form> +</div> diff --git a/catalog-ui/app/scripts/directives/edit-name-popover/edit-name-popover-directive.ts b/catalog-ui/app/scripts/directives/edit-name-popover/edit-name-popover-directive.ts new file mode 100644 index 0000000000..a033df054b --- /dev/null +++ b/catalog-ui/app/scripts/directives/edit-name-popover/edit-name-popover-directive.ts @@ -0,0 +1,98 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ +/// <reference path="../../references"/> +module Sdc.Directives { + 'use strict'; + + export interface IEditNamePopoverDirectiveScope extends ng.IScope { + isOpen: boolean; + templateUrl: string; + module: any; + direction: string; + header: string; + heatNameValidationPattern:RegExp; + originalName:string; + onSave:any; + + closePopover(isCancel:boolean):void; + validateField(field:any, originalName:string):boolean; + updateHeatName(heatName:string):void; + onInit():void; + } + + export class EditNamePopoverDirective implements ng.IDirective { + + constructor(private $templateCache:ng.ITemplateCacheService, private ValidationPattern:RegExp) { + } + + scope = { + direction: "@?", + module: "=", + header: "@?", + onSave: "&" + }; + + link = (scope:IEditNamePopoverDirectiveScope) => { + if(!scope.direction) { + scope.direction = 'top'; + } + + scope.originalName = ''; + scope.templateUrl = "/app/scripts/directives/edit-name-popover/edit-module-name-popover.html"; + scope.isOpen = false; + + scope.closePopover = (isCancel:boolean = true) => { + scope.isOpen = !scope.isOpen; + + if(isCancel) { + scope.module.heatName = scope.originalName; + } + }; + + scope.onInit = () => { + scope.originalName = scope.module.heatName; + }; + + scope.validateField = (field:any):boolean => { + return !!(field && field.$dirty && field.$invalid); + }; + + scope.heatNameValidationPattern = this.ValidationPattern; + + scope.updateHeatName = () => { + scope.closePopover(false); + scope.onSave(); + } + + }; + + replace = true; + restrict = 'E'; + template = ():string => { + return this.$templateCache.get('/app/scripts/directives/edit-name-popover/edit-name-popover-view.html'); + }; + + public static factory = ($templateCache:ng.ITemplateCacheService, ValidationPattern:RegExp)=> { + return new EditNamePopoverDirective($templateCache, ValidationPattern); + } + } + + EditNamePopoverDirective.factory.$inject = ['$templateCache', 'ValidationPattern']; +} diff --git a/catalog-ui/app/scripts/directives/edit-name-popover/edit-name-popover-view.html b/catalog-ui/app/scripts/directives/edit-name-popover/edit-name-popover-view.html new file mode 100644 index 0000000000..17beead6b3 --- /dev/null +++ b/catalog-ui/app/scripts/directives/edit-name-popover/edit-name-popover-view.html @@ -0,0 +1 @@ +<div uib-popover-template="templateUrl" popover-title="{{header}}" popover-placement="{{direction}}" popover-is-open="isOpen" popover-append-to-body="true"></div> diff --git a/catalog-ui/app/scripts/directives/edit-name-popover/edit-name-popover.less b/catalog-ui/app/scripts/directives/edit-name-popover/edit-name-popover.less new file mode 100644 index 0000000000..3d76a352ce --- /dev/null +++ b/catalog-ui/app/scripts/directives/edit-name-popover/edit-name-popover.less @@ -0,0 +1,71 @@ +.popover { + max-width: none; + width: 310px; + left: initial !important; + right: 10px; + z-index: 100; + border-radius: 0; + + .top > .arrow { + bottom: -8px !important; + } + + .popover-title { + background-color: transparent; + border-bottom: 2px solid #40b7e4; + margin-left: 20px; + margin-right: 20px; + padding: 8px 14px 8px 0px; + font-family: @font-omnes-medium; + font-weight: bold; + } + + .arrow { + left: 95% !important; + border-width: 7px; + bottom: -8px !important; + } + + .popover-content { + width: inherit; + padding: 9px 20px; + } + + .form-group { + margin-top: 9px; + } + + .popover-btn { + float:right; + margin-left: 10px; + margin-bottom: 20px; + } + + .close-popover-btn { + position: absolute; + top: 11px; + right: 25px; + } + + .close-popover-btn:hover { + cursor: pointer; + } + + .popover-input { + height: 47px; + margin-bottom:5px; + } + + .popover-label { + text-overflow: ellipsis; + display: block; + white-space: nowrap; + margin-bottom: 10px; + font-family: @font-omnes-medium; + color: @main_color_l; + } + + .w-sdc-form .i-sdc-form-item { + margin-bottom: 0px; + } +} diff --git a/catalog-ui/app/scripts/directives/elements/checkbox/checkbox.html b/catalog-ui/app/scripts/directives/elements/checkbox/checkbox.html new file mode 100644 index 0000000000..daf2a89ac2 --- /dev/null +++ b/catalog-ui/app/scripts/directives/elements/checkbox/checkbox.html @@ -0,0 +1,13 @@ +<label class="tlv-checkbox" ng-class="{'disabled' : disabled}"> + <input id="{{elemId}}" + name="{{elemId}}" + class="tlv-checkbox-i" + type="checkbox" + checked="" + ng-disabled="disabled" + checklist-model="sdcChecklistModel" + checklist-value="sdcChecklistValue" + /> + + <span sdc-smart-tooltip class="tlv-checkbox-label" data-tests-id="{{elemId}}">{{text}}</span> +</label> diff --git a/catalog-ui/app/scripts/directives/elements/checkbox/checkbox.less b/catalog-ui/app/scripts/directives/elements/checkbox/checkbox.less new file mode 100644 index 0000000000..0747a680a9 --- /dev/null +++ b/catalog-ui/app/scripts/directives/elements/checkbox/checkbox.less @@ -0,0 +1,35 @@ +label.tlv-checkbox { + font-weight: normal; +} + +/* +input[type="checkbox"] { + display:none; +} + +input[type="checkbox"] + label span { + margin-right: 6px; + vertical-align: text-bottom; + .sprite-new; + .checkbox_unchecked; + cursor:pointer; +} + +input[type="checkbox"]:checked + label span { + vertical-align: text-bottom; + .sprite-new; + .checkbox_checked; +} + +input[type="checkbox"]:focus + label span { + vertical-align: text-bottom; + .sprite-new; + .checkbox_focus; +} + +input[type="checkbox"][disabled] + label{ + vertical-align: text-bottom; + .sprite-new; + .checkbox_disabled; +} +*/ diff --git a/catalog-ui/app/scripts/directives/elements/checkbox/checkbox.ts b/catalog-ui/app/scripts/directives/elements/checkbox/checkbox.ts new file mode 100644 index 0000000000..c45a9d92e1 --- /dev/null +++ b/catalog-ui/app/scripts/directives/elements/checkbox/checkbox.ts @@ -0,0 +1,66 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ +/// <reference path="../../../references"/> +module Sdc.Directives { + 'use strict'; + + export interface ICheckboxElementScope extends ng.IScope { + elemId: string; + text: string; + sdcChecklistModel: any; + sdcChecklistValue: string; + disabled:boolean; + } + + export class CheckboxElementDirective implements ng.IDirective { + + constructor(private $templateCache:ng.ITemplateCacheService, + private $filter:ng.IFilterService) { + } + + public replace = true; + public restrict = 'E'; + public transclude = false; + + scope = { + elemId: '@', + text: '@', + disabled: '=', + sdcChecklistModel: '=', + sdcChecklistValue: '=' + }; + + template = ():string => { + return this.$templateCache.get('/app/scripts/directives/elements/checkbox/checkbox.html'); + }; + + public link = (scope:ICheckboxElementScope, $elem:ng.IAugmentedJQuery, $attrs:angular.IAttributes) => { + //$elem.removeAttr("id") + //console.log(scope.sdcChecklistValue); + }; + + public static factory = ($templateCache:ng.ITemplateCacheService, $filter:ng.IFilterService)=> { + return new CheckboxElementDirective($templateCache, $filter); + }; + + } + + CheckboxElementDirective.factory.$inject = ['$templateCache', '$filter']; +} diff --git a/catalog-ui/app/scripts/directives/elements/radiobutton/radiobutton.html b/catalog-ui/app/scripts/directives/elements/radiobutton/radiobutton.html new file mode 100644 index 0000000000..b31fae5d73 --- /dev/null +++ b/catalog-ui/app/scripts/directives/elements/radiobutton/radiobutton.html @@ -0,0 +1,5 @@ +<div class="tlv-radio"> + <input name="{{elemName}}" id="{{elemId}}" class="tlv-radio-i" type="radio" ng-model="sdcModel" ng-click="onValueChange()" + ng-disabled="disabled" value="{{value}}" /> + <label for="{{elemId}}" sdc-smart-tooltip class="tlv-radio-label">{{text}}</label> +</div> diff --git a/catalog-ui/app/scripts/directives/elements/radiobutton/radiobutton.less b/catalog-ui/app/scripts/directives/elements/radiobutton/radiobutton.less new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/catalog-ui/app/scripts/directives/elements/radiobutton/radiobutton.less diff --git a/catalog-ui/app/scripts/directives/elements/radiobutton/radiobutton.ts b/catalog-ui/app/scripts/directives/elements/radiobutton/radiobutton.ts new file mode 100644 index 0000000000..9fe58d8f8b --- /dev/null +++ b/catalog-ui/app/scripts/directives/elements/radiobutton/radiobutton.ts @@ -0,0 +1,71 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ +/// <reference path="../../../references"/> +module Sdc.Directives { + import INgModelController = angular.INgModelController; + 'use strict'; + + export interface IRadiobuttonElementScope extends ng.IScope { + elemId: string; + elemName: string; + text: string; + sdcModel: any; + value: any; + disabled: boolean; + onValueChange:Function; + } + + export class RadiobuttonElementDirective implements ng.IDirective { + + constructor(private $templateCache:ng.ITemplateCacheService, + private $filter:ng.IFilterService) { + } + + public replace = true; + public restrict = 'E'; + public transclude = false; + + scope = { + elemId: '@', + elemName: '@', + text: '@', + sdcModel: '=', + value: '@', + disabled: '=', + onValueChange: '&' + }; + + template = ():string => { + return this.$templateCache.get('/app/scripts/directives/elements/radiobutton/radiobutton.html'); + }; + + public link = (scope:IRadiobuttonElementScope, $elem:ng.IAugmentedJQuery, $attrs:angular.IAttributes) => { + //$elem.removeAttr("id") + //console.log(scope.sdcChecklistValue); + }; + + public static factory = ($templateCache:ng.ITemplateCacheService, $filter:ng.IFilterService)=> { + return new RadiobuttonElementDirective($templateCache, $filter); + }; + + } + + RadiobuttonElementDirective.factory.$inject = ['$templateCache', '$filter']; +} diff --git a/catalog-ui/app/scripts/directives/ellipsis/ellipsis-directive.html b/catalog-ui/app/scripts/directives/ellipsis/ellipsis-directive.html new file mode 100644 index 0000000000..31fa06adda --- /dev/null +++ b/catalog-ui/app/scripts/directives/ellipsis/ellipsis-directive.html @@ -0,0 +1,7 @@ +{{actualText}} + +<span class="ellipsis-directive-more-less" + data-ng-click="collapsed = !collapsed; toggleText()" + data-ng-hide="ellipsis.length <= maxChars"> + {{collapsed ? "More" : "Less"}} +</span> diff --git a/catalog-ui/app/scripts/directives/ellipsis/ellipsis-directive.less b/catalog-ui/app/scripts/directives/ellipsis/ellipsis-directive.less new file mode 100644 index 0000000000..d8dfdbb73b --- /dev/null +++ b/catalog-ui/app/scripts/directives/ellipsis/ellipsis-directive.less @@ -0,0 +1,10 @@ +.ellipsis-directive-more-less { + .a_9; + .bold; + .hand; + float: right; + margin-right: 17px; + line-height: 23px; + text-decoration: underline; + text-align: left; +} diff --git a/catalog-ui/app/scripts/directives/ellipsis/ellipsis-directive.ts b/catalog-ui/app/scripts/directives/ellipsis/ellipsis-directive.ts new file mode 100644 index 0000000000..a5ccf248e0 --- /dev/null +++ b/catalog-ui/app/scripts/directives/ellipsis/ellipsis-directive.ts @@ -0,0 +1,80 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ +/// <reference path="../../references"/> +module Sdc.Directives { + 'use strict'; + export interface IEllipsisScope extends ng.IScope { + ellipsis: string; + maxChars: number; + toggleText(): void; + collapsed: boolean; + actualText: string; + + } + + export class EllipsisDirective implements ng.IDirective { + + constructor(private $templateCache: ng.ITemplateCacheService) {} + + scope = { + ellipsis: '=', + moreClass: '@', + maxChars: '=' + }; + + replace = false; + restrict = 'A'; + template = (): string => { + return this.$templateCache.get('/app/scripts/directives/ellipsis/ellipsis-directive.html'); + }; + + link = (scope:IEllipsisScope, $elem:any) => { + + + scope.collapsed = true; + + scope.toggleText = (): void => { + if(scope.ellipsis && scope.collapsed) { + scope.actualText = scope.ellipsis.substr(0, scope.maxChars); + scope.actualText += scope.ellipsis.length > scope.maxChars ? '...' : ''; + } + else + { + scope.actualText = scope.ellipsis; + } + }; + + scope.$watch("ellipsis", function(){ + scope.collapsed = true; + scope.toggleText(); + }); + + + + }; + + public static factory = ($templateCache: ng.ITemplateCacheService)=> { + return new EllipsisDirective($templateCache); + }; + + } + + EllipsisDirective.factory.$inject = ['$templateCache']; +} diff --git a/catalog-ui/app/scripts/directives/events/on-last-repeat/on-last-repeat.ts b/catalog-ui/app/scripts/directives/events/on-last-repeat/on-last-repeat.ts new file mode 100644 index 0000000000..0fb682d202 --- /dev/null +++ b/catalog-ui/app/scripts/directives/events/on-last-repeat/on-last-repeat.ts @@ -0,0 +1,61 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ +/// <reference path="../../../references"/> +module Sdc.Directives { + 'use strict'; + + /** + * Usage: + * In data-ng-repeat html: <ol ng-repeat="record in records" on-last-repeat> + * In the controller, catch the last repeat: + * $scope.$on('onRepeatLast', function(scope, element, attrs){ + * //work your magic + * }); + */ + export interface IOnLastRepeatDirectiveScope extends ng.IScope { + $last:any; + } + + export class OnLastRepeatDirective implements ng.IDirective { + + constructor() {} + + scope = {}; + + restrict = 'AE'; + replace = true; + + link = (scope:IOnLastRepeatDirectiveScope, element:any, attrs:any) => { + let s:any = scope.$parent; // repeat scope + if (s.$last) { + setTimeout(function(){ + s.$emit('onRepeatLast', element, attrs); + }, 1); + } + }; + + public static factory = ()=> { + return new OnLastRepeatDirective(); + }; + + } + + OnLastRepeatDirective.factory.$inject = []; +} diff --git a/catalog-ui/app/scripts/directives/file-opener/file-opener.html b/catalog-ui/app/scripts/directives/file-opener/file-opener.html new file mode 100644 index 0000000000..38f82554e9 --- /dev/null +++ b/catalog-ui/app/scripts/directives/file-opener/file-opener.html @@ -0,0 +1,3 @@ +<div> +<input class="i-sdc-dashboard-item-upload-input" type="file" data-tests-id="file-{{testsId}}" data-ng-model="importFile" base-sixty-four-input data-ng-change="onFileSelect()" accept="{{getExtensionsWithDot()}}"/> +</div> diff --git a/catalog-ui/app/scripts/directives/file-opener/file-opener.ts b/catalog-ui/app/scripts/directives/file-opener/file-opener.ts new file mode 100644 index 0000000000..b7e3e1804c --- /dev/null +++ b/catalog-ui/app/scripts/directives/file-opener/file-opener.ts @@ -0,0 +1,77 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ +/// <reference path="../../references"/> +module Sdc.Directives { + 'use strict'; + + export interface IFileOpenerScope extends ng.IScope { + importFile:any; + testsId:any; + extensions:string; + + onFileSelect():void; + onFileUpload(file:any):void; + getExtensionsWithDot():string; + } + + export class FileOpenerDirective implements ng.IDirective { + + constructor(private $templateCache:ng.ITemplateCacheService, + private $compile:ng.ICompileService) { + } + + scope = { + onFileUpload: '&', + testsId: '@', + extensions: '@' + }; + + restrict = 'AE'; + replace = true; + template = ():string => { + return this.$templateCache.get('/app/scripts/directives/file-opener/file-opener.html'); + }; + + link = (scope:IFileOpenerScope, element:any) => { + + scope.onFileSelect = () => { + scope.onFileUpload({file: scope.importFile}); + element.html(this.$templateCache.get('/app/scripts/directives/file-opener/file-opener.html')); + this.$compile(element.contents())(scope); + }; + + scope.getExtensionsWithDot = ():string => { + let ret = []; + _.each(scope.extensions.split(','), function(item){ + ret.push("." + item.toString()); + }); + return ret.join(","); + }; + + }; + + public static factory = ($templateCache:ng.ITemplateCacheService, $compile:ng.ICompileService)=> { + return new FileOpenerDirective($templateCache, $compile); + }; + + } + + FileOpenerDirective.factory.$inject = ['$templateCache', '$compile']; +} diff --git a/catalog-ui/app/scripts/directives/file-type/file-type.ts b/catalog-ui/app/scripts/directives/file-type/file-type.ts new file mode 100644 index 0000000000..e7dee17960 --- /dev/null +++ b/catalog-ui/app/scripts/directives/file-type/file-type.ts @@ -0,0 +1,66 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ +/// <reference path="../../references"/> +module Sdc.Directives { + 'use strict'; + + export class FileTypeDirective implements ng.IDirective { + + constructor() {} + + require = 'ngModel'; + + link = (scope, elem, attrs, ngModel) => { + + let typesToApprove = ""; + + attrs.$observe('fileType', (val:string) => { + typesToApprove = val; + validate(ngModel.$viewValue); + }); + + let validate: Function = function (value) { + let fileName:string = elem.val(), valid:boolean = true; + + if (fileName && value && typesToApprove) { + let extension:string = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase(); + valid = typesToApprove.split(',').indexOf(extension) > -1; + } + + ngModel.$setValidity('filetype', valid); + if(!value) { + ngModel.$setPristine(); + } + return value; + }; + + //For DOM -> model validation + ngModel.$parsers.unshift(validate); + + }; + + public static factory = ()=> { + return new FileTypeDirective(); + }; + + } + + FileTypeDirective.factory.$inject = []; +} diff --git a/catalog-ui/app/scripts/directives/file-upload/file-upload.html b/catalog-ui/app/scripts/directives/file-upload/file-upload.html new file mode 100644 index 0000000000..7cbc8d25f3 --- /dev/null +++ b/catalog-ui/app/scripts/directives/file-upload/file-upload.html @@ -0,0 +1,22 @@ +<div class="i-sdc-form-item i-sdc-form-file-upload"> + <span class="i-sdc-form-file-name" data-tests-id="filename">{{(fileModel && fileModel.filename) || defaultText}}</span> + <div class="i-sdc-form-file-upload-x-btn" ng-click="cancel()" data-ng-show="fileModel.filename && fileModel.filename!=='' && elementDisabled!=='true'"></div> + <label class="i-sdc-form-file-upload-label"> + <input + type="file" + name="{{elementName}}" + ng-model="myFileModel" + base-sixty-four-input + accept="{{getExtensionsWithDot()}}" + file-type="{{extensions}}" + data-ng-change="onFileChange()" + onchange="angular.element(this).scope().setEmptyError(this)" + onclick="angular.element(this).scope().onFileClick(this)" + data-ng-required="{{elementRequired}}" + data-ng-disabled="elementDisabled==='true'" + data-tests-id="browseButton" + maxsize="10240" + /> + <div class="file-upload-browse-btn" data-ng-class="{'disabled':elementDisabled==='true'}">Browse</div> + </label> +</div> diff --git a/catalog-ui/app/scripts/directives/file-upload/file-upload.less b/catalog-ui/app/scripts/directives/file-upload/file-upload.less new file mode 100644 index 0000000000..1c4b010853 --- /dev/null +++ b/catalog-ui/app/scripts/directives/file-upload/file-upload.less @@ -0,0 +1,75 @@ +.i-sdc-form-file-upload { + + display: flex; + margin-top: 0; + width: 100%; + .p_1; + .bg_c; + .border-radius(2px); + border: solid 1px @border_color_f; + height: 30px; + + input[type="file"] { + cursor: inherit; + display: block; + filter: alpha(opacity=0); + width: 100px; + height: 30px; + opacity: 0; + position: absolute; + right: 0; + text-align: right; + top: 0; + } + + .i-sdc-form-file-name{ + flex-grow: 999; + text-align: left; + padding: 3px 10px; + opacity: 0.6; + width: 80%; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + + } + + .i-sdc-form-file-upload-x-btn{ + flex-grow: 1; + .sprite; + .sprite.small-x-btn-black; + cursor: pointer; + top: 10px; + right: 9px; + width: 10px; + position: relative; + } + .i-sdc-form-file-upload-label { + float: right; + width: 100px; + height: 100%; + .bg_n; + .b_9; + + .file-upload-browse-btn { + .noselect; + padding: 4px 6px; + cursor: pointer; + z-index: 999; + position: absolute; + width: 100px; + height: 28px; + text-align: center; + + &.disabled { + cursor: default; + } + } + } + + &.error { + border-color: #da1f3d; + outline: none; + box-sizing: border-box; + } +} diff --git a/catalog-ui/app/scripts/directives/file-upload/file-upload.ts b/catalog-ui/app/scripts/directives/file-upload/file-upload.ts new file mode 100644 index 0000000000..16db3e7e21 --- /dev/null +++ b/catalog-ui/app/scripts/directives/file-upload/file-upload.ts @@ -0,0 +1,134 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ +/** + * Created by obarda on 1/27/2016. + */ +/// <reference path="../../references"/> +module Sdc.Directives { + 'use strict'; + + export class FileUploadModel { + filetype: string; + filename: string; + filesize: number; + base64: string; + } + + export interface IFileUploadScope extends ng.IScope { + fileModel: FileUploadModel; + formElement:ng.IFormController; + extensions: string; + elementDisabled: string; + elementName: string; + elementRequired: string; + myFileModel: any; // From the ng bind to <input type=file + defaultText: string; + onFileChangedInDirective:Function; + + getExtensionsWithDot():string; + onFileChange():void + onFileClick(element:any):void; + setEmptyError(element):void; + validateField(field:any):boolean; + cancel():void; + } + + + export class FileUploadDirective implements ng.IDirective { + + constructor(private $templateCache:ng.ITemplateCacheService, private sdcConfig:Models.IAppConfigurtaion) { + } + + scope = { + fileModel: '=', + formElement: '=', + extensions: '@', + elementDisabled: '@', + elementName: '@', + elementRequired: '@', + onFileChangedInDirective: '=?', + defaultText: '=', + }; + + restrict = 'E'; + replace = true; + template = ():string => { + return this.$templateCache.get('/app/scripts/directives/file-upload/file-upload.html'); + }; + + link = (scope:IFileUploadScope, element:any, $attr:any) => { + + // In case the browse has filename, set it valid. + // When editing artifact the file is not sent again, so if we have filename I do not want to show error. + if (scope.fileModel && scope.fileModel.filename && scope.fileModel.filename!==''){ + scope.formElement[scope.elementName].$setValidity('required', true); + } + + scope.getExtensionsWithDot = ():string => { + let ret = []; + if(scope.extensions) { + _.each(scope.extensions.split(','), function (item) { + ret.push("." + item.toString()); + }); + } + return ret.join(","); + }; + + scope.onFileChange = ():void => { + if (scope.onFileChangedInDirective) { + scope.onFileChangedInDirective(); + } + if (scope.myFileModel) { + scope.fileModel = scope.myFileModel; + scope.formElement[scope.elementName].$setValidity('required', true); + } + }; + + scope.setEmptyError = (element):void => { + if(element.files[0].size){ + scope.formElement[scope.elementName].$setValidity('emptyFile', true); + }else{ + scope.formElement[scope.elementName].$setValidity('emptyFile', false); + scope.fileModel = undefined; + } + + }; + + // Workaround, in case user select a file then cancel (X) then select the file again, the event onChange is not fired. + // This is a workaround to fix this issue. + scope.onFileClick = (element:any):void => { + element.value = null; + }; + + scope.cancel = ():void => { + scope.fileModel.filename = ''; + scope.formElement[scope.elementName].$pristine; + scope.formElement[scope.elementName].$setValidity('required', false); + } + }; + + public static factory = ($templateCache:ng.ITemplateCacheService, sdcConfig:Models.IAppConfigurtaion)=> { + return new FileUploadDirective($templateCache, sdcConfig); + }; + + } + + FileUploadDirective.factory.$inject = ['$templateCache', 'sdcConfig']; +} diff --git a/catalog-ui/app/scripts/directives/graphs-v2/common/common-graph-utils.ts b/catalog-ui/app/scripts/directives/graphs-v2/common/common-graph-utils.ts new file mode 100644 index 0000000000..e01e455e93 --- /dev/null +++ b/catalog-ui/app/scripts/directives/graphs-v2/common/common-graph-utils.ts @@ -0,0 +1,361 @@ +/** + * Created by obarda on 12/21/2016. + */ +/** + * Created by obarda on 12/13/2016. + */ +/// <reference path="../../../references"/> +module Sdc.Graph.Utils { + + export class CommonGraphUtils { + + constructor(private NodesFactory:Sdc.Utils.NodesFactory, private LinksFactory:Sdc.Utils.LinksFactory) { + + } + + public safeApply = (scope:ng.IScope, fn:any) => { //todo remove to general utils + let phase = scope.$root.$$phase; + if (phase == '$apply' || phase == '$digest') { + if (fn && (typeof(fn) === 'function')) { + fn(); + } + } else { + scope.$apply(fn); + } + }; + + /** + * Draw node on the graph + * @param cy + * @param compositionGraphNode + * @param position + * @returns {CollectionElements} + */ + public addNodeToGraph(cy:Cy.Instance, compositionGraphNode:Models.Graph.CommonNodeBase, position?:Cy.Position):Cy.CollectionElements { + + var node = cy.add(<Cy.ElementDefinition> { + group: 'nodes', + position: position, + data: compositionGraphNode, + classes: compositionGraphNode.classes + }); + + if(!node.data().isUcpe) { //ucpe should not have tooltip + this.initNodeTooltip(node); + } + return node; + }; + + /** + * The function will create a component instance node by the componentInstance position. + * If the node is UCPE the function will create all cp lan&wan for the ucpe + * @param cy + * @param compositionGraphNode + * @returns {Cy.CollectionElements} + */ + public addComponentInstanceNodeToGraph(cy:Cy.Instance, compositionGraphNode:Models.Graph.CompositionCiNodeBase):Cy.CollectionElements { + + let nodePosition = { + x: +compositionGraphNode.componentInstance.posX, + y: +compositionGraphNode.componentInstance.posY + }; + + let node = this.addNodeToGraph(cy, compositionGraphNode, nodePosition); + if (compositionGraphNode.isUcpe) { + this.createUcpeCpNodes(cy, node); + } + return node; + }; + + /** + * This function will create CP_WAN & CP_LAN for the UCPE. this is a special node on the group that will behave like ports on the ucpe + * @param cy + * @param ucpeGraphNode + */ + private createUcpeCpNodes(cy:Cy.Instance, ucpeGraphNode:Cy.CollectionNodes):void { + + let requirementsArray:Array<any> = ucpeGraphNode.data().componentInstance.requirements["tosca.capabilities.Node"]; + //show only LAN or WAN requirements + requirementsArray = _.reject(requirementsArray, (requirement:any) => { + let name:string = requirement.ownerName.toLowerCase(); + return name.indexOf('lan') === -1 && name.indexOf('wan') === -1; + }); + requirementsArray.sort(function (a, b) { + let nameA = a.ownerName.toLowerCase().match(/[^ ]+/)[0]; + let nameB = b.ownerName.toLowerCase().match(/[^ ]+/)[0]; + let numA = _.last(a.ownerName.toLowerCase().split(' ')); + let numB = _.last(b.ownerName.toLowerCase().split(' ')); + + if (nameA === nameB) return numA > numB ? 1 : -1; + return nameA < nameB ? 1 : -1; + }); + let position = angular.copy(ucpeGraphNode.boundingbox()); + //add CP nodes to group + let topCps:number = 0; + for (let i = 0; i < requirementsArray.length; i++) { + + let cpNode = this.NodesFactory.createUcpeCpNode(angular.copy(ucpeGraphNode.data().componentInstance)); + cpNode.componentInstance.capabilities = requirementsArray[i]; + cpNode.id = requirementsArray[i].ownerId; + cpNode.group = ucpeGraphNode.data().componentInstance.uniqueId; + cpNode.name = requirementsArray[i].ownerName; //for tooltip + cpNode.displayName = requirementsArray[i].ownerName; + cpNode.displayName = cpNode.displayName.length > 5 ? cpNode.displayName.substring(0, 5) + '...' : cpNode.displayName; + + + if (cpNode.name.toLowerCase().indexOf('lan') > -1) { + cpNode.textPosition = "top"; + cpNode.componentInstance.posX = position.x1 + (i * 90) - (topCps * 90) + 53; + cpNode.componentInstance.posY = position.y1 + 400 + 27; + } else { + cpNode.textPosition = "bottom"; + cpNode.componentInstance.posX = position.x1 + (topCps * 90) + 53; + cpNode.componentInstance.posY = position.y1 + 27; + topCps++; + } + let cyCpNode = this.addComponentInstanceNodeToGraph(cy, cpNode); + cyCpNode.lock(); + } + }; + + /** + * + * @param nodes - all nodes in graph in order to find the edge connecting the two nodes + * @param fromNodeId + * @param toNodeId + * @returns {boolean} true/false if the edge is certified (from node and to node are certified) + */ + public isRelationCertified(nodes:Cy.CollectionNodes, fromNodeId:string, toNodeId:string):boolean { + let resourceTemp = _.filter(nodes, function (node:Cy.CollectionFirst) { + return node.data().id === fromNodeId || node.data().id === toNodeId; + }); + let certified:boolean = true; + + _.forEach(resourceTemp, (item) => { + certified = certified && item.data().certified; + }); + + return certified; + } + + /** + * Add link to graph - only draw the link + * @param cy + * @param link + */ + public insertLinkToGraph = (cy:Cy.Instance, link:Models.CompositionCiLinkBase) => { + + if (!this.isRelationCertified(cy.nodes(), link.source, link.target)) { + link.classes = 'not-certified-link'; + } + cy.add({ + group: 'edges', + data: link, + classes: link.classes + }); + + }; + + /** + * go over the relations and draw links on the graph + * @param cy + * @param instancesRelations + */ + public initGraphLinks(cy:Cy.Instance, instancesRelations:Array<Models.RelationshipModel>) { + + if (instancesRelations) { + _.forEach(instancesRelations, (relationshipModel:Models.RelationshipModel) => { + _.forEach(relationshipModel.relationships, (relationship:Models.Relationship) => { + let linkToCreate = this.LinksFactory.createGraphLink(cy, relationshipModel, relationship); + this.insertLinkToGraph(cy, linkToCreate); + }); + }); + } + } + + /** + * Determine which nodes are in the UCPE and set child data for them. + * @param cy + */ + public initUcpeChildren(cy:Cy.Instance){ + let ucpe:Cy.CollectionNodes = cy.nodes('[?isUcpe]'); // Get ucpe on graph if exist + _.each(cy.edges('.ucpe-host-link'), (link)=>{ + + let ucpeChild:Cy.CollectionNodes = (link.source().id() == ucpe.id())? link.target() : link.source(); + this.initUcpeChildData(ucpeChild, ucpe); + + //vls dont have ucpe-host-link connection, so need to find them and iterate separately + let connectedVLs = ucpeChild.connectedEdges().connectedNodes('.vl-node'); + _.forEach(connectedVLs, (vl)=>{ //all connected vls must be UCPE children because not allowed to connect to a VL outside of the UCPE + this.initUcpeChildData(vl, ucpe); + }); + }); + } + + /** + * Set properties for nodes contained by the UCPE + * @param childNode- node contained in UCPE + * @param ucpe- ucpe container node + */ + public initUcpeChildData(childNode:Cy.CollectionNodes, ucpe:Cy.CollectionNodes){ + + if(!childNode.data('isInsideGroup')){ + this.updateUcpeChildPosition(childNode, ucpe); + childNode.data({isInsideGroup: true}); + } + + } + + /** + * Updates UCPE child node offset, which allows child nodes to be dragged in synchronization with ucpe + * @param childNode- node contained in UCPE + * @param ucpe- ucpe container node + */ + public updateUcpeChildPosition(childNode:Cy.CollectionNodes, ucpe:Cy.CollectionNodes){ + let childPos:Cy.Position = childNode.relativePosition(); + let ucpePos:Cy.Position = ucpe.relativePosition(); + let offset:Cy.Position = { + x: childPos.x - ucpePos.x, + y: childPos.y - ucpePos.y + }; + childNode.data("ucpeOffset", offset); + } + + /** + * Removes ucpe-child properties from the node + * @param childNode- node being removed from UCPE + */ + public removeUcpeChildData(childNode:Cy.CollectionNodes){ + childNode.removeData("ucpeOffset"); + childNode.data({isInsideGroup: false}); + + } + + + public HTMLCoordsToCytoscapeCoords(cytoscapeBoundingBox:Cy.Extent, mousePos:Cy.Position):Cy.Position { + return {x: mousePos.x + cytoscapeBoundingBox.x1, y: mousePos.y + cytoscapeBoundingBox.y1} + }; + + + public getCytoscapeNodePosition = (cy: Cy.Instance, event:IDragDropEvent):Cy.Position => { + let targetOffset = $(event.target).offset(); + let x = event.pageX - targetOffset.left; + let y = event.pageY - targetOffset.top; + + return this.HTMLCoordsToCytoscapeCoords(cy.extent(), { + x: x, + y: y + }); + }; + + + public getNodePosition(node:Cy.CollectionFirstNode):Cy.Position{ + let nodePosition = node.relativePoint(); + if(node.data().isUcpe){ //UCPEs use bounding box and not relative point. + nodePosition = {x: node.boundingbox().x1, y: node.boundingbox().y1}; + } + + return nodePosition; + } + + /** + * return true/false if first node contains in second - this used in order to verify is node is entirely inside ucpe + * @param firstBox + * @param secondBox + * @returns {boolean} + */ + public isFirstBoxContainsInSecondBox(firstBox:Cy.BoundingBox, secondBox:Cy.BoundingBox) { + + return firstBox.x1 > secondBox.x1 && firstBox.x2 < secondBox.x2 && firstBox.y1 > secondBox.y1 && firstBox.y2 < secondBox.y2; + + }; + + + /** + * Check if node node bounds position is inside any ucpe on graph, and return the ucpe + * @param {diagram} the diagram. + * @param {nodeActualBounds} the actual bound position of the node. + * @return the ucpe if found else return null + */ + public isInUcpe = (cy: Cy.Instance, nodeBounds: Cy.BoundingBox): Cy.CollectionElements => { + + let ucpeNodes = cy.nodes('[?isUcpe]').filterFn((ucpeNode) => { + return this.isFirstBoxContainsInSecondBox(nodeBounds, ucpeNode.boundingbox()); + }); + return ucpeNodes; + }; + + /** + * + * @param cy + * @param node + * @returns {Array} + */ + public getLinkableNodes(cy:Cy.Instance, node:Cy.CollectionFirstNode):Array<Models.Graph.CompositionCiNodeBase>{ + let compatibleNodes = []; + _.each(cy.nodes(), (tempNode)=>{ + if(this.nodeLocationsCompatible(cy, node, tempNode)){ + compatibleNodes.push(tempNode.data()); + } + }); + return compatibleNodes; + } + + /** + * Checks whether node locations are compatible in reference to UCPEs. + * Returns true if both nodes are in UCPE or both nodes out, or one node is UCPEpart. + * @param node1 + * @param node2 + */ + public nodeLocationsCompatible(cy:Cy.Instance, node1:Cy.CollectionFirstNode, node2:Cy.CollectionFirstNode){ + + let ucpe = cy.nodes('[?isUcpe]'); + if(!ucpe.length){ return true; } + if(node1.data().isUcpePart || node2.data().isUcpePart) { return true; } + + return (this.isFirstBoxContainsInSecondBox(node1.boundingbox(), ucpe.boundingbox()) == this.isFirstBoxContainsInSecondBox(node2.boundingbox(), ucpe.boundingbox())); + + } + + /** + * This function will init qtip tooltip on the node + * @param node - the node we want the tooltip to apply on + */ + public initNodeTooltip(node:Cy.CollectionNodes) { + + let opts = { + content: function () { + return this.data('name'); + }, + position: { + my: 'top center', + at: 'bottom center', + adjust: {x:0, y:-5} + }, + style: { + classes: 'qtip-dark qtip-rounded qtip-custom', + tip: { + width: 16, + height: 8 + } + }, + show: { + event: 'mouseover', + delay: 1000 + }, + hide: {event: 'mouseout mousedown'}, + includeLabels: true + }; + + if (node.data().isUcpePart){ //fix tooltip positioning for UCPE-cps + opts.position.adjust = {x:0, y:20}; + } + + node.qtip(opts); + }; + }; + + + + CommonGraphUtils.$inject = ['NodesFactory', 'LinksFactory']; +}
\ No newline at end of file diff --git a/catalog-ui/app/scripts/directives/graphs-v2/common/style/component-instances-nodes-style.ts b/catalog-ui/app/scripts/directives/graphs-v2/common/style/component-instances-nodes-style.ts new file mode 100644 index 0000000000..2ec0174aa9 --- /dev/null +++ b/catalog-ui/app/scripts/directives/graphs-v2/common/style/component-instances-nodes-style.ts @@ -0,0 +1,259 @@ +/** + * Created by obarda on 12/18/2016. + */ +/** + * Created by obarda on 12/13/2016. + */ +/// <reference path="../../../../references"/> +module Sdc.Graph.Utils.ComponentIntanceNodesStyle { + + export function getCompositionGraphStyle():Array<Cy.Stylesheet> { + return [ + { + selector: 'core', + css: { + 'shape': 'rectangle', + 'active-bg-size': 0, + 'selection-box-color': 'rgb(0, 159, 219)', + 'selection-box-opacity': 0.2, + 'selection-box-border-color': '#009fdb', + 'selection-box-border-width': 1 + + } + }, + { + selector: 'node', + css: { + 'font-family': 'omnes-regular,sans-serif', + 'font-size': 14, + 'events': 'yes', + 'text-events': 'yes', + 'text-border-width': 15, + 'text-border-color': Sdc.Utils.Constants.GraphColors.NODE_UCPE, + 'text-margin-y': 5 + } + }, + { + selector: '.vf-node', + css: { + 'background-color': 'transparent', + 'shape': 'rectangle', + 'label': 'data(displayName)', + 'background-image': 'data(img)', + 'width': 65, + 'height': 65, + 'background-opacity': 0, + "background-width": 65, + "background-height": 65, + 'text-valign': 'bottom', + 'text-halign': 'center', + 'background-fit': 'cover', + 'background-clip': 'node', + 'overlay-color': Sdc.Utils.Constants.GraphColors.NODE_BACKGROUND_COLOR, + 'overlay-opacity': 0 + } + }, + + { + selector: '.service-node', + css: { + 'background-color': 'transparent', + 'label': 'data(displayName)', + 'events': 'yes', + 'text-events': 'yes', + 'background-image': 'data(img)', + 'width': 64, + 'height': 64, + "border-width": 0, + 'text-valign': 'bottom', + 'text-halign': 'center', + 'background-opacity': 0, + 'overlay-color': Sdc.Utils.Constants.GraphColors.NODE_BACKGROUND_COLOR, + 'overlay-opacity': 0 + } + }, + { + selector: '.cp-node', + css: { + 'background-color': 'rgb(255,255,255)', + 'shape': 'rectangle', + 'label': 'data(displayName)', + 'background-image': 'data(img)', + 'background-width': 21, + 'background-height': 21, + 'width': 21, + 'height': 21, + 'text-valign': 'bottom', + 'text-halign': 'center', + 'background-opacity': 0, + 'overlay-color': Sdc.Utils.Constants.GraphColors.NODE_BACKGROUND_COLOR, + 'overlay-opacity': 0 + } + }, + { + selector: '.vl-node', + css: { + 'background-color': 'rgb(255,255,255)', + 'shape': 'rectangle', + 'label': 'data(displayName)', + 'background-image': 'data(img)', + 'background-width': 21, + 'background-height': 21, + 'width': 21, + 'height': 21, + 'text-valign': 'bottom', + 'text-halign': 'center', + 'background-opacity': 0, + 'overlay-color': Sdc.Utils.Constants.GraphColors.NODE_BACKGROUND_COLOR, + 'overlay-opacity': 0 + } + }, + { + selector: '.ucpe-cp', + css: { + 'background-color': Sdc.Utils.Constants.GraphColors.NODE_UCPE_CP, + 'background-width': 15, + 'background-height': 15, + 'width': 15, + 'height': 15, + 'text-halign': 'center', + 'overlay-opacity': 0, + 'label': 'data(displayName)', + 'text-valign': 'data(textPosition)', + 'text-margin-y': (ele:Cy.Collection) => { + return (ele.data('textPosition') == 'top')? -5 : 5; + }, + 'font-size': 12 + } + }, + { + selector: '.ucpe-node', + css: { + 'background-fit': 'cover', + 'padding-bottom': 0, + 'padding-top': 0 + } + }, + { + selector: '.simple-link', + css: { + 'width': 1, + 'line-color': Sdc.Utils.Constants.GraphColors.BASE_LINK, + 'target-arrow-color': '#3b7b9b', + 'target-arrow-shape': 'triangle', + 'curve-style': 'bezier', + 'control-point-step-size': 30 + } + }, + { + selector: '.vl-link', + css: { + 'width': 3, + 'line-color': Sdc.Utils.Constants.GraphColors.VL_LINK, + 'curve-style': 'bezier', + 'control-point-step-size': 30 + } + }, + { + selector: '.ucpe-host-link', + css: { + 'width': 0 + } + }, + { + selector: '.not-certified-link', + css: { + 'width': 1, + 'line-color': Sdc.Utils.Constants.GraphColors.NOT_CERTIFIED_LINK, + 'curve-style': 'bezier', + 'control-point-step-size': 30, + 'line-style': 'dashed', + 'target-arrow-color': '#3b7b9b', + 'target-arrow-shape': 'triangle' + + } + }, + + { + selector: '.not-certified', + css: { + 'shape': 'rectangle', + 'background-image': (ele:Cy.Collection) => { + return ele.data().initImage(ele) + }, + "border-width": 0 + } + }, + { + selector: 'node:selected', + css: { + "border-width": 2, + "border-color": Sdc.Utils.Constants.GraphColors.NODE_SELECTED_BORDER_COLOR, + 'shape': 'rectangle' + } + }, + { + selector: 'edge:selected', + css: { + 'line-color': Sdc.Utils.Constants.GraphColors.ACTIVE_LINK + + } + }, + { + selector: 'edge:active', + css: { + 'overlay-opacity': 0 + } + } + ] + } + + export function getBasicNodeHanlde() { + return { + positionX: "center", + positionY: "top", + offsetX: 15, + offsetY: -20, + color: "#27a337", + type: "default", + single: false, + nodeTypeNames: ["basic-node"], + imageUrl: Sdc.Utils.Constants.IMAGE_PATH + '/styles/images/resource-icons/' + 'canvasPlusIcon.png', + lineWidth: 2, + lineStyle: 'dashed' + + } + } + + export function getBasicSmallNodeHandle() { + return { + positionX: "center", + positionY: "top", + offsetX: 3, + offsetY: -25, + color: "#27a337", + type: "default", + single: false, + nodeTypeNames: ["basic-small-node"], + imageUrl: Sdc.Utils.Constants.IMAGE_PATH + '/styles/images/resource-icons/' + 'canvasPlusIcon.png', + lineWidth: 2, + lineStyle: 'dashed' + } + } + + export function getUcpeCpNodeHandle() { + return { + positionX: "center", + positionY: "center", + offsetX: -8, + offsetY: -10, + color: "#27a337", + type: "default", + single: false, + nodeTypeNames: ["ucpe-cp-node"], + imageUrl: Sdc.Utils.Constants.IMAGE_PATH + '/styles/images/resource-icons/' + 'canvasPlusIcon.png', + lineWidth: 2, + lineStyle: 'dashed' + } + } +}
\ No newline at end of file diff --git a/catalog-ui/app/scripts/directives/graphs-v2/common/style/module-node-style.ts b/catalog-ui/app/scripts/directives/graphs-v2/common/style/module-node-style.ts new file mode 100644 index 0000000000..62436fbf74 --- /dev/null +++ b/catalog-ui/app/scripts/directives/graphs-v2/common/style/module-node-style.ts @@ -0,0 +1,92 @@ +/** + * Created by obarda on 1/1/2017. + */ +/** + * Created by obarda on 12/18/2016. + */ +/** + * Created by obarda on 12/13/2016. + */ +/// <reference path="../../../../references"/> +module Sdc.Graph.Utils.ModulesNodesStyle { + + export function getModuleGraphStyle():Array<Cy.Stylesheet> { + + return [ + { + selector: '.cy-expand-collapse-collapsed-node', + css: { + 'background-image': 'data(img)', + 'width': 34, + 'height': 32, + 'background-opacity': 0, + 'shape': 'rectangle', + 'label': 'data(displayName)', + 'events': 'yes', + 'text-events': 'yes', + 'text-valign': 'bottom', + 'text-halign': 'center', + 'text-margin-y': 5, + 'border-opacity': 0 + } + }, + { + selector: '.module-node', + css: { + 'background-color': 'transparent', + 'background-opacity': 0, + "border-width": 2, + "border-color": Sdc.Utils.Constants.GraphColors.NODE_SELECTED_BORDER_COLOR, + 'border-style': 'dashed', + 'label': 'data(displayName)', + 'events': 'yes', + 'text-events': 'yes', + 'text-valign': 'bottom', + 'text-halign': 'center', + 'text-margin-y': 8 + } + }, + { + selector: 'node:selected', + css: { + "border-opacity": 0 + } + }, + { + selector: '.simple-link:selected', + css: { + 'line-color': Sdc.Utils.Constants.GraphColors.BASE_LINK, + } + }, + { + selector: '.vl-link:selected', + css: { + 'line-color': Sdc.Utils.Constants.GraphColors.VL_LINK, + } + }, + { + selector: '.cy-expand-collapse-collapsed-node:selected', + css: { + "border-color": Sdc.Utils.Constants.GraphColors.NODE_SELECTED_BORDER_COLOR, + 'border-opacity': 1, + 'border-style': 'solid', + 'border-width': 2 + } + }, + { + selector: '.module-node:selected', + css: { + "border-color": Sdc.Utils.Constants.GraphColors.NODE_SELECTED_BORDER_COLOR, + 'border-opacity': 1 + } + }, + { + selector: '.dummy-node', + css: { + 'width': 20, + 'height': 20 + } + }, + ] + } +}
\ No newline at end of file diff --git a/catalog-ui/app/scripts/directives/graphs-v2/composition-graph/composition-graph.directive.ts b/catalog-ui/app/scripts/directives/graphs-v2/composition-graph/composition-graph.directive.ts new file mode 100644 index 0000000000..708f1d091a --- /dev/null +++ b/catalog-ui/app/scripts/directives/graphs-v2/composition-graph/composition-graph.directive.ts @@ -0,0 +1,555 @@ +/// <reference path="../../../references"/> +module Sdc.Directives { + + import ComponentFactory = Sdc.Utils.ComponentFactory; + import LoaderService = Sdc.Services.LoaderService; + import GRAPH_EVENTS = Sdc.Utils.Constants.GRAPH_EVENTS; + + interface ICompositionGraphScope extends ng.IScope { + + component:Models.Components.Component; + isViewOnly:boolean; + // Link menu - create link menu + relationMenuDirectiveObj:Models.RelationMenuDirectiveObj; + isLinkMenuOpen:boolean; + createLinkFromMenu:(chosenMatch:Models.MatchBase, vl:Models.Components.Component)=>void; + + //modify link menu - for now only delete menu + relationMenuTimeout:ng.IPromise<any>; + linkMenuObject:Models.LinkMenu; + + //left palette functions callbacks + dropCallback(event:JQueryEventObject, ui:any):void; + beforeDropCallback(event:IDragDropEvent):void; + verifyDrop(event:JQueryEventObject, ui:any):void; + + //Links menus + deleteRelation(link:Cy.CollectionEdges):void; + hideRelationMenu(); + } + + export class CompositionGraph implements ng.IDirective { + private _cy:Cy.Instance; + private _currentlyCLickedNodePosition:Cy.Position; + private $document:JQuery = $(document); + private dragElement:JQuery; + private dragComponent: Sdc.Models.ComponentsInstances.ComponentInstance; + + constructor(private $q:ng.IQService, + private $filter:ng.IFilterService, + private $log:ng.ILogService, + private $timeout:ng.ITimeoutService, + private NodesFactory:Sdc.Utils.NodesFactory, + private CompositionGraphLinkUtils:Sdc.Graph.Utils.CompositionGraphLinkUtils, + private GeneralGraphUtils:Graph.Utils.CompositionGraphGeneralUtils, + private ComponentInstanceFactory:Utils.ComponentInstanceFactory, + private NodesGraphUtils:Sdc.Graph.Utils.CompositionGraphNodesUtils, + private eventListenerService:Services.EventListenerService, + private ComponentFactory:ComponentFactory, + private LoaderService:LoaderService, + private commonGraphUtils:Graph.Utils.CommonGraphUtils, + private matchCapabilitiesRequirementsUtils:Graph.Utils.MatchCapabilitiesRequirementsUtils) { + + } + + restrict = 'E'; + templateUrl = '/app/scripts/directives/graphs-v2/composition-graph/composition-graph.html'; + scope = { + component: '=', + isViewOnly: '=' + }; + + link = (scope:ICompositionGraphScope, el:JQuery) => { + this.loadGraph(scope, el); + + scope.$on('$destroy', () => { + this._cy.destroy(); + _.forEach(GRAPH_EVENTS, (event) => { + this.eventListenerService.unRegisterObserver(event); + }); + }); + + }; + + private loadGraph = (scope:ICompositionGraphScope, el:JQuery) => { + + + let graphEl = el.find('.sdc-composition-graph-wrapper'); + this.initGraph(graphEl, scope.isViewOnly); + this.initGraphNodes(scope.component.componentInstances, scope.isViewOnly); + this.commonGraphUtils.initGraphLinks(this._cy, scope.component.componentInstancesRelations); + this.commonGraphUtils.initUcpeChildren(this._cy); + this.initDropZone(scope); + this.registerCytoscapeGraphEvents(scope); + this.registerCustomEvents(scope, el); + this.initViewMode(scope.isViewOnly); + + }; + + private initGraph(graphEl:JQuery, isViewOnly:boolean) { + + this._cy = cytoscape({ + container: graphEl, + style: Sdc.Graph.Utils.ComponentIntanceNodesStyle.getCompositionGraphStyle(), + zoomingEnabled: false, + selectionType: 'single', + boxSelectionEnabled: true, + autolock: isViewOnly, + autoungrabify: isViewOnly + }); + } + + private initViewMode(isViewOnly:boolean) { + + if (isViewOnly) { + //remove event listeners + this._cy.off('drag'); + this._cy.off('handlemouseout'); + this._cy.off('handlemouseover'); + this._cy.edges().unselectify(); + } + }; + + private registerCustomEvents(scope:ICompositionGraphScope, el:JQuery) { + + this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_PALETTE_COMPONENT_HOVER_IN, (component:Models.DisplayComponent) => { + this.$log.info(`composition-graph::registerEventServiceEvents:: palette hover on component: ${component.uniqueId}`); + + let nodesData = this.NodesGraphUtils.getAllNodesData(this._cy.nodes()); + let nodesLinks = this.GeneralGraphUtils.getAllCompositionCiLinks(this._cy); + + if (this.GeneralGraphUtils.componentRequirementsAndCapabilitiesCaching.containsKey(component.uniqueId)) { + let cacheComponent = this.GeneralGraphUtils.componentRequirementsAndCapabilitiesCaching.getValue(component.uniqueId); + let filteredNodesData = this.matchCapabilitiesRequirementsUtils.findByMatchingCapabilitiesToRequirements(cacheComponent, nodesData, nodesLinks); + + this.matchCapabilitiesRequirementsUtils.highlightMatchingComponents(filteredNodesData, this._cy); + this.matchCapabilitiesRequirementsUtils.fadeNonMachingComponents(filteredNodesData, nodesData, this._cy); + + return; + } + + component.component.updateRequirementsCapabilities() + .then((res) => { + component.component.capabilities = res.capabilities; + component.component.requirements = res.requirements; + + let filteredNodesData = this.matchCapabilitiesRequirementsUtils.findByMatchingCapabilitiesToRequirements(component.component, nodesData, nodesLinks); + this.matchCapabilitiesRequirementsUtils.fadeNonMachingComponents(filteredNodesData, nodesData, this._cy); + this.matchCapabilitiesRequirementsUtils.highlightMatchingComponents(filteredNodesData, this._cy) + }); + }); + + this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_PALETTE_COMPONENT_HOVER_OUT, () => { + this._cy.emit('hidehandles'); + this.matchCapabilitiesRequirementsUtils.resetFadedNodes(this._cy); + }); + + this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_PALETTE_COMPONENT_DRAG_START, (dragElement, dragComponent) => { + + this.dragElement = dragElement; + this.dragComponent = this.ComponentInstanceFactory.createComponentInstanceFromComponent(dragComponent); + }); + + this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_PALETTE_COMPONENT_DRAG_ACTION, (event:IDragDropEvent) => { + this._onComponentDrag(event); + + }); + + this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_COMPONENT_INSTANCE_NAME_CHANGED, (component:Models.ComponentsInstances.ComponentInstance) => { + + let selectedNode = this._cy.getElementById(component.uniqueId); + selectedNode.data().componentInstance.name = component.name; + selectedNode.data('displayName', selectedNode.data().getDisplayName()); + + }); + + this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_DELETE_COMPONENT_INSTANCE, (componentInstance:Models.ComponentsInstances.ComponentInstance) => { + let nodeToDelete = this._cy.getElementById(componentInstance.uniqueId); + this.NodesGraphUtils.deleteNode(this._cy, scope.component, nodeToDelete); + }); + + this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_DELETE_MULTIPLE_COMPONENTS, () => { + + this._cy.$('node:selected').each((i:number, node:Cy.CollectionNodes) => { + this.NodesGraphUtils.deleteNode(this._cy, scope.component, node); + }); + + }); + + this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_DELETE_EDGE, (releaseLoading:boolean, linksToDelete:Cy.CollectionEdges) => { + this.CompositionGraphLinkUtils.deleteLink(this._cy, scope.component, releaseLoading, linksToDelete); + }); + + this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_INSERT_NODE_TO_UCPE, (node:Cy.CollectionNodes, ucpe:Cy.CollectionNodes, updateExistingNode: boolean) => { + + this.commonGraphUtils.initUcpeChildData(node, ucpe); + //check if item is a VL, and if so, skip adding the binding to ucpe + if(!(node.data() instanceof Sdc.Models.Graph.CompositionCiNodeVl)){ + this.CompositionGraphLinkUtils.createVfToUcpeLink(scope.component, this._cy, ucpe.data(), node.data()); //create link from the node to the ucpe + } + + if(updateExistingNode){ + let vlsPendingDeletion:Cy.CollectionNodes = this.NodesGraphUtils.deleteNodeVLsUponMoveToOrFromUCPE(scope.component, node.cy(), node); //delete connected VLs that no longer have 2 links + this.CompositionGraphLinkUtils.deleteLinksWhenNodeMovedFromOrToUCPE(scope.component, node.cy(), node, vlsPendingDeletion); //delete all connected links if needed + this.GeneralGraphUtils.pushUpdateComponentInstanceActionToQueue(scope.component, true, node.data().componentInstance); //update componentInstance position + } + + }); + + this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_REMOVE_NODE_FROM_UCPE, (node:Cy.CollectionNodes, ucpe:Cy.CollectionNodes) => { + this.commonGraphUtils.removeUcpeChildData(node); + let vlsPendingDeletion:Cy.CollectionNodes = this.NodesGraphUtils.deleteNodeVLsUponMoveToOrFromUCPE(scope.component, node.cy(), node); + this.CompositionGraphLinkUtils.deleteLinksWhenNodeMovedFromOrToUCPE(scope.component, node.cy(), node, vlsPendingDeletion); //delete all connected links if needed + this.GeneralGraphUtils.pushUpdateComponentInstanceActionToQueue(scope.component, true, node.data().componentInstance); //update componentInstance position + }); + + this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_VERSION_CHANGED, (component:Models.Components.Component) => { + scope.component = component; + this.loadGraph(scope, el); + }); + + + scope.createLinkFromMenu = (chosenMatch:Models.MatchBase, vl:Models.Components.Component):void => { + scope.isLinkMenuOpen = false; + + this.CompositionGraphLinkUtils.createLinkFromMenu(this._cy, chosenMatch, vl, scope.component); + }; + + scope.hideRelationMenu = () => { + this.commonGraphUtils.safeApply(scope, () => { + scope.linkMenuObject = null; + this.$timeout.cancel(scope.relationMenuTimeout); + }); + }; + + + scope.deleteRelation = (link:Cy.CollectionEdges) => { + scope.hideRelationMenu(); + + //if multiple edges selected, delete the VL itself so edges get deleted automatically + if (this._cy.$('edge:selected').length > 1) { + this.NodesGraphUtils.deleteNode(this._cy, scope.component, this._cy.$('node:selected')); + } else { + this.CompositionGraphLinkUtils.deleteLink(this._cy, scope.component, true, link); + } + }; + } + + + private registerCytoscapeGraphEvents(scope:ICompositionGraphScope) { + + this._cy.on('addedgemouseup', (event, data) => { + scope.relationMenuDirectiveObj = this.CompositionGraphLinkUtils.onLinkDrawn(this._cy, data.source, data.target); + if (scope.relationMenuDirectiveObj != null) { + scope.$apply(() => { + scope.isLinkMenuOpen = true; + }); + } + }); + this._cy.on('tapstart', 'node', (event:Cy.EventObject) => { + this._currentlyCLickedNodePosition = angular.copy(event.cyTarget[0].position()); //update node position on drag + if(event.cyTarget.data().isUcpe){ + this._cy.nodes('.ucpe-cp').unlock(); + event.cyTarget.style('opacity', 0.5); + } + }); + + this._cy.on('drag', 'node', (event:Cy.EventObject) => { + + if (event.cyTarget.data().isDraggable) { + event.cyTarget.style({'overlay-opacity': 0.24}); + if (this.GeneralGraphUtils.isValidDrop(this._cy, event.cyTarget)) { + event.cyTarget.style({'overlay-color': Utils.Constants.GraphColors.NODE_BACKGROUND_COLOR}); + } else { + event.cyTarget.style({'overlay-color': Utils.Constants.GraphColors.NODE_OVERLAPPING_BACKGROUND_COLOR}); + } + } + + if(event.cyTarget.data().isUcpe){ + let pos = event.cyTarget.position(); + + this._cy.nodes('[?isInsideGroup]').positions((i, node)=>{ + return { + x: pos.x + node.data("ucpeOffset").x, + y: pos.y + node.data("ucpeOffset").y + } + }); + } + }); + + + this._cy.on('handlemouseover', (event, payload) => { + + if (payload.node.grabbed()) { //no need to add opacity while we are dragging and hovering othe nodes + return; + } + + let nodesData = this.NodesGraphUtils.getAllNodesData(this._cy.nodes()); + let nodesLinks = this.GeneralGraphUtils.getAllCompositionCiLinks(this._cy); + + let linkableNodes = this.commonGraphUtils.getLinkableNodes(this._cy, payload.node); + let filteredNodesData = this.matchCapabilitiesRequirementsUtils.findByMatchingCapabilitiesToRequirements(payload.node.data().componentInstance, linkableNodes, nodesLinks); + this.matchCapabilitiesRequirementsUtils.highlightMatchingComponents(filteredNodesData, this._cy); + this.matchCapabilitiesRequirementsUtils.fadeNonMachingComponents(filteredNodesData, nodesData, this._cy, payload.node.data()); + + }); + + this._cy.on('handlemouseout', () => { + this._cy.emit('hidehandles'); + this.matchCapabilitiesRequirementsUtils.resetFadedNodes(this._cy); + }); + + + this._cy.on('tapend', (event:Cy.EventObject) => { + + if (event.cyTarget === this._cy) { //On Background clicked + if (this._cy.$('node:selected').length === 0) { //if the background click but not dragged + this.eventListenerService.notifyObservers(Sdc.Utils.Constants.GRAPH_EVENTS.ON_GRAPH_BACKGROUND_CLICKED); + } + scope.hideRelationMenu(); + } + + else if (event.cyTarget.isEdge()) { //On Edge clicked + if (scope.isViewOnly) return; + this.CompositionGraphLinkUtils.handleLinkClick(this._cy, event); + this.openModifyLinkMenu(scope, this.CompositionGraphLinkUtils.getModifyLinkMenu(event.cyTarget[0], event), 6000); + } + + else { //On Node clicked + this._cy.nodes(':grabbed').style({'overlay-opacity': 0}); + + let isUcpe:boolean = event.cyTarget.data().isUcpe; + let newPosition = event.cyTarget[0].position(); + //node position changed (drop after drag event) - we need to update position + if (this._currentlyCLickedNodePosition.x !== newPosition.x || this._currentlyCLickedNodePosition.y !== newPosition.y) { + let nodesMoved:Cy.CollectionNodes = this._cy.$(':grabbed'); + if(isUcpe){ + nodesMoved = nodesMoved.add(this._cy.nodes('[?isInsideGroup]:free')); //'child' nodes will not be recognized as "grabbed" elements within cytoscape. manually add them to collection of nodes moved. + } + this.NodesGraphUtils.onNodesPositionChanged(this._cy, scope.component, nodesMoved); + } else { + this.$log.debug('composition-graph::onNodeSelectedEvent:: fired'); + scope.$apply(() => { + this.eventListenerService.notifyObservers(Sdc.Utils.Constants.GRAPH_EVENTS.ON_NODE_SELECTED, event.cyTarget.data().componentInstance); + }); + } + + if(isUcpe){ + this._cy.nodes('.ucpe-cp').lock(); + event.cyTarget.style('opacity', 1); + } + + } + }); + + this._cy.on('boxselect', 'node', (event:Cy.EventObject) => { + this.eventListenerService.notifyObservers(Utils.Constants.GRAPH_EVENTS.ON_NODE_SELECTED, event.cyTarget.data().componentInstance); + }); + } + + private openModifyLinkMenu = (scope:ICompositionGraphScope, linkMenuObject:Models.LinkMenu, timeOutInMilliseconds?:number) => { + + this.commonGraphUtils.safeApply(scope, () => { + scope.linkMenuObject = linkMenuObject; + }); + + scope.relationMenuTimeout = this.$timeout(() => { + scope.hideRelationMenu(); + }, timeOutInMilliseconds ? timeOutInMilliseconds : 6000); + }; + + private initGraphNodes(componentInstances:Models.ComponentsInstances.ComponentInstance[], isViewOnly:boolean) { + + if (!isViewOnly) { //Init nodes handle extension - enable dynamic links + setTimeout(()=> { + let handles = new CytoscapeEdgeEditation; + handles.init(this._cy, 18); + handles.registerHandle(Sdc.Graph.Utils.ComponentIntanceNodesStyle.getBasicNodeHanlde()); + handles.registerHandle(Sdc.Graph.Utils.ComponentIntanceNodesStyle.getBasicSmallNodeHandle()); + handles.registerHandle(Sdc.Graph.Utils.ComponentIntanceNodesStyle.getUcpeCpNodeHandle()); + }, 0); + } + + _.each(componentInstances, (instance) => { + let compositionGraphNode:Models.Graph.CompositionCiNodeBase = this.NodesFactory.createNode(instance); + this.commonGraphUtils.addComponentInstanceNodeToGraph(this._cy, compositionGraphNode); + }); + + + + } + + + private initDropZone(scope:ICompositionGraphScope) { + + if (scope.isViewOnly) { + return; + } + scope.dropCallback = (event:IDragDropEvent) => { + this.$log.debug(`composition-graph::dropCallback:: fired`); + this.addNode(event, scope); + }; + + scope.verifyDrop = (event:JQueryEventObject) => { + + if(this.dragElement.hasClass('red')){ + return false; + } + return true; + }; + + scope.beforeDropCallback = (event:IDragDropEvent): ng.IPromise<void> => { + let deferred: ng.IDeferred<void> = this.$q.defer<void>(); + if(this.dragElement.hasClass('red')){ + deferred.reject(); + } else { + deferred.resolve(); + } + + return deferred.promise; + } + } + + private _getNodeBBox(event:IDragDropEvent, position?:Cy.Position) { + let bbox = <Cy.BoundingBox>{}; + if (!position) { + position = this.commonGraphUtils.getCytoscapeNodePosition(this._cy, event); + } + let cushionWidth:number = 40; + let cushionHeight:number = 40; + + bbox.x1 = position.x - cushionWidth / 2; + bbox.y1 = position.y - cushionHeight / 2; + bbox.x2 = position.x + cushionWidth / 2; + bbox.y2 = position.y + cushionHeight / 2; + return bbox; + } + + private createComponentInstanceOnGraphFromComponent(fullComponent:Models.Components.Component, event:IDragDropEvent, scope:ICompositionGraphScope) { + + let componentInstanceToCreate:Models.ComponentsInstances.ComponentInstance = this.ComponentInstanceFactory.createComponentInstanceFromComponent(fullComponent); + let cytoscapePosition:Cy.Position = this.commonGraphUtils.getCytoscapeNodePosition(this._cy, event); + + componentInstanceToCreate.posX = cytoscapePosition.x; + componentInstanceToCreate.posY = cytoscapePosition.y; + + + let onFailedCreatingInstance:(error:any) => void = (error:any) => { + this.LoaderService.hideLoader('composition-graph'); + }; + + //on success - update node data + let onSuccessCreatingInstance = (createInstance:Models.ComponentsInstances.ComponentInstance):void => { + + this.LoaderService.hideLoader('composition-graph'); + + createInstance.name = this.$filter('resourceName')(createInstance.name); + createInstance.requirements = new Models.RequirementsGroup(fullComponent.requirements); + createInstance.capabilities = new Models.CapabilitiesGroup(fullComponent.capabilities); + createInstance.componentVersion = fullComponent.version; + createInstance.icon = fullComponent.icon; + createInstance.setInstanceRC(); + + let newNode:Models.Graph.CompositionCiNodeBase = this.NodesFactory.createNode(createInstance); + let cyNode:Cy.CollectionNodes = this.commonGraphUtils.addComponentInstanceNodeToGraph(this._cy, newNode); + + //check if node was dropped into a UCPE + let ucpe:Cy.CollectionElements = this.commonGraphUtils.isInUcpe(this._cy, cyNode.boundingbox()); + if (ucpe.length > 0) { + this.eventListenerService.notifyObservers(Utils.Constants.GRAPH_EVENTS.ON_INSERT_NODE_TO_UCPE, cyNode, ucpe, false); + } + + }; + + // Create the component instance on server + this.GeneralGraphUtils.getGraphUtilsServerUpdateQueue().addBlockingUIAction(() => { + scope.component.createComponentInstance(componentInstanceToCreate).then(onSuccessCreatingInstance, onFailedCreatingInstance); + }); + } + + private _onComponentDrag(event:IDragDropEvent) { + + if(event.clientX < Sdc.Utils.Constants.GraphUIObjects.DIAGRAM_PALETTE_WIDTH_OFFSET || event.clientY < Sdc.Utils.Constants.GraphUIObjects.DIAGRAM_HEADER_OFFSET){ //hovering over palette. Dont bother computing validity of drop + this.dragElement.removeClass('red'); + return; + } + + let offsetPosition = {x: event.clientX - Sdc.Utils.Constants.GraphUIObjects.DIAGRAM_PALETTE_WIDTH_OFFSET, y: event.clientY - Sdc.Utils.Constants.GraphUIObjects.DIAGRAM_HEADER_OFFSET} + let bbox = this._getNodeBBox(event, offsetPosition); + + if (this.GeneralGraphUtils.isPaletteDropValid(this._cy, bbox, this.dragComponent)) { + this.dragElement.removeClass('red'); + } else { + this.dragElement.addClass('red'); + } + } + + private addNode(event:IDragDropEvent, scope:ICompositionGraphScope) { + this.LoaderService.showLoader('composition-graph'); + + this.$log.debug('composition-graph::addNode:: fired'); + let draggedComponent:Models.Components.Component = event.dataTransfer.component; + + if (this.GeneralGraphUtils.componentRequirementsAndCapabilitiesCaching.containsKey(draggedComponent.uniqueId)) { + this.$log.debug('composition-graph::addNode:: capabilities found in cache, creating component'); + let fullComponent = this.GeneralGraphUtils.componentRequirementsAndCapabilitiesCaching.getValue(draggedComponent.uniqueId); + this.createComponentInstanceOnGraphFromComponent(fullComponent, event, scope); + return; + } + + this.$log.debug('composition-graph::addNode:: capabilities not found, requesting from server'); + this.ComponentFactory.getComponentFromServer(draggedComponent.getComponentSubType(), draggedComponent.uniqueId) + .then((fullComponent:Models.Components.Component) => { + this.createComponentInstanceOnGraphFromComponent(fullComponent, event, scope); + }); + } + + public static factory = ($q, + $filter, + $log, + $timeout, + NodesFactory, + LinksGraphUtils, + GeneralGraphUtils, + ComponentInstanceFactory, + NodesGraphUtils, + EventListenerService, + ComponentFactory, + LoaderService, + CommonGraphUtils, + MatchCapabilitiesRequirementsUtils) => { + return new CompositionGraph( + $q, + $filter, + $log, + $timeout, + NodesFactory, + LinksGraphUtils, + GeneralGraphUtils, + ComponentInstanceFactory, + NodesGraphUtils, + EventListenerService, + ComponentFactory, + LoaderService, + CommonGraphUtils, + MatchCapabilitiesRequirementsUtils); + } + } + + CompositionGraph.factory.$inject = [ + '$q', + '$filter', + '$log', + '$timeout', + 'NodesFactory', + 'CompositionGraphLinkUtils', + 'CompositionGraphGeneralUtils', + 'ComponentInstanceFactory', + 'CompositionGraphNodesUtils', + 'EventListenerService', + 'ComponentFactory', + 'LoaderService', + 'CommonGraphUtils', + 'MatchCapabilitiesRequirementsUtils' + ]; +}
\ No newline at end of file diff --git a/catalog-ui/app/scripts/directives/graphs-v2/composition-graph/composition-graph.html b/catalog-ui/app/scripts/directives/graphs-v2/composition-graph/composition-graph.html new file mode 100644 index 0000000000..5f2c488341 --- /dev/null +++ b/catalog-ui/app/scripts/directives/graphs-v2/composition-graph/composition-graph.html @@ -0,0 +1,22 @@ +<loader display="isLoading" loader-type="composition-graph"></loader> +<div class="sdc-composition-graph-wrapper" ng-class="{'view-only':isViewOnly}" + data-drop="true" + data-jqyoui-options="{accept: verifyDrop}" + data-jqyoui-droppable="{onDrop:'dropCallback', beforeDrop: 'beforeDropCallback'}"> +</div> + +<relation-menu relation-menu-directive-obj="relationMenuDirectiveObj" is-link-menu-open="isLinkMenuOpen" + create-relation="createLinkFromMenu" cancel="cancelRelationMenu()"></relation-menu> + + +<div class="w-sdc-canvas-menu" + data-ng-show="linkMenuObject" ng-style="{left: linkMenuObject.position.x, top: linkMenuObject.position.y}" + id="relationMenu"> + + <div class="w-sdc-canvas-menu-content hand" data-ng-click="deleteRelation(linkMenuObject.link)"> + <div class="w-sdc-canvas-menu-content-delete-button"></div> + <!--{{relationComponent.data.relation.relationships[0].relationship.type | relationName }}--> + Delete + </div> + +</div> diff --git a/catalog-ui/app/scripts/directives/graphs-v2/composition-graph/composition-graph.less b/catalog-ui/app/scripts/directives/graphs-v2/composition-graph/composition-graph.less new file mode 100644 index 0000000000..7b999967b7 --- /dev/null +++ b/catalog-ui/app/scripts/directives/graphs-v2/composition-graph/composition-graph.less @@ -0,0 +1,14 @@ +composition-graph { + display: block; + + height:100%; + width: 100%; + .sdc-composition-graph-wrapper{ + height:100%; + width: 100%; + } + + &.view-only{ + background-color:rgb(248, 248, 248); + } +}
\ No newline at end of file diff --git a/catalog-ui/app/scripts/directives/graphs-v2/composition-graph/utils/composition-graph-general-utils.ts b/catalog-ui/app/scripts/directives/graphs-v2/composition-graph/utils/composition-graph-general-utils.ts new file mode 100644 index 0000000000..495a243d75 --- /dev/null +++ b/catalog-ui/app/scripts/directives/graphs-v2/composition-graph/utils/composition-graph-general-utils.ts @@ -0,0 +1,243 @@ +/// <reference path="../../../../references"/> +module Sdc.Graph.Utils { + + import Dictionary = Sdc.Utils.Dictionary; + + export class CompositionGraphGeneralUtils { + + public componentRequirementsAndCapabilitiesCaching = new Dictionary<string, Models.Components.Component>(); + protected static graphUtilsUpdateQueue: Sdc.Utils.Functions.QueueUtils; + + constructor(private $q: ng.IQService, + private LoaderService: Services.LoaderService, + private commonGraphUtils: Sdc.Graph.Utils.CommonGraphUtils, + private matchCapabilitiesRequirementsUtils: Graph.Utils.MatchCapabilitiesRequirementsUtils) { + CompositionGraphGeneralUtils.graphUtilsUpdateQueue = new Sdc.Utils.Functions.QueueUtils(this.$q); + } + + + /** + * Get the offset for the link creation Menu + * @param point + * @returns {Cy.Position} + */ + public calcMenuOffset: Function = (point: Cy.Position): Cy.Position => { + point.x = point.x + 60; + point.y = point.y + 105; + return point; + }; + + /** + * return the top left position of the link menu + * @param cy + * @param targetNodePosition + * @returns {Cy.Position} + */ + public getLinkMenuPosition = (cy: Cy.Instance, targetNodePosition: Cy.Position) => { + let menuPosition: Cy.Position = this.calcMenuOffset(targetNodePosition); //get the link mid point + if (document.body.scrollHeight < menuPosition.y + Sdc.Utils.Constants.GraphUIObjects.LINK_MENU_HEIGHT + $(document.getElementsByClassName('sdc-composition-graph-wrapper')).offset().top) { // if position menu is overflow bottom + menuPosition.y = document.body.scrollHeight - Sdc.Utils.Constants.GraphUIObjects.TOP_HEADER_HEIGHT - Sdc.Utils.Constants.GraphUIObjects.LINK_MENU_HEIGHT; + } + return menuPosition; + }; + + + /** + * will return true/false if two nodes overlapping + * + * @param graph node + */ + private isNodesOverlapping(node: Cy.CollectionFirstNode, draggedNode: Cy.CollectionFirstNode): boolean { + + let nodeBoundingBox: Cy.BoundingBox = node.renderedBoundingBox(); + let secondNodeBoundingBox: Cy.BoundingBox = draggedNode.renderedBoundingBox(); + + return this.isBBoxOverlapping(nodeBoundingBox, secondNodeBoundingBox); + } + + /** + * Checks whether the bounding boxes of two nodes are overlapping on any side + * @param nodeOneBBox + * @param nodeTwoBBox + * @returns {boolean} + */ + private isBBoxOverlapping(nodeOneBBox: Cy.BoundingBox, nodeTwoBBox: Cy.BoundingBox) { + return (((nodeOneBBox.x1 < nodeTwoBBox.x1 && nodeOneBBox.x2 > nodeTwoBBox.x1) || + (nodeOneBBox.x1 < nodeTwoBBox.x2 && nodeOneBBox.x2 > nodeTwoBBox.x2) || + (nodeTwoBBox.x1 < nodeOneBBox.x1 && nodeTwoBBox.x2 > nodeOneBBox.x2)) && + ((nodeOneBBox.y1 < nodeTwoBBox.y1 && nodeOneBBox.y2 > nodeTwoBBox.y1) || + (nodeOneBBox.y1 < nodeTwoBBox.y2 && nodeOneBBox.y2 > nodeTwoBBox.y2) || + (nodeTwoBBox.y1 < nodeOneBBox.y1 && nodeTwoBBox.y2 > nodeOneBBox.y2))) + } + + + /** + * Checks whether a specific component instance can be hosted on the UCPE instance + * @param cy - Cytoscape instance + * @param fromUcpeInstance + * @param toComponentInstance + * @returns {Models.MatchReqToCapability} + */ + public canBeHostedOn(cy: Cy.Instance, fromUcpeInstance: Models.ComponentsInstances.ComponentInstance, toComponentInstance: Models.ComponentsInstances.ComponentInstance): Models.MatchReqToCapability { + + let matches: Array<Models.MatchBase> = this.matchCapabilitiesRequirementsUtils.getMatchedRequirementsCapabilities(fromUcpeInstance, toComponentInstance, this.getAllCompositionCiLinks(cy)); + let hostedOnMatch: Models.MatchBase = _.find(matches, (match: Models.MatchReqToCapability) => { + return match.requirement.capability.toLowerCase() === 'tosca.capabilities.container'; + }); + + return <Models.MatchReqToCapability>hostedOnMatch; + }; + + + /** + * Checks whether node can be dropped into UCPE + * @param cy + * @param nodeToInsert + * @param ucpeNode + * @returns {boolean} + */ + private isValidDropInsideUCPE(cy: Cy.Instance, nodeToInsert: Models.ComponentsInstances.ComponentInstance, ucpeNode: Models.ComponentsInstances.ComponentInstance): boolean { + + let hostedOnMatch: Models.MatchReqToCapability = this.canBeHostedOn(cy, ucpeNode, nodeToInsert); + let result: boolean = !angular.isUndefined(hostedOnMatch) || nodeToInsert.isVl(); //group validation + return result; + + }; + + + /** + * For drops from palette, checks whether the node can be dropped. If node is being held over another node, check if capable of hosting + * @param cy + * @param pseudoNodeBBox + * @param paletteComponentInstance + * @returns {boolean} + */ + public isPaletteDropValid(cy: Cy.Instance, pseudoNodeBBox: Cy.BoundingBox, paletteComponentInstance:Sdc.Models.ComponentsInstances.ComponentInstance) { + + let componentIsUCPE:boolean = (paletteComponentInstance.capabilities && paletteComponentInstance.capabilities['tosca.capabilities.Container'] && paletteComponentInstance.name.toLowerCase().indexOf('ucpe') > -1); + + if(componentIsUCPE && cy.nodes('[?isUcpe]').length > 0) { //second UCPE not allowed + return false; + } + + let illegalOverlappingNodes = _.filter(cy.nodes("[isSdcElement]"), (graphNode: Cy.CollectionFirstNode) => { + + if(this.isBBoxOverlapping(pseudoNodeBBox, graphNode.renderedBoundingBox())){ + if (!componentIsUCPE && graphNode.data().isUcpe) { + return !this.isValidDropInsideUCPE(cy, paletteComponentInstance, graphNode.data().componentInstance); //if this is valid insert into ucpe, we return false - no illegal overlapping nodes + } + return true; + } + + return false; + }); + + return illegalOverlappingNodes.length === 0; + } + + /** + * will return true/false if a drop of a single node is valid + * + * @param graph node + */ + public isValidDrop(cy: Cy.Instance, draggedNode: Cy.CollectionFirstNode): boolean { + + let illegalOverlappingNodes = _.filter(cy.nodes("[isSdcElement]"), (graphNode: Cy.CollectionFirstNode) => { //all sdc nodes, removing child nodes (childe node allways collaps + + if (draggedNode.data().isUcpe && (graphNode.isChild() || graphNode.data().isInsideGroup)) { //ucpe cps always inside ucpe, no overlapping + return false; + } + if(draggedNode.data().isInsideGroup && (!draggedNode.active() || graphNode.data().isUcpe)) { + return false; + } + + if (!draggedNode.data().isUcpe && !(draggedNode.data() instanceof Sdc.Models.Graph.CompositionCiNodeUcpeCp) && graphNode.data().isUcpe) { //case we are dragging a node into UCPE + let isEntirelyInUCPE:boolean = this.commonGraphUtils.isFirstBoxContainsInSecondBox(draggedNode.renderedBoundingBox(), graphNode.renderedBoundingBox()); + if (isEntirelyInUCPE){ + if(this.isValidDropInsideUCPE(cy, draggedNode.data().componentInstance, graphNode.data().componentInstance)){ //if this is valid insert into ucpe, we return false - no illegal overlapping nodes + return false; + } + } + } + return graphNode.data().id !== draggedNode.data().id && this.isNodesOverlapping(draggedNode, graphNode); + + }); + // return false; + return illegalOverlappingNodes.length === 0; + }; + + /** + * will return true/false if the move of the nodes is valid (no node overlapping and verifying if insert into UCPE is valid) + * + * @param nodesArray - the selected drags nodes + */ + public isGroupValidDrop(cy: Cy.Instance, nodesArray: Cy.CollectionNodes): boolean { + var filterDraggedNodes = nodesArray.filter('[?isDraggable]'); + let isValidDrop = _.every(filterDraggedNodes, (node: Cy.CollectionFirstNode) => { + return this.isValidDrop(cy, node); + + }); + return isValidDrop; + }; + + /** + * get all links in diagram + * @param cy + * @returns {any[]|boolean[]} + */ + public getAllCompositionCiLinks = (cy: Cy.Instance): Array<Models.CompositionCiLinkBase> => { + return _.map(cy.edges("[isSdcElement]"), (edge: Cy.CollectionEdges) => { + return edge.data(); + }); + }; + + + /** + * Get Graph Utils server queue + * @returns {Sdc.Utils.Functions.QueueUtils} + */ + public getGraphUtilsServerUpdateQueue(): Sdc.Utils.Functions.QueueUtils { + return CompositionGraphGeneralUtils.graphUtilsUpdateQueue; + } + ; + + /** + * + * @param blockAction - true/false if this is a block action + * @param instances + * @param component + */ + public pushMultipleUpdateComponentInstancesRequestToQueue = (blockAction: boolean, instances: Array<Models.ComponentsInstances.ComponentInstance>, component: Models.Components.Component): void => { + if (blockAction) { + this.getGraphUtilsServerUpdateQueue().addBlockingUIAction( + () => component.updateMultipleComponentInstances(instances) + ); + } else { + this.getGraphUtilsServerUpdateQueue().addNonBlockingUIAction( + () => component.updateMultipleComponentInstances(instances), + () => this.LoaderService.hideLoader('composition-graph')); + } + }; + + /** + * this function will update component instance data + * @param blockAction - true/false if this is a block action + * @param updatedInstance + */ + public pushUpdateComponentInstanceActionToQueue = (component: Models.Components.Component, blockAction: boolean, updatedInstance: Models.ComponentsInstances.ComponentInstance): void => { + + if (blockAction) { + this.LoaderService.showLoader('composition-graph'); + this.getGraphUtilsServerUpdateQueue().addBlockingUIAction( + () => component.updateComponentInstance(updatedInstance) + ); + } else { + this.getGraphUtilsServerUpdateQueue().addNonBlockingUIAction( + () => component.updateComponentInstance(updatedInstance), + () => this.LoaderService.hideLoader('composition-graph')); + } + }; + } + + CompositionGraphGeneralUtils.$inject = ['$q', 'LoaderService', 'CommonGraphUtils', 'MatchCapabilitiesRequirementsUtils']; +}
\ No newline at end of file diff --git a/catalog-ui/app/scripts/directives/graphs-v2/composition-graph/utils/composition-graph-links-utils.ts b/catalog-ui/app/scripts/directives/graphs-v2/composition-graph/utils/composition-graph-links-utils.ts new file mode 100644 index 0000000000..602e6b6def --- /dev/null +++ b/catalog-ui/app/scripts/directives/graphs-v2/composition-graph/utils/composition-graph-links-utils.ts @@ -0,0 +1,347 @@ +/** + * Created by obarda on 6/28/2016. + */ +/// <reference path="../../../../references"/> +module Sdc.Graph.Utils { + + import ImageCreatorService = Sdc.Utils.ImageCreatorService; + import Module = Sdc.Models.Module; + export class CompositionGraphLinkUtils { + + private p2pVL:Models.Components.Component; + private mp2mpVL:Models.Components.Component; + + constructor(private linksFactory:Sdc.Utils.LinksFactory, + private loaderService:Services.LoaderService, + private generalGraphUtils:Sdc.Graph.Utils.CompositionGraphGeneralUtils, + private leftPaletteLoaderService:Services.Components.LeftPaletteLoaderService, + private componentInstanceFactory:Sdc.Utils.ComponentInstanceFactory, + private nodesFactory:Sdc.Utils.NodesFactory, + private commonGraphUtils: Sdc.Graph.Utils.CommonGraphUtils, + private matchCapabilitiesRequirementsUtils: Graph.Utils.MatchCapabilitiesRequirementsUtils) { + + this.initScopeVls(); + + } + + + /** + * Delete the link on server and then remove it from graph + * @param component + * @param releaseLoading - true/false release the loader when finished + * @param link - the link to delete + */ + public deleteLink = (cy:Cy.Instance, component:Models.Components.Component, releaseLoading:boolean, link:Cy.CollectionEdges) => { + + this.loaderService.showLoader('composition-graph'); + let onSuccessDeleteRelation = (response) => { + cy.remove(link); + }; + + if (!releaseLoading) { + this.generalGraphUtils.getGraphUtilsServerUpdateQueue().addBlockingUIAction( + () => component.deleteRelation(link.data().relation).then(onSuccessDeleteRelation) + ); + } else { + this.generalGraphUtils.getGraphUtilsServerUpdateQueue().addBlockingUIActionWithReleaseCallback( + () => component.deleteRelation(link.data().relation).then(onSuccessDeleteRelation), + () => this.loaderService.hideLoader('composition-graph')); + } + }; + + /** + * create the link on server and than draw it on graph + * @param link - the link to create + * @param cy + * @param component + */ + public createLink = (link:Models.CompositionCiLinkBase, cy:Cy.Instance, component:Models.Components.Component):void => { + + this.loaderService.showLoader('composition-graph'); + + let onSuccess:(response:Models.RelationshipModel) => void = (relation:Models.RelationshipModel) => { + link.setRelation(relation); + this.commonGraphUtils.insertLinkToGraph(cy, link); + }; + + link.updateLinkDirection(); + + this.generalGraphUtils.getGraphUtilsServerUpdateQueue().addBlockingUIActionWithReleaseCallback( + () => component.createRelation(link.relation).then(onSuccess), + () => this.loaderService.hideLoader('composition-graph') + ); + }; + + + public initScopeVls = ():void => { + + let vls = this.leftPaletteLoaderService.getFullDataComponentList(Sdc.Utils.Constants.ResourceType.VL); + vls.forEach((item) => { + let key = _.find(Object.keys(item.capabilities), (key) => { + return _.includes(key.toLowerCase(), 'linkable'); + }); + let linkable = item.capabilities[key]; + if (linkable) { + if (linkable[0].maxOccurrences == '2') { + this.p2pVL = _.find(vls, (component:Models.Components.Component) => { + return component.uniqueId === item.uniqueId; + }); + + } else {//assuming unbounded occurrences + this.mp2mpVL = _.find(vls, (component:Models.Components.Component) => { + return component.uniqueId === item.uniqueId; + }); + } + } + }); + }; + + private setVLlinks = (match:Models.MatchReqToReq, vl:Models.ComponentsInstances.ComponentInstance):Array<Models.RelationshipModel> => { + + let relationship1 = new Models.Relationship(); + let relationship2 = new Models.Relationship(); + let newRelationshipModel1 = new Models.RelationshipModel(); + let newRelationshipModel2 = new Models.RelationshipModel(); + + let capability:Models.Capability = vl.capabilities.findValueByKey('linkable')[0]; + relationship1.setRelationProperties(capability, match.requirement); + relationship2.setRelationProperties(capability, match.secondRequirement); + + newRelationshipModel1.setRelationshipModelParams(match.fromNode, vl.uniqueId, [relationship1]); + newRelationshipModel2.setRelationshipModelParams(match.toNode, vl.uniqueId, [relationship2]); + + return [newRelationshipModel1, newRelationshipModel2]; + }; + + private createVlinks = (cy:Cy.Instance, component:Models.Components.Component, matchReqToReq:Models.MatchReqToReq, vl:Models.Components.Component):void => { + + let componentInstance:Models.ComponentsInstances.ComponentInstance = this.componentInstanceFactory.createComponentInstanceFromComponent(vl); + let fromNodePosition:Cy.Position = cy.getElementById(matchReqToReq.fromNode).relativePosition(); + let toNodePosition:Cy.Position = cy.getElementById(matchReqToReq.toNode).relativePosition(); + let location:Cy.Position = { + x: 0.5 * (fromNodePosition.x + toNodePosition.x), + y: 0.5 * (fromNodePosition.y + toNodePosition.y) + } + + componentInstance.posX = location.x; + componentInstance.posY = location.y; + + let onFailed:(error:any) => void = (error:any) => { + this.loaderService.hideLoader('composition-graph'); + console.info('onFailed', error); + }; + + let onSuccess = (response:Models.ComponentsInstances.ComponentInstance):void => { + + console.info('onSuccses', response); + response.requirements = new Models.RequirementsGroup(vl.requirements); + response.capabilities = new Models.CapabilitiesGroup(vl.capabilities); + response.componentVersion = vl.version; + response.setInstanceRC(); + + let newLinks = this.setVLlinks(matchReqToReq, response); + let newNode = this.nodesFactory.createNode(response); + + this.commonGraphUtils.addComponentInstanceNodeToGraph(cy, newNode); + + _.forEach(newLinks, (link) => { + let linkObg:Models.CompositionCiLinkBase = this.linksFactory.createGraphLink(cy, link, link.relationships[0]); + this.createLink(linkObg, cy, component); + }); + }; + component.createComponentInstance(componentInstance).then(onSuccess, onFailed); + }; + + private createSimpleLink = (match:Models.MatchReqToCapability, cy:Cy.Instance, component:Models.Components.Component):void => { + let newRelation:Models.RelationshipModel = match.matchToRelationModel(); + let linkObg:Models.CompositionCiLinkBase = this.linksFactory.createGraphLink(cy,newRelation, newRelation.relationships[0]); + this.createLink(linkObg, cy, component); + }; + + public createLinkFromMenu = (cy:Cy.Instance, chosenMatch:Models.MatchBase, vl:Models.Components.Component, component:Models.Components.Component):void => { + + if (chosenMatch) { + if (chosenMatch && chosenMatch instanceof Models.MatchReqToReq) { + this.createVlinks(cy, component, chosenMatch, vl); //TODO orit implement + } + if (chosenMatch && chosenMatch instanceof Models.MatchReqToCapability) { + this.createSimpleLink(chosenMatch, cy, component); + } + } + }; + + + /** + * Filters the matches for UCPE links so that shown requirements and capabilites are only related to the selected ucpe-cp + * @param fromNode + * @param toNode + * @param matchesArray + * @returns {Array<Models.MatchBase>} + */ + public filterUcpeLinks(fromNode: Models.Graph.CompositionCiNodeBase, toNode: Models.Graph.CompositionCiNodeBase, matchesArray: Array<Models.MatchBase>): any { + + let matchLink: Array<Models.MatchBase>; + + if (fromNode.isUcpePart) { + matchLink = _.filter(matchesArray, (match: Models.MatchBase) => { + return match.isOwner(fromNode.id); + }); + } + + if (toNode.isUcpePart) { + matchLink = _.filter(matchesArray, (match: Models.MatchBase) => { + return match.isOwner(toNode.id); + }); + } + return matchLink ? matchLink : matchesArray; + } + + + /** + * open the connect link menu if the link drawn is valid - match requirements & capabilities + * @param cy + * @param fromNode + * @param toNode + * @returns {any} + */ + public onLinkDrawn(cy:Cy.Instance, fromNode:Cy.CollectionFirstNode, toNode:Cy.CollectionFirstNode):Models.RelationMenuDirectiveObj { + + if(!this.commonGraphUtils.nodeLocationsCompatible(cy, fromNode, toNode)){ return null; } + let linkModel:Array<Models.CompositionCiLinkBase> = this.generalGraphUtils.getAllCompositionCiLinks(cy); + + let possibleRelations:Array<Models.MatchBase> = this.matchCapabilitiesRequirementsUtils.getMatchedRequirementsCapabilities(fromNode.data().componentInstance, + toNode.data().componentInstance, linkModel, this.mp2mpVL); //TODO orit - add p2p and mp2mp + + //filter relations found to limit to specific ucpe-cp + possibleRelations = this.filterUcpeLinks(fromNode.data(), toNode.data(), possibleRelations); + + //if found possibleRelations between the nodes we create relation menu directive and open the link menu + if (possibleRelations.length) { + let menuPosition = this.generalGraphUtils.getLinkMenuPosition(cy, toNode.renderedPoint()); + return new Models.RelationMenuDirectiveObj(fromNode.data(), toNode.data(), this.mp2mpVL, this.p2pVL, menuPosition, possibleRelations); + } + return null; + }; + + + /** + * when we drag instance in to UCPE or out of UCPE - get all links we need to delete - one node in ucpe and one node outside of ucpe + * @param node - the node we dragged into or out of the ucpe + */ + public deleteLinksWhenNodeMovedFromOrToUCPE(component:Models.Components.Component, cy:Cy.Instance, nodeMoved:Cy.CollectionNodes, vlsPendingDeletion?:Cy.CollectionNodes):void { + + + let linksToDelete:Cy.CollectionElements = cy.collection(); + _.forEach(nodeMoved.neighborhood('node'), (neighborNode)=>{ + + if(neighborNode.data().isUcpePart){ //existing connections to ucpe or ucpe-cp - we want to delete even though nodeLocationsCompatible will technically return true + linksToDelete = linksToDelete.add(nodeMoved.edgesWith(neighborNode)); // This will delete the ucpe-host-link, or the vl-ucpe-link if nodeMoved is vl + } else if(!this.commonGraphUtils.nodeLocationsCompatible(cy, nodeMoved, neighborNode)){ //connection to regular node or vl - check if locations are compatible + if(!vlsPendingDeletion || !vlsPendingDeletion.intersect(neighborNode).length){ //Check if this is a link to a VL pending deletion, to prevent double deletion of between the node moved and vl + linksToDelete = linksToDelete.add(nodeMoved.edgesWith(neighborNode)); + } + } + }); + + + + linksToDelete.each((i, link)=>{ + this.deleteLink(cy, component, false, link); + }); + + }; + + + /** + * Creates a hostedOn link between a VF and UCPE + * @param component + * @param cy + * @param ucpeNode + * @param vfNode + */ + public createVfToUcpeLink = (component: Models.Components.Component, cy:Cy.Instance, ucpeNode:Models.Graph.NodeUcpe, vfNode:Models.Graph.CompositionCiNodeVf):void => { + let hostedOnMatch:Models.MatchReqToCapability = this.generalGraphUtils.canBeHostedOn(cy, ucpeNode.componentInstance, vfNode.componentInstance); + /* create relation */ + let newRelation = new Models.RelationshipModel(); + newRelation.fromNode = ucpeNode.id; + newRelation.toNode = vfNode.id; + + let link:Models.CompositionCiLinkBase = this.linksFactory.createUcpeHostLink(newRelation); + link.relation = hostedOnMatch.matchToRelationModel(); + this.createLink(link, cy, component); + }; + + + /** + * Handles click event on links. + * If one edge selected: do nothing. + /*Two edges selected - always select all + /* Three or more edges: first click - select all, secondary click - select single. + * @param cy + * @param event + */ + public handleLinkClick(cy:Cy.Instance, event : Cy.EventObject) { + if(cy.$('edge:selected').length > 2 && event.cyTarget[0].selected()) { + cy.$(':selected').unselect(); + } else { + + let vl: Cy.CollectionNodes = event.cyTarget[0].target('.vl-node'); + let connectedEdges:Cy.CollectionEdges = vl.connectedEdges(); + if (vl.length && connectedEdges.length > 1) { + + setTimeout(() => { + vl.select(); + connectedEdges.select(); + }, 0); + } + } + + } + + + /** + * Calculates the position for the menu that modifies an existing link + * @param event + * @param elementWidth + * @param elementHeight + * @returns {Sdc.Models.Graph.Point} + */ + public calculateLinkMenuPosition(event, elementWidth, elementHeight): Sdc.Models.Graph.Point { + let point: Sdc.Models.Graph.Point = new Sdc.Models.Graph.Point(event.originalEvent.x,event.originalEvent.y); + if(event.originalEvent.view.screen.height-elementHeight<point.y){ + point.y = event.originalEvent.view.screen.height-elementHeight; + } + if(event.originalEvent.view.screen.width-elementWidth<point.x){ + point.x = event.originalEvent.view.screen.width-elementWidth; + } + return point; + }; + + + /** + * Gets the menu that is displayed when you click an existing link. + * @param link + * @param event + * @returns {Models.LinkMenu} + */ + public getModifyLinkMenu(link:Cy.CollectionFirstEdge, event:Cy.EventObject):Models.LinkMenu{ + let point:Sdc.Models.Graph.Point = this.calculateLinkMenuPosition(event,Sdc.Utils.Constants.GraphUIObjects.MENU_LINK_VL_WIDTH_OFFSET,Sdc.Utils.Constants.GraphUIObjects.MENU_LINK_VL_HEIGHT_OFFSET); + let menu:Models.LinkMenu = new Models.LinkMenu(point, true, link); + return menu; + }; + + } + + + + CompositionGraphLinkUtils.$inject = [ + 'LinksFactory', + 'LoaderService', + 'CompositionGraphGeneralUtils', + 'LeftPaletteLoaderService', + 'ComponentInstanceFactory', + 'NodesFactory', + 'CommonGraphUtils', + 'MatchCapabilitiesRequirementsUtils' + ]; +}
\ No newline at end of file diff --git a/catalog-ui/app/scripts/directives/graphs-v2/composition-graph/utils/composition-graph-nodes-utils.ts b/catalog-ui/app/scripts/directives/graphs-v2/composition-graph/utils/composition-graph-nodes-utils.ts new file mode 100644 index 0000000000..95c31d16b1 --- /dev/null +++ b/catalog-ui/app/scripts/directives/graphs-v2/composition-graph/utils/composition-graph-nodes-utils.ts @@ -0,0 +1,220 @@ +/** + * Created by obarda on 11/9/2016. + */ + +/// <reference path="../../../../references"/> +module Sdc.Graph.Utils { + + export class CompositionGraphNodesUtils { + constructor(private NodesFactory:Sdc.Utils.NodesFactory, private $log:ng.ILogService, + private GeneralGraphUtils:Graph.Utils.CompositionGraphGeneralUtils, + private commonGraphUtils: Sdc.Graph.Utils.CommonGraphUtils, + private eventListenerService: Services.EventListenerService, + private loaderService:Services.LoaderService) { + + } + + /** + * Returns component instances for all nodes passed in + * @param nodes - Cy nodes + * @returns {any[]} + */ + public getAllNodesData(nodes:Cy.CollectionNodes) { + return _.map(nodes, (node:Cy.CollectionFirstNode)=> { + return node.data(); + }) + }; + + /** + * Deletes component instances on server and then removes it from the graph as well + * @param cy + * @param component + * @param nodeToDelete + */ + public deleteNode(cy: Cy.Instance, component:Models.Components.Component, nodeToDelete:Cy.CollectionNodes):void { + + this.loaderService.showLoader('composition-graph'); + let onSuccess:(response:Models.ComponentsInstances.ComponentInstance) => void = (response:Models.ComponentsInstances.ComponentInstance) => { + console.info('onSuccess', response); + + //if node to delete is a UCPE, remove all children (except UCPE-CPs) and remove their "hostedOn" links + if (nodeToDelete.data().isUcpe){ + _.each(cy.nodes('[?isInsideGroup]'), (node)=>{ + this.eventListenerService.notifyObservers(Sdc.Utils.Constants.GRAPH_EVENTS.ON_REMOVE_NODE_FROM_UCPE, node, nodeToDelete); + }); + } + + //check whether the node is connected to any VLs that only have one other connection. If so, delete that VL as well + if(!(nodeToDelete.data() instanceof Sdc.Models.Graph.CompositionCiNodeVl)){ + let connectedVls:Array<Cy.CollectionFirstNode> = this.getConnectedVlToNode(nodeToDelete); + this.handleConnectedVlsToDelete(connectedVls); + } + + //update UI + cy.remove(nodeToDelete); + + }; + + let onFailed:(response:any) => void = (response:any) => { + console.info('onFailed', response); + }; + + + this.GeneralGraphUtils.getGraphUtilsServerUpdateQueue().addBlockingUIActionWithReleaseCallback( + () => component.deleteComponentInstance(nodeToDelete.data().componentInstance.uniqueId).then(onSuccess, onFailed), + () => this.loaderService.hideLoader('composition-graph') + ); + + }; + + + /** + * Finds all VLs connected to a single node + * @param node + * @returns {Array<Cy.CollectionFirstNode>} + */ + public getConnectedVlToNode = (node: Cy.CollectionNodes): Array<Cy.CollectionFirstNode> => { + let connectedVls: Array<Cy.CollectionFirstNode> = new Array<Cy.CollectionFirstNode>(); + _.forEach(node.connectedEdges().connectedNodes(), (node: Cy.CollectionFirstNode) => { + if (node.data() instanceof Models.Graph.CompositionCiNodeVl) { + connectedVls.push(node); + } + }); + return connectedVls; + }; + + + /** + * Delete all VLs that have only two connected nodes (this function is called when deleting a node) + * @param connectedVls + */ + public handleConnectedVlsToDelete = (connectedVls: Array<Cy.CollectionFirstNode>) => { + _.forEach(connectedVls, (vlToDelete: Cy.CollectionNodes) => { + + if (vlToDelete.connectedEdges().length === 2) { // if vl connected only to 2 nodes need to delete the vl + this.eventListenerService.notifyObservers(Sdc.Utils.Constants.GRAPH_EVENTS.ON_DELETE_COMPONENT_INSTANCE, vlToDelete.data().componentInstance); + } + }); + }; + + + /** + * This function is called when moving a node in or out of UCPE. + * Deletes all connected VLs that have less than 2 valid connections remaining after the move + * Returns the collection of vls that are in the process of deletion (async) to prevent duplicate calls while deletion is in progress + * @param component + * @param cy + * @param node - node that was moved in/out of ucpe + */ + public deleteNodeVLsUponMoveToOrFromUCPE = (component:Models.Components.Component, cy:Cy.Instance, node:Cy.CollectionNodes):Cy.CollectionNodes =>{ + if(node.data() instanceof Models.Graph.CompositionCiNodeVl){ return;} + + let connectedVLsToDelete:Cy.CollectionNodes = cy.collection(); + _.forEach(node.neighborhood('node'), (connectedNode) => { + + //Find all neighboring nodes that are VLs + if(connectedNode.data() instanceof Models.Graph.CompositionCiNodeVl){ + + //check VL's neighbors to see if it has 2 or more nodes whose location is compatible with VL (regardless of whether VL is in or out of UCPE) + let compatibleNodeCount = 0; + let vlNeighborhood = connectedNode.neighborhood('node'); + _.forEach(vlNeighborhood, (vlNeighborNode)=>{ + if(this.commonGraphUtils.nodeLocationsCompatible(cy, connectedNode, vlNeighborNode)) { + compatibleNodeCount ++; + } + }); + + if(compatibleNodeCount < 2) { + connectedVLsToDelete = connectedVLsToDelete.add(connectedNode); + } + } + }); + + connectedVLsToDelete.each((i, vlToDelete:Cy.CollectionNodes)=>{ + this.deleteNode(cy, component, vlToDelete); + }); + return connectedVLsToDelete; + }; + + /** + * This function will update nodes position. if the new position is into or out of ucpe, the node will trigger the ucpe events + * @param cy + * @param component + * @param nodesMoved - the node/multiple nodes now moved by the user + */ + public onNodesPositionChanged = (cy: Cy.Instance, component:Models.Components.Component, nodesMoved: Cy.CollectionNodes): void => { + + if (nodesMoved.length === 0) { + return; + } + + let isValidMove:boolean = this.GeneralGraphUtils.isGroupValidDrop(cy, nodesMoved); + if (isValidMove) { + + this.$log.debug(`composition-graph::ValidDrop:: updating node position`); + let instancesToUpdateInNonBlockingAction:Array<Models.ComponentsInstances.ComponentInstance> = new Array<Models.ComponentsInstances.ComponentInstance>(); + + _.each(nodesMoved, (node:Cy.CollectionFirstNode)=> { //update all nodes new position + + if(node.data().isUcpePart && !node.data().isUcpe){ return; }//No need to update UCPE-CPs + + //update position + let newPosition:Cy.Position = this.commonGraphUtils.getNodePosition(node); + node.data().componentInstance.updatePosition(newPosition.x, newPosition.y); + + //check if node moved to or from UCPE + let ucpe = this.commonGraphUtils.isInUcpe(node.cy(), node.boundingbox()); + if(node.data().isInsideGroup || ucpe.length) { + this.handleUcpeChildMove(node, ucpe, instancesToUpdateInNonBlockingAction); + } else { + instancesToUpdateInNonBlockingAction.push(node.data().componentInstance); + } + + }); + + if (instancesToUpdateInNonBlockingAction.length > 0) { + this.GeneralGraphUtils.pushMultipleUpdateComponentInstancesRequestToQueue(false, instancesToUpdateInNonBlockingAction, component); + } + } else { + this.$log.debug(`composition-graph::notValidDrop:: node return to latest position`); + //reset nodes position + nodesMoved.positions((i, node) => { + return { + x: +node.data().componentInstance.posX, + y: +node.data().componentInstance.posY + }; + }) + } + + this.GeneralGraphUtils.getGraphUtilsServerUpdateQueue().addBlockingUIActionWithReleaseCallback(() => { + }, () => { + this.loaderService.hideLoader('composition-graph'); + }); + + }; + + /** + * Checks whether the node has been added or removed from UCPE and triggers appropriate events + * @param node - node moved + * @param ucpeContainer - UCPE container that the node has been moved to. When moving a node out of ucpe, param will be empty + * @param instancesToUpdateInNonBlockingAction + */ + public handleUcpeChildMove(node:Cy.CollectionFirstNode, ucpeContainer:Cy.CollectionElements, instancesToUpdateInNonBlockingAction:Array<Models.ComponentsInstances.ComponentInstance>){ + + if(node.data().isInsideGroup){ + if(ucpeContainer.length){ //moving node within UCPE. Simply update position + this.commonGraphUtils.updateUcpeChildPosition(<Cy.CollectionNodes>node, ucpeContainer); + instancesToUpdateInNonBlockingAction.push(node.data().componentInstance); + } else { //removing node from UCPE. Notify observers + this.eventListenerService.notifyObservers(Sdc.Utils.Constants.GRAPH_EVENTS.ON_REMOVE_NODE_FROM_UCPE, node, ucpeContainer); + } + } else if(!node.data().isInsideGroup && ucpeContainer.length && !node.data().isUcpePart){ //adding node to UCPE + this.eventListenerService.notifyObservers(Sdc.Utils.Constants.GRAPH_EVENTS.ON_INSERT_NODE_TO_UCPE, node, ucpeContainer, true); + } + } + + } + + + CompositionGraphNodesUtils.$inject = ['NodesFactory', '$log', 'CompositionGraphGeneralUtils', 'CommonGraphUtils', 'EventListenerService', 'LoaderService']; +}
\ No newline at end of file diff --git a/catalog-ui/app/scripts/directives/graphs-v2/composition-graph/utils/match-capability-requierment-utils.ts b/catalog-ui/app/scripts/directives/graphs-v2/composition-graph/utils/match-capability-requierment-utils.ts new file mode 100644 index 0000000000..5a401df317 --- /dev/null +++ b/catalog-ui/app/scripts/directives/graphs-v2/composition-graph/utils/match-capability-requierment-utils.ts @@ -0,0 +1,265 @@ +/** + * Created by obarda on 1/1/2017. + */ +/// <reference path="../../../../references"/> +module Sdc.Graph.Utils { + + export class MatchCapabilitiesRequirementsUtils { + + constructor() { + } + + + + public static linkable(requirement1:Models.Requirement, requirement2:Models.Requirement, vlCapability:Models.Capability):boolean { + return MatchCapabilitiesRequirementsUtils.isMatch(requirement1, vlCapability) && MatchCapabilitiesRequirementsUtils.isMatch(requirement2, vlCapability); + }; + + + /** + * Shows + icon in corner of each node passed in + * @param filteredNodesData + * @param cy + */ + public highlightMatchingComponents(filteredNodesData, cy:Cy.Instance) { + _.each(filteredNodesData, (data:any) => { + let node = cy.getElementById(data.id); + cy.emit('showhandle', [node]); + }); + } + + /** + * Adds opacity to each node that cannot be linked to hovered node + * @param filteredNodesData + * @param nodesData + * @param cy + * @param hoveredNodeData + */ + public fadeNonMachingComponents(filteredNodesData, nodesData, cy:Cy.Instance, hoveredNodeData?) { + let fadeNodes = _.xorWith(nodesData, filteredNodesData, (node1, node2) => { + return node1.id === node2.id; + }); + if (hoveredNodeData) { + _.remove(fadeNodes, hoveredNodeData); + } + cy.batch(()=> { + _.each(fadeNodes, (node) => { + cy.getElementById(node.id).style({'background-image-opacity': 0.4}); + }); + }) + } + + /** + * Resets all nodes to regular opacity + * @param cy + */ + public resetFadedNodes(cy:Cy.Instance) { + cy.batch(()=> { + cy.nodes().style({'background-image-opacity': 1}); + }) + } + + // -------------------------------------------ALL FUNCTIONS NEED REFACTORING---------------------------------------------------------------// + + private static requirementFulfilled(fromNodeId:string, requirement:any, links:Array<Models.CompositionCiLinkBase>):boolean { + return _.some(links, { + 'relation': { + 'fromNode': fromNodeId, + 'relationships': [{ + 'requirementOwnerId': requirement.ownerId, + 'requirement': requirement.name, + 'relationship': { + 'type': requirement.relationship + } + } + ] + } + }); + }; + + private static isMatch(requirement:Models.Requirement, capability:Models.Capability):boolean { + if (capability.type === requirement.capability) { + if (requirement.node) { + if (_.includes(capability.capabilitySources, requirement.node)) { + return true; + } + } else { + return true; + } + } + return false; + }; + + private getFromToMatches(requirements1:Models.RequirementsGroup, + requirements2:Models.RequirementsGroup, + capabilities:Models.CapabilitiesGroup, + links:Array<Models.CompositionCiLinkBase>, + fromId:string, + toId:string, + vlCapability?:Models.Capability):Array<Models.MatchBase> { + let matches:Array<Models.MatchBase> = new Array<Models.MatchBase>(); + _.forEach(requirements1, (requirementValue:Array<Models.Requirement>, key) => { + _.forEach(requirementValue, (requirement:Models.Requirement) => { + if (requirement.name !== "dependency" && !MatchCapabilitiesRequirementsUtils.requirementFulfilled(fromId, requirement, links)) { + _.forEach(capabilities, (capabilityValue:Array<Models.Capability>, key) => { + _.forEach(capabilityValue, (capability:Models.Capability) => { + if (MatchCapabilitiesRequirementsUtils.isMatch(requirement, capability)) { + let match:Models.MatchReqToCapability = new Models.MatchReqToCapability(requirement, capability, true, fromId, toId); + matches.push(match); + } + }); + }); + if (vlCapability) { + _.forEach(requirements2, (requirement2Value:Array<Models.Requirement>, key) => { + _.forEach(requirement2Value, (requirement2:Models.Requirement) => { + if (!MatchCapabilitiesRequirementsUtils.requirementFulfilled(toId, requirement2, links) && MatchCapabilitiesRequirementsUtils.linkable(requirement, requirement2, vlCapability)) { + let match:Models.MatchReqToReq = new Models.MatchReqToReq(requirement, requirement2, true, fromId, toId); + matches.push(match); + } + }); + }); + } + } + }); + }); + return matches; + } + + private getToFromMatches(requirements:Models.RequirementsGroup, capabilities:Models.CapabilitiesGroup, links:Array<Models.CompositionCiLinkBase>, fromId:string, toId:string):Array<Models.MatchReqToCapability> { + let matches:Array<Models.MatchReqToCapability> = []; + _.forEach(requirements, (requirementValue:Array<Models.Requirement>, key) => { + _.forEach(requirementValue, (requirement:Models.Requirement) => { + if (requirement.name !== "dependency" && !MatchCapabilitiesRequirementsUtils.requirementFulfilled(toId, requirement, links)) { + _.forEach(capabilities, (capabilityValue:Array<Models.Capability>, key) => { + _.forEach(capabilityValue, (capability:Models.Capability) => { + if (MatchCapabilitiesRequirementsUtils.isMatch(requirement, capability)) { + let match:Models.MatchReqToCapability = new Models.MatchReqToCapability(requirement, capability, false, toId, fromId); + matches.push(match); + } + }); + }); + } + }); + }); + return matches; + } + + public getMatchedRequirementsCapabilities(fromComponentInstance:Models.ComponentsInstances.ComponentInstance, + toComponentInstance:Models.ComponentsInstances.ComponentInstance, + links:Array<Models.CompositionCiLinkBase>, + vl?:Models.Components.Component):Array<Models.MatchBase> {//TODO allow for VL array + let linkCapability; + if (vl) { + let linkCapabilities:Array<Models.Capability> = vl.capabilities.findValueByKey('linkable'); + if (linkCapabilities) { + linkCapability = linkCapabilities[0]; + } + } + let fromToMatches:Array<Models.MatchBase> = this.getFromToMatches(fromComponentInstance.requirements, + toComponentInstance.requirements, + toComponentInstance.capabilities, + links, + fromComponentInstance.uniqueId, + toComponentInstance.uniqueId, + linkCapability); + let toFromMatches:Array<Models.MatchReqToCapability> = this.getToFromMatches(toComponentInstance.requirements, + fromComponentInstance.capabilities, + links, + fromComponentInstance.uniqueId, + toComponentInstance.uniqueId); + + return fromToMatches.concat(toFromMatches); + } + + + + + + /** + * Step I: Check if capabilities of component match requirements of nodeDataArray + * 1. Get component capabilities and loop on each capability + * 2. Inside the loop, perform another loop on all nodeDataArray, and fetch the requirements for each one + * 3. Loop on the requirements, and verify match (see in code the rules) + * + * Step II: Check if requirements of component match capabilities of nodeDataArray + * 1. Get component requirements and loop on each requirement + * 2. + * + * @param component - this is the hovered resource of the left panel of composition screen + * @param nodeDataArray - Array of resource instances that are on the canvas + * @param links -getMatchedRequirementsCapabilities + * @param vl - + * @returns {any[]|T[]} + */ + public findByMatchingCapabilitiesToRequirements(component:Models.Components.Component, + nodeDataArray:Array<Models.Graph.CompositionCiNodeBase>, + links:Array<Models.CompositionCiLinkBase>, + vl?:Models.Components.Component):Array<any> {//TODO allow for VL array + let res = []; + + // STEP I + { + let capabilities:any = component.capabilities; + _.forEach(capabilities, (capabilityValue:Array<any>, capabilityKey)=> { + _.forEach(capabilityValue, (capability)=> { + _.forEach(nodeDataArray, (node:Models.Graph.CompositionCiNodeBase)=> { + if (node && node.componentInstance) { + let requirements:any = node.componentInstance.requirements; + let fromNodeId:string = node.componentInstance.uniqueId; + _.forEach(requirements, (requirementValue:Array<any>, requirementKey)=> { + _.forEach(requirementValue, (requirement)=> { + if (requirement.name !== "dependency" && MatchCapabilitiesRequirementsUtils.isMatch(requirement, capability) + && !MatchCapabilitiesRequirementsUtils.requirementFulfilled(fromNodeId, requirement, links)) { + res.push(node); + } + }); + }); + } + }); + }); + }); + } + + // STEP II + { + let requirements:any = component.requirements; + let fromNodeId:string = component.uniqueId; + let linkCapability:Array<Models.Capability> = vl ? vl.capabilities.findValueByKey('linkable') : undefined; + + _.forEach(requirements, (requirementValue:Array<any>, requirementKey)=> { + _.forEach(requirementValue, (requirement)=> { + if (requirement.name !== "dependency" && !MatchCapabilitiesRequirementsUtils.requirementFulfilled(fromNodeId, requirement, links)) { + _.forEach(nodeDataArray, (node:any)=> { + if (node && node.componentInstance && node.category !== 'groupCp') { + let capabilities:any = node.componentInstance.capabilities; + _.forEach(capabilities, (capabilityValue:Array<any>, capabilityKey)=> { + _.forEach(capabilityValue, (capability)=> { + if (MatchCapabilitiesRequirementsUtils.isMatch(requirement, capability)) { + res.push(node); + } + }); + }); + if (linkCapability) { + let linkRequirements = node.componentInstance.requirements; + _.forEach(linkRequirements, (value:Array<any>, key)=> { + _.forEach(value, (linkRequirement)=> { + if (!MatchCapabilitiesRequirementsUtils.requirementFulfilled(node.componentInstance.uniqueId, linkRequirement, links) + && MatchCapabilitiesRequirementsUtils.linkable(requirement, linkRequirement, linkCapability[0])) { + res.push(node); + } + }); + }); + } + } + }); + } + }); + }); + } + + return _.uniq(res); + }; + } + + MatchCapabilitiesRequirementsUtils.$inject = []; +}
\ No newline at end of file diff --git a/catalog-ui/app/scripts/directives/graphs-v2/deployment-graph/deployment-graph.directive.ts b/catalog-ui/app/scripts/directives/graphs-v2/deployment-graph/deployment-graph.directive.ts new file mode 100644 index 0000000000..d6d4aef374 --- /dev/null +++ b/catalog-ui/app/scripts/directives/graphs-v2/deployment-graph/deployment-graph.directive.ts @@ -0,0 +1,114 @@ +/** + * Created by obarda on 12/19/2016. + */ +/// <reference path="../../../references"/> +module Sdc.Directives { + + import Util = jasmine.Util; + + interface IDeploymentGraphScope extends ng.IScope { + component:Models.Components.Component; + } + + export class DeploymentGraph implements ng.IDirective { + private _cy:Cy.Instance; + + constructor(private NodesFactory:Utils.NodesFactory, private commonGraphUtils:Graph.Utils.CommonGraphUtils, + private deploymentGraphGeneralUtils:Graph.Utils.DeploymentGraphGeneralUtils, private ComponentInstanceFactory: Sdc.Utils.ComponentInstanceFactory) { + } + + restrict = 'E'; + templateUrl = '/app/scripts/directives/graphs-v2/deployment-graph/deployment-graph.html'; + scope = { + component: '=', + isViewOnly: '=' + }; + + link = (scope:IDeploymentGraphScope, el:JQuery) => { + if(scope.component.isResource()) { + this.loadGraph(scope, el); + this.registerGraphEvents(); + } + }; + + + public initGraphNodes = (cy:Cy.Instance, component:Models.Components.Component):void => { + if (component.groups) { // Init module nodes + _.each(component.groups, (groupModule:Models.Module) => { + let moduleNode = this.NodesFactory.createModuleNode(groupModule); + this.commonGraphUtils.addNodeToGraph(cy, moduleNode); + + }); + } + _.each(component.componentInstances, (instance:Models.ComponentsInstances.ComponentInstance) => { // Init component instance nodes + let componentInstanceNode = this.NodesFactory.createNode(instance); + componentInstanceNode.parent = this.deploymentGraphGeneralUtils.findInstanceModule(component.groups, instance.uniqueId); + if (componentInstanceNode.parent) { // we are not drawing instances that are not a part of a module + this.commonGraphUtils.addComponentInstanceNodeToGraph(cy, componentInstanceNode); + } + }); + + // This is a special functionality to pass the cytoscape default behavior - we can't create Parent module node without children's + // so we must add an empty dummy child node + _.each(this._cy.nodes('[?isGroup]'), (moduleNode: Cy.CollectionFirstNode) => { + if (!moduleNode.isParent()) { + let dummyInstance = this.ComponentInstanceFactory.createEmptyComponentInstance(); + let componentInstanceNode = this.NodesFactory.createNode(dummyInstance); + componentInstanceNode.parent = moduleNode.id(); + let dummyNode = this.commonGraphUtils.addNodeToGraph(cy, componentInstanceNode, moduleNode.position()); + dummyNode.addClass('dummy-node'); + } + }) + }; + + private registerGraphEvents() { + + this._cy.on('afterExpand', (event) => { + event.cyTarget.qtip({}); + }); + + this._cy.on('afterCollapse', (event) => { + this.commonGraphUtils.initNodeTooltip(event.cyTarget); + }); + } + + private loadGraph = (scope:IDeploymentGraphScope, el:JQuery) => { + + let graphEl = el.find('.sdc-deployment-graph-wrapper'); + this._cy = cytoscape({ + container: graphEl, + style: Sdc.Graph.Utils.ComponentIntanceNodesStyle.getCompositionGraphStyle().concat(Sdc.Graph.Utils.ModulesNodesStyle.getModuleGraphStyle()), + zoomingEnabled: false, + selectionType: 'single', + + }); + + //adding expand collapse extension + this._cy.expandCollapse({ + layoutBy: { + name: "grid", + animate: true, + randomize: false, + fit: true + }, + fisheye: false, + undoable: false, + expandCollapseCueSize: 18, + expandCueImage: Sdc.Utils.Constants.IMAGE_PATH + '/styles/images/resource-icons/' + 'closeModule.png', + collapseCueImage: Sdc.Utils.Constants.IMAGE_PATH + '/styles/images/resource-icons/' + 'openModule.png', + expandCollapseCueSensitivity: 2, + cueOffset: -20 + }); + + this.initGraphNodes(this._cy, scope.component); //creating instances nodes + this.commonGraphUtils.initGraphLinks(this._cy, scope.component.componentInstancesRelations); + this._cy.collapseAll(); + }; + + public static factory = (NodesFactory:Utils.NodesFactory, CommonGraphUtils:Graph.Utils.CommonGraphUtils, DeploymentGraphGeneralUtils:Graph.Utils.DeploymentGraphGeneralUtils, ComponentInstanceFactory: Utils.ComponentInstanceFactory) => { + return new DeploymentGraph(NodesFactory, CommonGraphUtils, DeploymentGraphGeneralUtils, ComponentInstanceFactory) + } + } + + DeploymentGraph.factory.$inject = ['NodesFactory', 'CommonGraphUtils', 'DeploymentGraphGeneralUtils', 'ComponentInstanceFactory']; +}
\ No newline at end of file diff --git a/catalog-ui/app/scripts/directives/graphs-v2/deployment-graph/deployment-graph.html b/catalog-ui/app/scripts/directives/graphs-v2/deployment-graph/deployment-graph.html new file mode 100644 index 0000000000..55e1c131f4 --- /dev/null +++ b/catalog-ui/app/scripts/directives/graphs-v2/deployment-graph/deployment-graph.html @@ -0,0 +1,2 @@ +<div class="sdc-deployment-graph-wrapper" ng-class="{'view-only':isViewOnly}"> +</div>
\ No newline at end of file diff --git a/catalog-ui/app/scripts/directives/graphs-v2/deployment-graph/deployment-graph.less b/catalog-ui/app/scripts/directives/graphs-v2/deployment-graph/deployment-graph.less new file mode 100644 index 0000000000..ff8fc46380 --- /dev/null +++ b/catalog-ui/app/scripts/directives/graphs-v2/deployment-graph/deployment-graph.less @@ -0,0 +1,14 @@ +deployment-graph { + display: block; + height:100%; + width: 100%; + + .sdc-deployment-graph-wrapper { + height:100%; + width: 100%; + } + + .view-only{ + background-color:rgb(248, 248, 248); + } +}
\ No newline at end of file diff --git a/catalog-ui/app/scripts/directives/graphs-v2/deployment-graph/deployment-utils/deployment-graph-general-utils.ts b/catalog-ui/app/scripts/directives/graphs-v2/deployment-graph/deployment-utils/deployment-graph-general-utils.ts new file mode 100644 index 0000000000..3ad9da56be --- /dev/null +++ b/catalog-ui/app/scripts/directives/graphs-v2/deployment-graph/deployment-utils/deployment-graph-general-utils.ts @@ -0,0 +1,24 @@ +/** + * Created by obarda on 12/21/2016. + */ +/// <reference path="../../../../references"/> +module Sdc.Graph.Utils { + + export class DeploymentGraphGeneralUtils { + + constructor() { + + } + + public findInstanceModule = (groupsArray:Array<Models.Module>, componentInstanceId:string):string => { + let parentGroup:Sdc.Models.Module = _.find(groupsArray, (group:Sdc.Models.Module) => { + return _.find(group.members, (member) => { + return member === componentInstanceId; + }); + }); + return parentGroup ? parentGroup.uniqueId : ""; + }; + } + + DeploymentGraphGeneralUtils.$inject = []; +}
\ No newline at end of file diff --git a/catalog-ui/app/scripts/directives/graphs-v2/image-creator/image-creator.service.ts b/catalog-ui/app/scripts/directives/graphs-v2/image-creator/image-creator.service.ts new file mode 100644 index 0000000000..e3b17e163d --- /dev/null +++ b/catalog-ui/app/scripts/directives/graphs-v2/image-creator/image-creator.service.ts @@ -0,0 +1,46 @@ +module Sdc.Utils { + export class ImageCreatorService { + static '$inject' = ['$q']; + private _canvas: HTMLCanvasElement; + + constructor(private $q: ng.IQService) { + this._canvas = <HTMLCanvasElement>$('<canvas>')[0]; + this._canvas.setAttribute('style', 'display:none'); + + let body = document.getElementsByTagName('body')[0]; + body.appendChild(this._canvas); + } + + getImageBase64(imageBaseUri: string, imageLayerUri: string): ng.IPromise<string> { + let deferred = this.$q.defer(); + let imageBase = new Image(); + let imageLayer = new Image(); + let imagesLoaded = 0; + let onImageLoaded = () => { + imagesLoaded++; + + if (imagesLoaded < 2) { + return; + } + this._canvas.setAttribute('width', imageBase.width.toString()); + this._canvas.setAttribute('height', imageBase.height.toString()); + + let canvasCtx = this._canvas.getContext('2d'); + canvasCtx.clearRect(0, 0, this._canvas.width, this._canvas.height); + + canvasCtx.drawImage(imageBase, 0, 0, imageBase.width, imageBase.height); + canvasCtx.drawImage(imageLayer, imageBase.width - imageLayer.width, 0, imageLayer.width, imageLayer.height); + + let base64Image = this._canvas.toDataURL(); + deferred.resolve(base64Image); + }; + + imageBase.onload = onImageLoaded; + imageLayer.onload = onImageLoaded; + imageBase.src = imageBaseUri; + imageLayer.src = imageLayerUri; + + return deferred.promise; + } + } +}
\ No newline at end of file diff --git a/catalog-ui/app/scripts/directives/graphs-v2/palette/interfaces/i-dragdrop-event.d.ts b/catalog-ui/app/scripts/directives/graphs-v2/palette/interfaces/i-dragdrop-event.d.ts new file mode 100644 index 0000000000..26c042611c --- /dev/null +++ b/catalog-ui/app/scripts/directives/graphs-v2/palette/interfaces/i-dragdrop-event.d.ts @@ -0,0 +1,7 @@ +interface IDragDropEvent extends JQueryEventObject { + dataTransfer: any; + toElement: { + naturalWidth: number; + naturalHeight: number; + } +}
\ No newline at end of file diff --git a/catalog-ui/app/scripts/directives/graphs-v2/palette/palette.directive.ts b/catalog-ui/app/scripts/directives/graphs-v2/palette/palette.directive.ts new file mode 100644 index 0000000000..c00da6d1df --- /dev/null +++ b/catalog-ui/app/scripts/directives/graphs-v2/palette/palette.directive.ts @@ -0,0 +1,327 @@ +/// <reference path="../../../references"/> + +module Sdc.Directives { + import Dictionary = Sdc.Utils.Dictionary; + import GRAPH_EVENTS = Sdc.Utils.Constants.GRAPH_EVENTS; + import ImageCreatorService = Sdc.Utils.ImageCreatorService; + interface IPaletteScope { + components: any; + currentComponent: any; + model: any; + displaySortedCategories: any; + expandedSection: string; + + p2pVL: Models.Components.Component; + mp2mpVL: Models.Components.Component; + vlType: string; + dragElement: JQuery; + dragbleNode: { + event: JQueryEventObject, + components: Models.DisplayComponent, + ui: any + } + + sectionClick: (section: string)=>void; + searchComponents: (searchText: string)=>void; + onMouseOver: (displayComponent: Models.DisplayComponent)=>void; + onMouseOut: (displayComponent: Models.DisplayComponent)=>void; + dragStartCallback: (event: JQueryEventObject, ui, displayComponent: Models.DisplayComponent)=>void; + dragStopCallback: ()=>void; + onDragCallback: (event:JQueryEventObject) => void; + setElementTemplate: (e: JQueryEventObject)=>void; + + isOnDrag: boolean; + isDragable: boolean; + isLoading: boolean; + isViewOnly: boolean; + } + + export class Palette implements ng.IDirective { + constructor(private $log: ng.ILogService, + private LeftPaletteLoaderService, + private sdcConfig, + private ComponentFactory, + private ComponentInstanceFactory: Utils.ComponentInstanceFactory, + private NodesFactory: Utils.NodesFactory, + private CompositionGraphGeneralUtils: Graph.Utils.CompositionGraphGeneralUtils, + private EventListenerService: Services.EventListenerService, + private sdcMenu: Models.IAppMenu) { + + } + + private fetchingComponentFromServer: boolean = false; + private nodeHtmlSubstitute: JQuery; + + scope = { + components: '=', + currentComponent: '=', + isViewOnly: '=', + isLoading: '=' + }; + restrict = 'E'; + templateUrl = '/app/scripts/directives/graphs-v2/palette/palette.html'; + + link = (scope: IPaletteScope, el: JQuery) => { + this.nodeHtmlSubstitute = $('<div class="node-substitute"><span></span><img /></div>'); + el.append(this.nodeHtmlSubstitute); + + this.initComponents(scope); + this.initScopeVls(scope); + this.initEvents(scope); + this.initDragEvents(scope); + this._initExpandedSection(scope, ''); + }; + + private leftPanelResourceFilter(resourcesNotAbstract: Array<Models.DisplayComponent>, resourceFilterTypes: Array<string>): Array<Models.DisplayComponent> { + let filterResources = _.filter(resourcesNotAbstract, (component) => { + return resourceFilterTypes.indexOf(component.getComponentSubType()) > -1; + }); + return filterResources; + } + + private initLeftPanel(leftPanelComponents: Array<Models.DisplayComponent>, resourceFilterTypes: Array<string>): Models.LeftPanelModel { + let leftPanelModel = new Models.LeftPanelModel(); + + if (resourceFilterTypes && resourceFilterTypes.length) { + leftPanelComponents = this.leftPanelResourceFilter(leftPanelComponents, resourceFilterTypes); + } + leftPanelModel.numberOfElements = leftPanelComponents && leftPanelComponents.length || 0; + + if (leftPanelComponents && leftPanelComponents.length) { + + let categories: any = _.groupBy(leftPanelComponents, 'mainCategory'); + for (let category in categories) + categories[category] = _.groupBy(categories[category], 'subCategory'); + + leftPanelModel.sortedCategories = categories; + } + return leftPanelModel; + } + + private initScopeVls(scope: IPaletteScope): void { + let vls = this.LeftPaletteLoaderService.getFullDataComponentList(Utils.Constants.ResourceType.VL); + scope.vlType = null; + vls.forEach((item) => { + let key = _.find(Object.keys(item.capabilities), (key) => { + return _.includes(key.toLowerCase(), 'linkable'); + }); + let linkable = item.capabilities[key]; + if (linkable) { + if (linkable[0].maxOccurrences == '2') { + scope.p2pVL = _.find(vls, (component: Models.Components.Component) => { + return component.uniqueId === item.uniqueId; + }); + + } else {//assuming unbounded occurrences + scope.mp2mpVL = _.find(vls, (component: Models.Components.Component) => { + return component.uniqueId === item.uniqueId; + }); + } + } + }); + }; + + private initEvents(scope: IPaletteScope) { + /** + * + * @param section + */ + scope.sectionClick = (section: string) => { + if (section === scope.expandedSection) { + scope.expandedSection = ''; + return; + } + scope.expandedSection = section; + }; + + scope.onMouseOver = (displayComponent: Models.DisplayComponent) => { + if (scope.isOnDrag) { + return; + } + scope.isOnDrag = true; + + this.EventListenerService.notifyObservers(GRAPH_EVENTS.ON_PALETTE_COMPONENT_HOVER_IN, displayComponent); + this.$log.debug('palette::onMouseOver:: fired'); + + if (this.CompositionGraphGeneralUtils.componentRequirementsAndCapabilitiesCaching.containsKey(displayComponent.uniqueId)) { + this.$log.debug(`palette::onMouseOver:: component id ${displayComponent.uniqueId} found in cache`); + let cacheComponent: Models.Components.Component = this.CompositionGraphGeneralUtils.componentRequirementsAndCapabilitiesCaching.getValue(displayComponent.uniqueId); + + //TODO: Danny: fire event to highlight matching nodes + //showMatchingNodes(cacheComponent); + return; + } + + this.$log.debug(`palette::onMouseOver:: component id ${displayComponent.uniqueId} not found in cache, initiating server get`); + // This will bring the component from the server including requirements and capabilities + // Check that we do not fetch many times, because only in the success we add the component to componentRequirementsAndCapabilitiesCaching + if (this.fetchingComponentFromServer) { + return; + } + + this.fetchingComponentFromServer = true; + this.ComponentFactory.getComponentFromServer(displayComponent.componentSubType, displayComponent.uniqueId) + .then((component: Models.Components.Component) => { + this.$log.debug(`palette::onMouseOver:: component id ${displayComponent.uniqueId} fetch success`); + this.LeftPaletteLoaderService.updateSpecificComponentLeftPalette(component, scope.currentComponent.componentType); + this.CompositionGraphGeneralUtils.componentRequirementsAndCapabilitiesCaching.setValue(component.uniqueId, component); + this.fetchingComponentFromServer = false; + + //TODO: Danny: fire event to highlight matching nodes + //showMatchingNodes(component); + }) + .catch(() => { + this.$log.debug('palette::onMouseOver:: component id fetch error'); + this.fetchingComponentFromServer = false; + }); + + + }; + + scope.onMouseOut = () => { + scope.isOnDrag = false; + this.EventListenerService.notifyObservers(GRAPH_EVENTS.ON_PALETTE_COMPONENT_HOVER_OUT); + } + } + + private initComponents(scope: IPaletteScope) { + scope.searchComponents = (searchText: any): void => { + scope.displaySortedCategories = this._searchComponents(searchText, scope.model.sortedCategories); + this._initExpandedSection(scope, searchText); + }; + + scope.isDragable = scope.currentComponent.isComplex(); + let entityType: string = scope.currentComponent.componentType.toLowerCase(); + let resourceFilterTypes: Array<string> = this.sdcConfig.resourceTypesFilter[entityType]; + + scope.components = this.LeftPaletteLoaderService.getLeftPanelComponentsForDisplay(scope.currentComponent.componentType); + scope.model = this.initLeftPanel(scope.components, resourceFilterTypes); + scope.displaySortedCategories = angular.copy(scope.model.sortedCategories); + } + + private _initExpandedSection(scope: IPaletteScope, searchText: string): void { + if (searchText == '') { + let isContainingCategory: boolean = false; + let categoryToExpand: string; + if (scope.currentComponent && scope.currentComponent.categories && scope.currentComponent.categories[0]) { + categoryToExpand = this.sdcMenu.categoriesDictionary[scope.currentComponent.categories[0].name]; + for (let category in scope.model.sortedCategories) { + if (categoryToExpand == category) { + isContainingCategory = true; + break; + } + } + } + isContainingCategory ? scope.expandedSection = categoryToExpand : scope.expandedSection = 'Generic'; + } + else { + scope.expandedSection = Object.keys(scope.displaySortedCategories).sort()[0]; + } + }; + + private initDragEvents(scope: IPaletteScope) { + scope.dragStartCallback = (event: IDragDropEvent, ui, displayComponent: Models.DisplayComponent): void => { + if (scope.isLoading || !scope.isDragable || scope.isViewOnly) { + return; + } + + let component = _.find(this.LeftPaletteLoaderService.getFullDataComponentListWithVls(scope.currentComponent.componentType), (componentFullData: Models.DisplayComponent) => { + return displayComponent.uniqueId === componentFullData.uniqueId; + }); + this.EventListenerService.notifyObservers(GRAPH_EVENTS.ON_PALETTE_COMPONENT_DRAG_START, scope.dragElement, component); + + scope.isOnDrag = true; + + + + // this.graphUtils.showMatchingNodes(component, myDiagram, scope.sdcConfig.imagesPath); + // document.addEventListener('mousemove', moveOnDocument); + event.dataTransfer.component = component; + }; + + scope.dragStopCallback = () => { + scope.isOnDrag = false; + }; + + scope.onDragCallback = (event:IDragDropEvent): void => { + this.EventListenerService.notifyObservers(GRAPH_EVENTS.ON_PALETTE_COMPONENT_DRAG_ACTION, event); + }; + scope.setElementTemplate = (e) => { + let dragComponent: Models.Components.Component = _.find(this.LeftPaletteLoaderService.getFullDataComponentListWithVls(scope.currentComponent.componentType), + (fullComponent: Models.Components.Component) => { + return (<any>angular.element(e.currentTarget).scope()).component.uniqueId === fullComponent.uniqueId; + }); + let componentInstance: Models.ComponentsInstances.ComponentInstance = this.ComponentInstanceFactory.createComponentInstanceFromComponent(dragComponent); + let node: Models.Graph.CompositionCiNodeBase = this.NodesFactory.createNode(componentInstance); + + // myDiagram.dragFromPalette = node; + this.nodeHtmlSubstitute.find("img").attr('src', node.img); + scope.dragElement = this.nodeHtmlSubstitute.clone().show(); + + return scope.dragElement; + }; + } + + private _searchComponents = (searchText: string, categories: any): void => { + let displaySortedCategories = angular.copy(categories); + if (searchText != '') { + angular.forEach(categories, function (category: any, categoryKey) { + + angular.forEach(category, function (subcategory: Array<Models.DisplayComponent>, subcategoryKey) { + let filteredResources = []; + angular.forEach(subcategory, function (component: Models.DisplayComponent) { + + let resourceFilterTerm: string = component.searchFilterTerms; + if (resourceFilterTerm.indexOf(searchText.toLowerCase()) >= 0) { + filteredResources.push(component); + } + }); + if (filteredResources.length > 0) { + displaySortedCategories[categoryKey][subcategoryKey] = filteredResources; + } + else { + delete displaySortedCategories[categoryKey][subcategoryKey]; + } + }); + if (!(Object.keys(displaySortedCategories[categoryKey]).length > 0)) { + delete displaySortedCategories[categoryKey]; + } + + }); + } + return displaySortedCategories; + }; + + public static factory = ($log, + LeftPaletteLoaderService, + sdcConfig, + ComponentFactory, + ComponentInstanceFactory, + NodesFactory, + CompositionGraphGeneralUtils, + EventListenerService, + sdcMenu) => { + return new Palette($log, + LeftPaletteLoaderService, + sdcConfig, + ComponentFactory, + ComponentInstanceFactory, + NodesFactory, + CompositionGraphGeneralUtils, + EventListenerService, + sdcMenu); + }; + } + + Palette.factory.$inject = [ + '$log', + 'LeftPaletteLoaderService', + 'sdcConfig', + 'ComponentFactory', + 'ComponentInstanceFactory', + 'NodesFactory', + 'CompositionGraphGeneralUtils', + 'EventListenerService', + 'sdcMenu' + ]; +}
\ No newline at end of file diff --git a/catalog-ui/app/scripts/directives/graphs-v2/palette/palette.html b/catalog-ui/app/scripts/directives/graphs-v2/palette/palette.html new file mode 100644 index 0000000000..a8dd827927 --- /dev/null +++ b/catalog-ui/app/scripts/directives/graphs-v2/palette/palette.html @@ -0,0 +1,59 @@ +<div class="w-sdc-designer-leftbar"> + <div class="w-sdc-designer-leftbar-title">Elements <span class="w-sdc-designer-leftbar-title-count">{{model.numberOfElements}}</span> + </div> + + <div class="w-sdc-designer-leftbar-search"> + <input type="text" class="w-sdc-designer-leftbar-search-input" placeholder="Search..." + data-ng-model="searchText" data-ng-change="searchComponents(searchText)" + ng-model-options="{ debounce: 500 }" data-tests-id="searchAsset"/> + <span class="w-sdc-search-icon leftbar" data-ng-class="{'cancel':searchText, 'magnification':!searchText}" + data-ng-click="searchText=''; searchComponents('',categories)"></span> + </div> + <div class="i-sdc-designer-leftbar-section" + data-ng-repeat="(entityCategory, objCategory) in displaySortedCategories track by $index" + data-ng-class="{'expanded': expandedSection.indexOf(entityCategory) !== -1}"> + <div class="i-sdc-designer-leftbar-section-title pointer" data-ng-click="sectionClick(entityCategory)" + data-tests-id="leftbar-section-title-{{entityCategory}}"> + {{entityCategory}} + <div class="i-sdc-designer-leftbar-section-title-icon"></div> + </div> + <div class="i-sdc-designer-leftbar-section-content" + data-ng-repeat="(subCategory, components) in objCategory track by $index"> + <div class="i-sdc-designer-leftbar-section-content-subcat i-sdc-designer-leftbar-section-content-item"> + {{subCategory}} + </div> + <div class="i-sdc-designer-leftbar-section-content-item" + data-ng-class="{'default-pointer': isViewOnly}" + data-ng-mouseover="!isViewOnly && onMouseOver(component)" + data-ng-mouseleave="!isViewOnly && onMouseOut()" + data-drag="{{!isViewOnly}}" + data-jqyoui-options="{revert: 'invalid', helper:setElementTemplate, appendTo:'body', cursorAt: {left:38, top: 38}, cursor:'move'}" + jqyoui-draggable="{index:{{$index}},animate:true,onStart:'dragStartCallback(component)',onStop:'dragStopCallback()', onDrag:'onDragCallback()'}" + data-ng-repeat="component in components | orderBy: 'displayName' track by $index" + data-tests-id={{component.displayName}}> + <div class="i-sdc-designer-leftbar-section-content-item-icon-ph"> + <div class="medium {{component.iconClass}}" + data-tests-id="leftbar-section-content-item-{{component.displayName}}"> + <div class="{{component.certifiedIconClass}}" uib-tooltip="Not certified" + tooltip-class="uib-custom-tooltip" tooltip-placement="bottom" tooltip-popup-delay="700"> + </div> + </div> + </div> + <div class="i-sdc-designer-leftbar-section-content-item-info"> + <span class="i-sdc-designer-leftbar-section-content-item-info-title" + uib-tooltip="{{component.displayName}}" tooltip-class="uib-custom-tooltip" + tooltip-placement="bottom" tooltip-popup-delay="700"> + {{component.displayName}}</span> + <div class="i-sdc-designer-leftbar-section-content-item-info-text"> + V.{{component.version}} + </div> + <div class="i-sdc-designer-leftbar-section-content-item-info-text"> Type: + {{component.componentSubType}} + <a data-ng-click="openViewerModal(component)" + class="i-sdc-designer-leftbar-section-content-item-info-text-link hand">More</a> + </div> + </div> + </div> + </div> + </div> +</div>
\ No newline at end of file diff --git a/catalog-ui/app/scripts/directives/graphs-v2/palette/palette.less b/catalog-ui/app/scripts/directives/graphs-v2/palette/palette.less new file mode 100644 index 0000000000..85657a43a5 --- /dev/null +++ b/catalog-ui/app/scripts/directives/graphs-v2/palette/palette.less @@ -0,0 +1,92 @@ +.drag-icon-border{ + border: 7px solid red; + border-radius: 500px; + -webkit-border-radius: 500px; + -moz-border-radius: 500px; + width: 53px; + height: 53px; +} + +.drag-icon-circle{ + width: 60px; + height: 60px; + -webkit-border-radius: 50%; + -moz-border-radius: 50%; + border-radius: 50%; + position: relative; + +} + + +@green-shadow: rgba(29, 154, 149, 0.3); +@red-shadow: rgba(218, 31, 61, 0.3); +.drag-icon-circle .sprite-resource-icons { + position: absolute; + top: 10px; + left: 10px; +} + +.drag-icon-circle.red { + background: @red-shadow; +} + +.drag-icon-circle.green { + background: @green-shadow; +} + + +.node-substitute { + display: none; + position: absolute; + z-index: 9999; + height: 80px; + width: 80px; + border-radius: 50%; + text-align: center; + + span { + display: inline-block; + vertical-align: middle; + height: 100%; + } + + img { + height: 40px; + width: 40px; + box-shadow: 0 0 0 10px @green-shadow; + border-radius: 50%; + + -webkit-user-drag: none; + -moz-user-drag: none; + user-drag: none; + } + &.red img { + box-shadow: 0 0 0 10px @red-shadow; + } + &.bounce img { + -moz-animation:bounceOut 0.3s linear; + -webkit-animation:bounceOut 0.3s linear; + animation:bounceOut 0.3s linear; + } +} + +@keyframes bounceOut { + 0%{ box-shadow: 0 0 0 10px @green-shadow; width: 40px; height: 40px; } + 60%{ box-shadow: 0 0 0 0px @green-shadow; width: 60px; height: 60px; } + 85%{ box-shadow: 0 0 0 0px @green-shadow; width: 75px; height: 75px; } + 100%{ box-shadow: 0 0 0 0px @green-shadow; width: 60px; height: 60px; } +} + +@-moz-keyframes bounceOut { + 0%{ box-shadow: 0 0 0 10px @green-shadow; width: 40px; height: 40px; } + 60%{ box-shadow: 0 0 0 0px @green-shadow; width: 60px; height: 60px; } + 85%{ box-shadow: 0 0 0 0px @green-shadow; width: 75px; height: 75px; } + 100%{ box-shadow: 0 0 0 0px @green-shadow; width: 60px; height: 60px; } +} + +@-webkit-keyframes bounceOut { + 0%{ box-shadow: 0 0 0 10px @green-shadow; width: 40px; height: 40px; } + 60%{ box-shadow: 0 0 0 0px @green-shadow; width: 60px; height: 60px; } + 85%{ box-shadow: 0 0 0 0px @green-shadow; width: 75px; height: 75px; } + 100%{ box-shadow: 0 0 0 0px @green-shadow; width: 60px; height: 60px; } +} diff --git a/catalog-ui/app/scripts/directives/graphs-v2/relation-menu/relation-menu.html b/catalog-ui/app/scripts/directives/graphs-v2/relation-menu/relation-menu.html new file mode 100644 index 0000000000..a0a9e4af27 --- /dev/null +++ b/catalog-ui/app/scripts/directives/graphs-v2/relation-menu/relation-menu.html @@ -0,0 +1,63 @@ +<div class="link-menu-open" data-tests-id="link-menu-open" data-ng-show="isLinkMenuOpen" ng-style="{left: relationMenuDirectiveObj.menuPosition.x, top: relationMenuDirectiveObj.menuPosition.y}" clicked-outside="{onClickedOutside: 'hideRelationMatch()', clickedOutsideEnable: 'isLinkMenuOpen'}" > + <h4 sdc-smart-tooltip>{{relationMenuDirectiveObj.leftSideLink.componentInstance.name | resourceName}}</h4> + <h4 sdc-smart-tooltip>{{relationMenuDirectiveObj.rightSideLink.componentInstance.name | resourceName}}</h4> + + <p>Select one of the options below to connect</p> + + <perfect-scrollbar scroll-y-margin-offset="0" include-padding="true" class="scrollbar-container"> + <div class="inner-title" data-ng-show="hasMatchesToShow(relationMenuDirectiveObj.leftSideLink.requirements, relationMenuDirectiveObj.rightSideLink.selectedMatch)">Requirements</div> + <div class="link-item" data-tests-id="link-item-requirements" data-ng-repeat="(req ,matchArr) in relationMenuDirectiveObj.leftSideLink.requirements" + data-ng-click="relationMenuDirectiveObj.leftSideLink.selectMatchArr(matchArr); updateSelectionText()" + data-ng-show="showMatch(relationMenuDirectiveObj.rightSideLink.selectedMatch, matchArr)" + data-ng-class="{ 'selected': relationMenuDirectiveObj.leftSideLink.selectedMatch === matchArr}"> + <div sdc-smart-tooltip>{{matchArr[0].requirement.getFullTitle()}}</div> + </div> + + <div class="inner-title" data-ng-show="hasMatchesToShow(relationMenuDirectiveObj.leftSideLink.capabilities, relationMenuDirectiveObj.rightSideLink.selectedMatch)">Capabilities</div> + <div class="link-item" data-tests-id="link-item-capabilities" data-ng-repeat="(cap, matchArr) in relationMenuDirectiveObj.leftSideLink.capabilities" + data-ng-click="relationMenuDirectiveObj.leftSideLink.selectMatchArr(matchArr); updateSelectionText()" + data-ng-show="showMatch(relationMenuDirectiveObj.rightSideLink.selectedMatch, matchArr)" + data-ng-class="{ 'selected': relationMenuDirectiveObj.leftSideLink.selectedMatch === matchArr}"> + <div sdc-smart-tooltip>{{matchArr[0].capability.getFullTitle()}}</div> + </div> + </perfect-scrollbar> + + <perfect-scrollbar scroll-y-margin-offset="0" include-padding="true" class="scrollbar-container"> + <div class="inner-title" data-ng-show="hasMatchesToShow(relationMenuDirectiveObj.rightSideLink.requirements, relationMenuDirectiveObj.leftSideLink.selectedMatch)">Requirements</div> + <div class="link-item" data-tests-id="link-item-requirements" data-ng-repeat="(req, matchArr) in relationMenuDirectiveObj.rightSideLink.requirements" + data-ng-click="relationMenuDirectiveObj.rightSideLink.selectMatchArr(matchArr); updateSelectionText()" + data-ng-show="showMatch(relationMenuDirectiveObj.leftSideLink.selectedMatch, matchArr)" + data-ng-class="{ 'selected': relationMenuDirectiveObj.rightSideLink.selectedMatch === matchArr}"> + <div sdc-smart-tooltip>{{matchArr[0].secondRequirement ? matchArr[0].secondRequirement.getFullTitle() : matchArr[0].requirement.getFullTitle()}}</div> + </div> + + <div class="inner-title" data-ng-show="hasMatchesToShow(relationMenuDirectiveObj.rightSideLink.capabilities, relationMenuDirectiveObj.leftSideLink.selectedMatch)">Capabilities</div> + <div class="link-item" data-tests-id="link-item-capabilities" data-ng-repeat="(cap, matchArr) in relationMenuDirectiveObj.rightSideLink.capabilities" + data-ng-click="relationMenuDirectiveObj.rightSideLink.selectMatchArr(matchArr); updateSelectionText()" + data-ng-show="showMatch(relationMenuDirectiveObj.leftSideLink.selectedMatch, matchArr)" + data-ng-class="{ 'selected': relationMenuDirectiveObj.rightSideLink.selectedMatch === matchArr}"> + <div sdc-smart-tooltip>{{matchArr[0].capability.getFullTitle()}}</div> + </div> + </perfect-scrollbar> + + <div class="vl-type" data-ng-class="{'disabled': !relationMenuDirectiveObj.leftSideLink.selectedMatch[0].secondRequirement || !relationMenuDirectiveObj.rightSideLink.selectedMatch[0].secondRequirement}"> + <sdc-radio-button sdc-model="relationMenuDirectiveObj.vlType" value="ptp" + disabled="!relationMenuDirectiveObj.leftSideLink.selectedMatch[0].secondRequirement || !relationMenuDirectiveObj.rightSideLink.selectedMatch[0].secondRequirement || !relationMenuDirectiveObj.p2pVL" + text="Point to point" elem-id="radioPTP" elem-name="vlType"></sdc-radio-button> + + <sdc-radio-button sdc-model="relationMenuDirectiveObj.vlType" value="mptmp" + disabled="!relationMenuDirectiveObj.leftSideLink.selectedMatch[0].secondRequirement || !relationMenuDirectiveObj.rightSideLink.selectedMatch[0].secondRequirement || !relationMenuDirectiveObj.mp2mpVL" + text="Multi point" elem-id="radioMPTMP" elem-name="vlType"></sdc-radio-button> + + <span class="sprite-new info-icon" tooltips tooltip-content="You are required to choose the type of the Virtual Link."></span> + </div> + + <div class="result" sdc-smart-tooltip>​{{relationMenuDirectiveObj.selectionText}} + + </div> + + <button class="tlv-btn grey" data-tests-id="link-menu-button-cancel" data-ng-click="hideRelationMatch()">Cancel</button> + <button class="tlv-btn blue" data-tests-id="link-menu-button-connect" data-ng-disabled="!relationMenuDirectiveObj.leftSideLink.selectedMatch || !relationMenuDirectiveObj.rightSideLink.selectedMatch || + (relationMenuDirectiveObj.leftSideLink.selectedMatch[0].secondRequirement && !relationMenuDirectiveObj.vlType)" + data-ng-click="saveRelation()">Connect</button> +</div> diff --git a/catalog-ui/app/scripts/directives/graphs-v2/relation-menu/relation-menu.less b/catalog-ui/app/scripts/directives/graphs-v2/relation-menu/relation-menu.less new file mode 100644 index 0000000000..dea814dbec --- /dev/null +++ b/catalog-ui/app/scripts/directives/graphs-v2/relation-menu/relation-menu.less @@ -0,0 +1,118 @@ +.link-menu-open { + display: block !important; + color: @main_color_m; + font-size: 14px; + position: absolute; + z-index: 99999; + border-radius: 2px; + background-color: #ffffff; + box-shadow: 0px 0px 6px 0px rgba(0, 0, 0, 0.5); + width: 460px; + height: 418px; + + h4 { + width: 50%; + float: left; + background-color: @tlv_color_u; + font-size: 14px; + font-weight: bold; + line-height: 36px; + margin: 0; + padding: 0 15px; + + & + h4 { + border-left: #d8d8d8 1px solid; + } + } + p { + clear: both; + text-indent: 15px; + border-bottom: #d8d8d8 1px solid; + line-height: 34px; + margin: 0; + color: @func_color_s; + } + + .scrollbar-container { + height: 232px; + width: 50%; + float: left; + margin-bottom: 5px; + .perfect-scrollbar; + + & + .scrollbar-container { + border-left: #d8d8d8 1px solid; + } + + .inner-title { + width: 189px; + margin: 5px auto 3px auto; + //text-indent: 10px; + color: @func_color_s; + text-transform: uppercase; + font-weight: bold; + + //&:not(:first-child) { + // margin-top: 10px; + //} + } + + .link-item { + padding: 0 10px; + line-height: 23px; + height: 23px; + text-indent: 5px; + .hand; + + &.selected { + background-color: @tlv_color_v; + } + } + } + + .vl-type { + height: 33px; + border-top: #d8d8d8 solid 1px; + clear: both; + padding: 0 10px; + line-height: 32px; + color: @main_color_m; + + &.disabled { + background-color: #f2f2f2; + color: @color_m; + } + .info-icon { + float:right; + margin-top: 9px; + } + .tlv-radio { + margin-right: 10px; + } + } + + .result { + background-color: @main_color_m; + line-height: 29px; + color: #ffffff; + padding: 0 15px; + } + + button { + float: right; + margin-top: 9px; + margin-right: 10px; + } +} +.link-menu-item { + cursor: pointer; + line-height: 24px; + padding: 0 10px; + &:hover { + color: @color_a; + } +} +.link-menu::before { + right: inherit !important; + left: 50px; +}
\ No newline at end of file diff --git a/catalog-ui/app/scripts/directives/graphs-v2/relation-menu/relation-menu.ts b/catalog-ui/app/scripts/directives/graphs-v2/relation-menu/relation-menu.ts new file mode 100644 index 0000000000..22a2d078b7 --- /dev/null +++ b/catalog-ui/app/scripts/directives/graphs-v2/relation-menu/relation-menu.ts @@ -0,0 +1,113 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ +/// <reference path="../../../references"/> +module Sdc.Directives { + 'use strict'; + + + export interface IRelationMenuScope extends ng.IScope { + relationMenuDirectiveObj:Models.RelationMenuDirectiveObj; + createRelation:Function; + isLinkMenuOpen:boolean; + hideRelationMatch:Function; + cancel:Function; + + saveRelation(); + showMatch(arr1:Array<Models.MatchBase>, arr2:Array<Models.MatchBase>):boolean; + hasMatchesToShow(matchesObj:Models.MatchBase, selectedMatch:Array<Models.MatchBase>); + updateSelectionText():void; + + } + + + export class RelationMenuDirective implements ng.IDirective { + + constructor(private $templateCache:ng.ITemplateCacheService, + private $filter:ng.IFilterService + ) { + } + + scope = { + relationMenuDirectiveObj: '=', + isLinkMenuOpen: '=', + createRelation: '&', + cancel:'&' + }; + + restrict = 'E'; + replace = true; + template = ():string => { + return this.$templateCache.get('/app/scripts/directives/graphs-v2/relation-menu/relation-menu.html'); + }; + + link = (scope:IRelationMenuScope, element:JQuery, $attr:ng.IAttributes) => { + + scope.saveRelation = ():void=> { + let chosenMatches:Array<any> = _.intersection(scope.relationMenuDirectiveObj.rightSideLink.selectedMatch, scope.relationMenuDirectiveObj.leftSideLink.selectedMatch); + let chosenMatch:Models.MatchBase = chosenMatches[0]; + let chosenVL:Models.Components.Component; + if ("mptmp" === scope.relationMenuDirectiveObj.vlType) { + chosenVL = scope.relationMenuDirectiveObj.mp2mpVL; + } else { + chosenVL = scope.relationMenuDirectiveObj.p2pVL; + } + scope.createRelation()(chosenMatch,chosenVL); + }; + + + scope.hideRelationMatch = () => { + scope.isLinkMenuOpen = false; + scope.cancel(); + }; + + //to show options in link menu + scope.showMatch = (arr1:Array<Models.MatchBase>, arr2:Array<Models.MatchBase>):boolean => { + return !arr1 || !arr2 || _.intersection(arr1, arr2).length > 0; + }; + + //to show requirements/capabilities title + scope.hasMatchesToShow = (matchesObj:Models.MatchBase, selectedMatch:Array<Models.MatchBase>):boolean => { + let result:boolean = false; + _.forEach(matchesObj, (matchesArr:Array<Models.MatchBase>) => { + if (!result) { + result = scope.showMatch(matchesArr, selectedMatch); + } + }); + return result; + }; + + + scope.updateSelectionText = ():void => { + let left:string = scope.relationMenuDirectiveObj.leftSideLink.selectedMatch ? this.$filter('resourceName')(scope.relationMenuDirectiveObj.leftSideLink.selectedMatch[0].getDisplayText('left')) : ''; + let both:string = scope.relationMenuDirectiveObj.leftSideLink.selectedMatch && scope.relationMenuDirectiveObj.rightSideLink.selectedMatch ? ' - ' + + this.$filter('resourceName')(scope.relationMenuDirectiveObj.leftSideLink.selectedMatch[0].requirement.relationship) + ' - ' : ''; + let right:string = scope.relationMenuDirectiveObj.rightSideLink.selectedMatch ? this.$filter('resourceName')(scope.relationMenuDirectiveObj.rightSideLink.selectedMatch[0].getDisplayText('right')) : ''; + scope.relationMenuDirectiveObj.selectionText = left + both + right; + }; + + + } + public static factory = ($templateCache:ng.ITemplateCacheService , $filter:ng.IFilterService)=> { + return new RelationMenuDirective($templateCache, $filter); + }; + } + + RelationMenuDirective.factory.$inject = ['$templateCache', '$filter']; +} diff --git a/catalog-ui/app/scripts/directives/info-tooltip/info-tooltip.html b/catalog-ui/app/scripts/directives/info-tooltip/info-tooltip.html new file mode 100644 index 0000000000..5c2bdcf5f1 --- /dev/null +++ b/catalog-ui/app/scripts/directives/info-tooltip/info-tooltip.html @@ -0,0 +1,10 @@ +<div> + <span class="sprite-new info-icon hand" data-ng-click="showMessage=!showMessage"></span> + <div class="info-tooltip" data-ng-show="showMessage"> + <div class="info-tooltip-arrow"></div> + <div class="info-tooltip-content" data-ng-class="direction"> + <span class="close-tooltip sprite-new close-info-tooltip-button" data-ng-click="showMessage=false"></span> + <p class="info-tooltip-message" translate="{{infoMessageTranslate}}"></p> + </div> + </div> +</div> diff --git a/catalog-ui/app/scripts/directives/info-tooltip/info-tooltip.less b/catalog-ui/app/scripts/directives/info-tooltip/info-tooltip.less new file mode 100644 index 0000000000..8811af16a4 --- /dev/null +++ b/catalog-ui/app/scripts/directives/info-tooltip/info-tooltip.less @@ -0,0 +1,39 @@ +.info-tooltip { + position: fixed; + z-index: 1070; + display: block; + width: 250px; + .info-tooltip-arrow { + width: 0; + height: 0; + border-style: solid; + border-width: 0 5px 5px 5px; + border-color: transparent transparent @main_color_a transparent; + position: relative; + left: 2px; + } + .info-tooltip-content { + box-shadow: 0px 3px 6px 0px rgba(0, 0, 0, 0.5); + border: 1px solid @main_color_o; + border-radius: 3px; + border-top: 3px solid @main_color_a; + position: relative; + background-color: white; + &.right{ + left: -13px; + } + &.left{ + left: -223px; + } + .close-tooltip{ + float: right; + margin: 5px; + } + + .info-tooltip-message{ + margin: 15px; + word-break: normal; + font-size: 14px; + } + } +} diff --git a/catalog-ui/app/scripts/directives/info-tooltip/info-tooltip.ts b/catalog-ui/app/scripts/directives/info-tooltip/info-tooltip.ts new file mode 100644 index 0000000000..cd81b14ce8 --- /dev/null +++ b/catalog-ui/app/scripts/directives/info-tooltip/info-tooltip.ts @@ -0,0 +1,60 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ +/** + * Created by rcohen on 9/25/2016. + */ +/// <reference path="../../references"/> +module Sdc.Directives { + 'use strict'; + + export interface IInfoTooltipScope extends ng.IScope { + infoMessageTranslate:string; + direction:string; + } + + + export class InfoTooltipDirective implements ng.IDirective { + + constructor(private $templateCache:ng.ITemplateCacheService) { + } + + scope = { + infoMessageTranslate:'@', + direction:'@'//get 'right' or 'left', the default is 'right' + }; + + restrict = 'E'; + replace = true; + template = ():string => { + return this.$templateCache.get('/app/scripts/directives/info-tooltip/info-tooltip.html'); + }; + + link = (scope:IInfoTooltipScope, element:any, $attr:any) => { + scope.direction = scope.direction || 'right'; + }; + + public static factory = ($templateCache:ng.ITemplateCacheService)=> { + return new InfoTooltipDirective($templateCache); + }; + + } + + InfoTooltipDirective.factory.$inject = ['$templateCache']; +} diff --git a/catalog-ui/app/scripts/directives/invalid-characters/invalid-characters.ts b/catalog-ui/app/scripts/directives/invalid-characters/invalid-characters.ts new file mode 100644 index 0000000000..7ab98b0d23 --- /dev/null +++ b/catalog-ui/app/scripts/directives/invalid-characters/invalid-characters.ts @@ -0,0 +1,72 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ +/// <reference path="../../references"/> +module Sdc.Directives { + 'use strict'; + + export class InvalidCharactersDirective implements ng.IDirective { + + constructor() {} + + require = 'ngModel'; + + link = (scope, elem, attrs, ngModel) => { + + let invalidCharacters = []; + + attrs.$observe('invalidCharacters', (val:string) => { + invalidCharacters = val.split(''); + validate(ngModel.$viewValue); + }); + + let validate: Function = function (value) { + + let valid:boolean = true; + + if(value) { + for (let i = 0; i < invalidCharacters.length; i++) { + if (value.indexOf(invalidCharacters[i]) != - 1) { + valid = false; + } + } + } + + ngModel.$setValidity('invalidCharacters', valid); + if(!value) { + ngModel.$setPristine(); + } + return value; + }; + + //For DOM -> model validation + ngModel.$parsers.unshift(validate); + //For model -> DOM validation + ngModel.$formatters.unshift(validate); + + }; + + public static factory = ()=> { + return new InvalidCharactersDirective(); + }; + + } + + InvalidCharactersDirective.factory.$inject = []; +} diff --git a/catalog-ui/app/scripts/directives/layout/top-nav/top-nav.html b/catalog-ui/app/scripts/directives/layout/top-nav/top-nav.html new file mode 100644 index 0000000000..40b1e86d90 --- /dev/null +++ b/catalog-ui/app/scripts/directives/layout/top-nav/top-nav.html @@ -0,0 +1,54 @@ +<nav class="top-nav"> + + <div class="asdc-app-title-wrapper"> + <a class="asdc-app-title">SDC</a> <!-- data-ui-sref="dashboard" --> + <div class="asdc-version"> v.{{version}}</div> + </div> + + <ul class="top-menu" data-ng-show="!menuModel"> + <!-- no hierarchy & dropdowns mode --> + <li data-ng-repeat="item in topLvlMenu.menuItems" + data-ng-class="{'selected': $index == topLvlMenu.selectedIndex}"> + <a data-ng-click="menuItemClick(topLvlMenu, item)" + data-tests-id="main-menu-button-{{item.text | lowercase}}">{{item.text}}</a> + </li> + </ul> + + <ul class="top-menu" data-ng-show="menuModel"> + <!-- with hierarchy & dropdowns mode --> + <li data-ng-repeat-start="groupItem in menuModel" + data-ng-class="{'selected': $last }"> + <a data-ng-click="menuItemClick(groupItem, groupItem.menuItems[groupItem.selectedIndex])" + data-tests-id="breadcrumbs-button-{{$index}}"> + {{groupItem.menuItems[groupItem.selectedIndex].text}} + </a> + </li> + <li data-ng-repeat-end="" class="triangle-dropdown" + data-ng-class="{'item-click': groupItem.itemClick}" data-ng-mouseover="groupItem.itemClick = true"> + <div class="triangle"><span class="sprite-new arrow-right"></span></div> + <perfect-scrollbar scroll-y-margin-offset="15" include-padding="true"> + <ul> + <li data-ng-repeat="ddItem in groupItem.menuItems" + data-ng-click="menuItemClick(groupItem, ddItem)" + data-ng-class="{'selected': $index == groupItem.selectedIndex, 'disabled': ddItem.isDisabled}" + data-tests-id="sub-menu-button-{{ddItem.text | lowercase}}"> + <span sdc-smart-tooltip="">{{ddItem.text}}</span> + </li> + </ul> + </perfect-scrollbar> + </li> + </ul> + + <div class="top-search" data-ng-hide="hideSearch === true"> + <input type="text" + class="search-text" + placeholder="Search" + data-ng-model="searchBind" + data-tests-id="main-menu-input-search" + ng-model-options="{ debounce: 500 }" /> + <span class="w-sdc-search-icon magnification"></span> + </div> + + <div class="notification-icon" data-ng-disabled= "progress > 0" data-ng-class="{'disabled' : progress > 0}" data-ng-if="user.role === 'DESIGNER' && notificationIconCallback" data-ng-click="notificationIconCallback()" tooltips tooltip-side="left" tooltip-content="Vendor Software Product Repository" data-tests-id="repository-icon"></div> + +</nav> diff --git a/catalog-ui/app/scripts/directives/layout/top-nav/top-nav.less b/catalog-ui/app/scripts/directives/layout/top-nav/top-nav.less new file mode 100644 index 0000000000..65021bdc4d --- /dev/null +++ b/catalog-ui/app/scripts/directives/layout/top-nav/top-nav.less @@ -0,0 +1,218 @@ +.top-nav { + position: fixed; + top: @header_height; + background-color: @main_color_p; + .box-shadow(0px 1px 3px 0px rgba(0, 0, 0, 0.33)); + width: 100%; + height: @top_nav_height; + line-height: @top_nav_height; + z-index: 10; + display: flex; + flex-direction: row; + align-items: center; + + .asdc-app-title-wrapper { + flex-grow: 1; + line-height: 16px; + margin: 0 20px; + + a.asdc-app-title { + .m_18_r; + text-decoration: none; + } + + .asdc-version { + .m_12_r; + .opacity(0.8); + line-height: 14px; + flex-grow: 1; + } + + } + + ul.top-menu { + list-style-type: none; + margin: 0 0 0 20px; + padding: 0; + flex-grow: 999; + + & > li { + float: left; + cursor: pointer; + line-height: 50px; + height: 50px; + padding: 0 20px; + + &.selected { + border-bottom: solid 4px @main_color_a; + + a { + color: @func_color_s; + } + } + + /*&:hover { + border-bottom: solid 4px @main_color_a; + }*/ + + a { + font-family: @font-omnes-medium; + color: @main_color_m; + font-size: 18px; + display: block; + text-align: center; + text-decoration: none; + } + + &.triangle-dropdown { + padding: 0; + position: relative; + + div.triangle { + margin-top: 15px; + border-radius: 2px; + width: 17px; + height: 18px; + + //temp use - until new triangle gets in + line-height: 18px; + text-align: center; + font-size: 10px; + + &:hover { + background-color: rgba(156, 156, 156, 0.2); + + span { + .arrow-right-hover; + } + } + } + + + li a { + font-size: 16px; + } + + .ps-container { + .perfect-scrollbar; + position: absolute; + left: 0; + top: 40px; + z-index: 1; + + overflow: hidden; + max-height: 0; + -webkit-transition: max-height 200ms ease-in; + -moz-transition: max-height 200ms ease-in; + -o-transition: max-height 200ms ease-in; + transition: max-height 200ms ease-in; + + div ul { + + padding: 0; + background-color: white; + + li { + + height: 35px; + background-color: white; + font-size: 13px; + width: 150px; + line-height: 35px; + padding: 0 10px; + + &.disabled { + opacity: 1; + } + &.selected { + background-color: @tlv_color_v; + font-weight: bold; + } + &:hover { + color: @main_color_a; + } + span { + height: 35px; + width: 130px; + display: inline-block; + } + } + } + } + &.item-click:hover .ps-container, + &.item-click:active .ps-container { + max-height: 500px; + border: 1px solid @func_color_b; + border-radius: 2px; + box-shadow: 0px 2px 2px 0px rgba(24, 24, 25, 0.1); + + div ul { + + } + } + } + } + + } + + .top-search { + position: relative; + flex-grow: 1; + padding: 0 20px; + + input.search-text { + .border-radius(2px); + width: 245px; + height: 32px; + line-height: 32px; + border: 1px solid @main_color_o; + outline: none; + text-indent: 10px; + + &::-webkit-input-placeholder { font-style: italic; } /* Safari, Chrome and Opera */ + &:-moz-placeholder { font-style: italic; } /* Firefox 18- */ + &::-moz-placeholder { font-style: italic; } /* Firefox 19+ */ + &:-ms-input-placeholder { font-style: italic; } /* IE 10+ */ + &:-ms-input-placeholder { font-style: italic; } /* Edge */ + /* font-style: italic; + }*/ + /* Firefox 18- */ + &::-moz-placeholder { + font-style: italic; + } + /* Firefox 19+ */ + &:-ms-input-placeholder { + font-style: italic; + } + /* IE 10+ */ + &:-ms-input-placeholder { + font-style: italic; + } + /* Edge */ + } + + .magnification { + position: absolute; + top: 19px; + right: 26px; + } + + } + + .notification-icon { + cursor: pointer; + flex-grow: 1; + margin: 0 10px 6px 0; + .sprite-new; + .vsp-list-icon; + + &:hover { + .vsp-list-icon-hover; + } + + &:active { + .vsp-list-icon-active; + } + + } + +} diff --git a/catalog-ui/app/scripts/directives/layout/top-nav/top-nav.ts b/catalog-ui/app/scripts/directives/layout/top-nav/top-nav.ts new file mode 100644 index 0000000000..356e43b7f7 --- /dev/null +++ b/catalog-ui/app/scripts/directives/layout/top-nav/top-nav.ts @@ -0,0 +1,155 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ +/// <reference path="../../../references"/> +module Sdc.Directives { + 'use strict'; + + export interface ITopNavScope extends ng.IScope { + topLvlSelectedIndex: number; + hideSearch: boolean; + searchBind: any; + menuModel: Array<Utils.MenuItemGroup>; + + topLvlMenu: Utils.MenuItemGroup; + goToState(state:string, params:Array<any>):ng.IPromise<boolean>; + menuItemClick: Function; + user: Models.IUserProperties; + version:string; + } + + + export class TopNavDirective implements ng.IDirective { + + constructor(private $templateCache:ng.ITemplateCacheService, + private $filter:ng.IFilterService, + private $state:ng.ui.IStateService, + private $q: ng.IQService, + private userResourceService: Sdc.Services.IUserResourceClass + ) { + } + + public replace = true; + public restrict = 'E'; + public transclude = false; + + + scope = { + topLvlSelectedIndex: '@?', + hideSearch: '=', + searchBind: '=', + version: '@', + notificationIconCallback: '=', + menuModel: '=?', + }; + + template = ():string => { + return this.$templateCache.get('/app/scripts/directives/layout/top-nav/top-nav.html'); + }; + + public link = (scope:ITopNavScope, $elem:ng.IAugmentedJQuery, $attrs:angular.IAttributes) => { + + let getTopLvlSelectedIndexByState = ():number => { + if (!scope.topLvlMenu.menuItems) { + return 0; + } + + let result = -1; + + //set result to current state + scope.topLvlMenu.menuItems.forEach((item:Utils.MenuItem, index:number)=> { + if (item.state === this.$state.current.name) { + result = index; + } + }); + + //if it's a different state , checking previous state param + if (result === -1) { + scope.topLvlMenu.menuItems.forEach((item:Utils.MenuItem, index:number)=> { + if (item.state === this.$state.params['previousState']) { + result = index; + } + }); + } + + if (result === -1) { + result = 0; + } + + return result; + }; + + scope.user = this.userResourceService.getLoggedinUser(); + + let tmpArray:Array<Utils.MenuItem> = [ + new Utils.MenuItem(this.$filter('translate')("TOP_MENU_HOME_BUTTON"), null, "dashboard", "goToState", null, null), + new Utils.MenuItem(this.$filter('translate')("TOP_MENU_CATALOG_BUTTON"), null, "catalog", "goToState", null, null) + ]; + + // Only designer can perform onboarding + if (scope.user && scope.user.role === 'DESIGNER'){ + tmpArray.push(new Utils.MenuItem(this.$filter('translate')("TOP_MENU_ON_BOARD_BUTTON"), null, "onboardVendor", "goToState", null, null)); + } + + scope.topLvlMenu = new Utils.MenuItemGroup(0, tmpArray , true ); + scope.topLvlMenu.selectedIndex = isNaN(scope.topLvlSelectedIndex) ? getTopLvlSelectedIndexByState() : scope.topLvlSelectedIndex; + + let generateMenu = () => { + if (scope.menuModel && scope.menuModel[0] !== scope.topLvlMenu) { + scope.menuModel.unshift(scope.topLvlMenu); + } + }; + scope.$watch('menuModel', generateMenu); + + generateMenu(); + + /////scope functions//// + + scope.goToState = (state:string, params:Array<any>):ng.IPromise<boolean> => { + let deferred = this.$q.defer(); + this.$state.go(state, params && params.length > 0 ? [0] : undefined); + deferred.resolve(true); + return deferred.promise; + }; + + scope.menuItemClick = (itemGroup:Utils.MenuItemGroup, item:Utils.MenuItem) => { + + itemGroup.itemClick = false; + + let onSuccess = ():void => { + itemGroup.selectedIndex = itemGroup.menuItems.indexOf(item); + }; + let onFailed = ():void => {}; + + if (item.callback) { + (item.callback.apply(undefined, item.params)).then(onSuccess, onFailed); + } else { + scope[item.action](item.state, item.params).then(onSuccess, onFailed); + } + }; + }; + + public static factory = ($templateCache:ng.ITemplateCacheService, $filter:ng.IFilterService, $state:ng.ui.IStateService, $q: ng.IQService, userResourceService: Sdc.Services.IUserResourceClass)=> { + return new TopNavDirective($templateCache, $filter, $state,$q, userResourceService); + }; + + } + + TopNavDirective.factory.$inject = ['$templateCache', '$filter', '$state','$q', 'Sdc.Services.UserResourceService']; +} diff --git a/catalog-ui/app/scripts/directives/layout/top-progress/top-progress.html b/catalog-ui/app/scripts/directives/layout/top-progress/top-progress.html new file mode 100644 index 0000000000..ab2c8e364e --- /dev/null +++ b/catalog-ui/app/scripts/directives/layout/top-progress/top-progress.html @@ -0,0 +1,22 @@ +<div class="top-progress"> + + <!--======================= Top progress var =======================--> + <div data-ng-if="progressValue>0 && progressValue<100"> + <span class="sdc-progress-title">{{progressMessage}}<span class="progress-percentage">{{progressValue}} %</span></span> + <div class="sdc-progress"> + <div class="progress-bar progress-bar-striped active" role="progressbar" aria-valuenow="{{progressValue}}" aria-valuemin="0" aria-valuemax="100" data-ng-style="{width: progressValue+'%'}"></div> + </div> + </div> + + <div class="sdc-progress-success-wrapper" data-ng-if="progressValue===100"> + <span class="sdc-progress-success"></span> + <span class="sdc-progress-success-title">{{progressMessage}}</span> + </div> + + <div class="sdc-progress-error-wrapper" data-ng-if="progressValue===-1"> + <span class="sdc-progress-error"></span> + <span class="sdc-progress-error-title">{{progressMessage}}</span> + </div> + <!--======================= Top progress var =======================--> + +</div> diff --git a/catalog-ui/app/scripts/directives/layout/top-progress/top-progress.less b/catalog-ui/app/scripts/directives/layout/top-progress/top-progress.less new file mode 100644 index 0000000000..acce826f80 --- /dev/null +++ b/catalog-ui/app/scripts/directives/layout/top-progress/top-progress.less @@ -0,0 +1,58 @@ +.top-progress { + text-align: left; + + .sdc-progress-title { + .n_12_r; + + .progress-percentage { + float: right; + } + } + + .sdc-progress { + position: relative; + display: block; + height: 6px; + background-color: @main_color_o; + border-radius: 3px; + box-shadow: inset 0 1px 2px rgba(0,0,0,.1); + + .progress-bar { + border-radius: 3px; + background-color: @main_color_a; + } + + } + + .sdc-progress-success-wrapper { + display: flex; + align-items: flex-end; + + .sdc-progress-success-title { + .d_12_r; + margin-left: 10px; + } + + .sdc-progress-success { + .sprite-new; + .success-circle; + } + } + + .sdc-progress-error-wrapper { + display: flex; + align-items: flex-end; + + .sdc-progress-error-title { + .q_12_r; + margin-left: 10px; + } + + .sdc-progress-error { + .sprite-new; + .error-icon; + } + + } + +} diff --git a/catalog-ui/app/scripts/directives/layout/top-progress/top-progress.ts b/catalog-ui/app/scripts/directives/layout/top-progress/top-progress.ts new file mode 100644 index 0000000000..8e8a289281 --- /dev/null +++ b/catalog-ui/app/scripts/directives/layout/top-progress/top-progress.ts @@ -0,0 +1,57 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ +/// <reference path="../../../references"/> +module Sdc.Directives { + 'use strict'; + + export interface ITopProgressScope extends ng.IScope { + progressValue:number; + progressMessage:string; + } + + export class TopProgressDirective implements ng.IDirective { + + constructor(private $templateCache:ng.ITemplateCacheService) {} + + public replace = true; + public restrict = 'E'; + public transclude = false; + + scope = { + progressValue: '=', + progressMessage: '=' + }; + + template = ():string => { + return this.$templateCache.get('/app/scripts/directives/layout/top-progress/top-progress.html'); + }; + + public link = (scope:ITopProgressScope, $elem:ng.IAugmentedJQuery, $attrs:angular.IAttributes) => { + + }; + + public static factory = ($templateCache:ng.ITemplateCacheService)=> { + return new TopProgressDirective($templateCache); + }; + + } + + TopProgressDirective.factory.$inject = ['$templateCache']; +} diff --git a/catalog-ui/app/scripts/directives/loader/loader-directive.html b/catalog-ui/app/scripts/directives/loader/loader-directive.html new file mode 100644 index 0000000000..e40b059a57 --- /dev/null +++ b/catalog-ui/app/scripts/directives/loader/loader-directive.html @@ -0,0 +1,4 @@ +<div data-ng-if="display" data-tests-id="tlv-loader"> + <div class="tlv-loader-back " data-ng-class="{'tlv-loader-relative':relative}"></div> + <div class="tlv-loader {{size}}"></div> +</div> diff --git a/catalog-ui/app/scripts/directives/loader/loader-directive.less b/catalog-ui/app/scripts/directives/loader/loader-directive.less new file mode 100644 index 0000000000..ae0b41aab1 --- /dev/null +++ b/catalog-ui/app/scripts/directives/loader/loader-directive.less @@ -0,0 +1,74 @@ +.tlv-loader-back { + background-color: @main_color_p; + position: fixed; + top: 50px; + left: 0; + right: 0; + bottom: 0; + z-index: 9999; + opacity: 0.5; +} + +.tlv-loader-relative { position: absolute; top: 0;} + +.tlv-loader { + z-index: 10002; +} + +@keyframes fadein { + from { opacity: 0; } + to { opacity: 0.8; } +} + +/* Firefox < 16 */ +@-moz-keyframes fadein { + from { opacity: 0; } + to { opacity: 0.8; } +} + +/* Safari, Chrome and Opera > 12.1 */ +@-webkit-keyframes fadein { + from { opacity: 0; } + to { opacity: 0.8; } +} + +/* Internet Explorer */ +@-ms-keyframes fadein { + from { opacity: 0; } + to { opacity: 0.8; } +} + +/* Opera < 12.1 */ +@-o-keyframes fadein { + from { opacity: 0; } + to { opacity: 0.8; } +} + +@keyframes fadeout { + from { opacity: 0.8; } + to { opacity: 0; } +} + +/* Firefox < 16 */ +@-moz-keyframes fadeout { + from { opacity: 0.8; } + to { opacity: 0; } +} + +/* Safari, Chrome and Opera > 12.1 */ +@-webkit-keyframes fadeout { + from { opacity: 0.8; } + to { opacity: 0; } +} + +/* Internet Explorer */ +@-ms-keyframes fadeout { + from { opacity: 0.8; } + to { opacity: 0; } +} + +/* Opera < 12.1 */ +@-o-keyframes fadeout { + from { opacity: 0.8; } + to { opacity: 0; } +} diff --git a/catalog-ui/app/scripts/directives/loader/loader-directive.ts b/catalog-ui/app/scripts/directives/loader/loader-directive.ts new file mode 100644 index 0000000000..77c8977ac5 --- /dev/null +++ b/catalog-ui/app/scripts/directives/loader/loader-directive.ts @@ -0,0 +1,155 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ +/// <reference path="../../references"/> +module Sdc.Directives { + 'use strict'; + export interface ILoaderScope extends ng.IScope { + display: boolean; // Toggle show || hide scroll + size: string; // small || medium || large + elementSelector: string; // Jquery selector to hide and scroll inside + relative: boolean; // Will use the parent of <loader> element and hide it and scroll inside + loaderType: string; + } + + export class LoaderDirective implements ng.IDirective { + + constructor(private $templateCache: ng.ITemplateCacheService, private EventListenerService: Services.EventListenerService) { + } + + /* + * relative is used when inserting the HTML loader inside some div <loader data-display="isLoading" relative="true"></loader> + * elementSelector when we want to pass the Jquery selector of the loader. + */ + scope = { + display: '=', + size: '@?', + elementSelector: '@?', + relative: '=?', + loaderType: '@?' + }; + + public replace = false; + public restrict = 'E'; + template = (): string => { + return this.$templateCache.get('/app/scripts/directives/loader/loader-directive.html'); + }; + + link = (scope: ILoaderScope, element: any) => { + + let interval; + + this.EventListenerService.registerObserverCallback(Utils.Constants.EVENTS.SHOW_LOADER_EVENT, (loaderType)=> { + if (scope.loaderType !== loaderType) { + return; + } + scope.display = true; + }); + this.EventListenerService.registerObserverCallback(Utils.Constants.EVENTS.HIDE_LOADER_EVENT, (loaderType)=> { + if (scope.loaderType !== loaderType) { + return; + } + scope.display = false; + }); + + let calculateSizesForFixPosition = (positionStyle: string): void => { + // This is problematic, I do not want to change the parent position. + // set the loader on all the screen + let parentPosition = element.parent().position(); + let parentWidth = element.parent().width(); + let parentHeight = element.parent().height(); + element.css('position', positionStyle); + element.css('top', parentPosition.top); + element.css('left', parentPosition.left); + element.css('width', parentWidth); + element.css('height', parentHeight); + }; + + let setStyle = (positionStyle: string): void => { + + switch (positionStyle) { + case 'absolute': + case 'fixed': + // The parent size is not set yet, still loading, so need to use interval to update the size. + interval = window.setInterval(()=> { + calculateSizesForFixPosition(positionStyle); + }, 2000); + break; + default: + // Can change the parent position to relative without causing style issues. + element.parent().css('position', 'relative'); + break; + } + }; + + // This should be executed after the dom loaded + window.setTimeout((): void => { + + element.css('display', 'none'); + + if (scope.elementSelector) { + let elemParent = angular.element(scope.elementSelector); + let positionStyle: string = elemParent.css('position'); + setStyle(positionStyle); + } + + if (scope.relative === true) { + let positionStyle: string = element.parent().css('position'); + setStyle(positionStyle); + } + + if (!scope.size) { + scope.size = 'large'; + } + + }, 0); + + if (scope.elementSelector) { + + } + + function cleanUp() { + clearInterval(interval); + } + + scope.$watch("display", (newVal, oldVal) => { + element.css('display', 'none'); + if (newVal === true) { + window.setTimeout((): void => { + element.css('display', 'block'); + }, 500); + } else { + window.setTimeout((): void => { + element.css('display', 'none'); + }, 0); + } + }); + + scope.$on('$destroy', cleanUp); + + }; + + public static factory = ($templateCache: ng.ITemplateCacheService, EventListenerService: Services.EventListenerService)=> { + return new LoaderDirective($templateCache, EventListenerService); + }; + + } + + LoaderDirective.factory.$inject = ['$templateCache', 'EventListenerService']; +} diff --git a/catalog-ui/app/scripts/directives/modal/sdc-modal.html b/catalog-ui/app/scripts/directives/modal/sdc-modal.html new file mode 100644 index 0000000000..a8419f162d --- /dev/null +++ b/catalog-ui/app/scripts/directives/modal/sdc-modal.html @@ -0,0 +1,18 @@ +<div data-ng-class="{'w-sdc-modal': type===undefined, 'w-sdc-classic-modal': type==='classic'}"> + <div class="w-sdc-modal-head"> + <span data-ng-if="header" class="w-sdc-modal-head-text">{{header}}</span> + <span data-ng-if="headerTranslate" class="w-sdc-modal-head-text" translate="{{headerTranslate}}" translate-values="{{headerTranslateValues}}"></span> + <div data-ng-if="showCloseButton==='true'" class="w-sdc-modal-close" data-ng-click="cancel()"></div> + </div> + <div class="w-sdc-modal-body" data-ng-class="{'classic': type==='classic'}"> + <ng-transclude></ng-transclude> + </div> + <div class="w-sdc-modal-footer" data-ng-if="type==='classic' && buttons!==undefined"> + <button data-ng-repeat="button in buttons" + data-tests-id="{{button.name}}" + class="tlv-btn {{button.css}}" + data-ng-class="{'disabled': button.disabled===true}" + data-ng-disabled="button.disabled===true" + data-ng-click="button.callback()">{{button.name}}</button> + </div> +</div> diff --git a/catalog-ui/app/scripts/directives/modal/sdc-modal.less b/catalog-ui/app/scripts/directives/modal/sdc-modal.less new file mode 100644 index 0000000000..d8dfdbb73b --- /dev/null +++ b/catalog-ui/app/scripts/directives/modal/sdc-modal.less @@ -0,0 +1,10 @@ +.ellipsis-directive-more-less { + .a_9; + .bold; + .hand; + float: right; + margin-right: 17px; + line-height: 23px; + text-decoration: underline; + text-align: left; +} diff --git a/catalog-ui/app/scripts/directives/modal/sdc-modal.ts b/catalog-ui/app/scripts/directives/modal/sdc-modal.ts new file mode 100644 index 0000000000..338035c9f1 --- /dev/null +++ b/catalog-ui/app/scripts/directives/modal/sdc-modal.ts @@ -0,0 +1,103 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ +/// <reference path="../../references"/> +module Sdc.Directives { + 'use strict'; + + export interface ISdcModalScope extends ng.IScope { + modal:ng.ui.bootstrap.IModalServiceInstance; + hideBackground:string; + ok():void; + close(result:any):void; + cancel(reason:any):void; + } + + export interface ISdcModalButton { + name:string; + css:string; + disabled?:boolean; + callback:Function; + } + + export class SdcModalDirective implements ng.IDirective { + + constructor( + private $templateCache: ng.ITemplateCacheService + ) {} + + scope = { + modal: '=', + type: '@', + header: '@', + headerTranslate: '@', + headerTranslateValues: '@', + showCloseButton: '@', + hideBackground: '@', + buttons: '=', + getCloseModalResponse: '=' + }; + + public replace = true; + public restrict = 'E'; + public transclude = true; + + template = (): string => { + return this.$templateCache.get('/app/scripts/directives/modal/sdc-modal.html'); + }; + + link = (scope:ISdcModalScope, $elem:any) => { + + if (scope.hideBackground==="true"){ + $(".modal-backdrop").css('opacity','0'); + } + + scope.close = function (result:any) { + scope.modal.close(result); + }; + + scope.ok = function () { + scope.modal.close(); + }; + + scope.cancel = function (reason:any) { + if(this.getCloseModalResponse) + scope.modal.dismiss(this.getCloseModalResponse()); + else { + scope.modal.dismiss(); + } + }; + + if (scope.modal) { + scope.modal.result.then(function (selectedItem) { + //$scope.selected = selectedItem; + }, function () { + //console.info('Modal dismissed at: ' + new Date()); + }); + } + } + + public static factory = ($templateCache: ng.ITemplateCacheService)=> { + return new SdcModalDirective($templateCache); + }; + + } + + SdcModalDirective.factory.$inject = ['$templateCache']; +} diff --git a/catalog-ui/app/scripts/directives/page-scroller/page-scroller.html b/catalog-ui/app/scripts/directives/page-scroller/page-scroller.html new file mode 100644 index 0000000000..7359386901 --- /dev/null +++ b/catalog-ui/app/scripts/directives/page-scroller/page-scroller.html @@ -0,0 +1,22 @@ +<div class="sdc-page-scroller"> + + <nav data-ng-if="showNav!==false" class="welcome-nav"> + <div data-ng-if="showCloseButton===true" data-ng-click="onCloseButtonClick()" class="asdc-welcome-close"></div> + <ul> + <li data-ng-repeat="slide in slidesData | orderBy:'+position'"><a href="#{{slide.id}}" data-ng-click="onNavButtonClick(slide)" class=""></a></li> + </ul> + </nav> + + <div class="nav-previous-next" data-ng-if="showPreviousNext===true"> + <span class="go-prev" data-ng-click="goToPrevSlide()">previous slide</span> + <span class="go-next" data-ng-click="goToNextSlide()">next slide</span> + </div> + + <div class="slides-container"> + <section data-ng-repeat="slide in slidesData | orderBy:'+position'" class="slide" id="{{slide.id}}" on-last-repeat> + <ng-include src="slide.url"></ng-include> + </section> + </div> + +</div> + diff --git a/catalog-ui/app/scripts/directives/page-scroller/page-scroller.less b/catalog-ui/app/scripts/directives/page-scroller/page-scroller.less new file mode 100644 index 0000000000..14f8568f07 --- /dev/null +++ b/catalog-ui/app/scripts/directives/page-scroller/page-scroller.less @@ -0,0 +1,98 @@ +.sdc-page-scroller { + + /****************** Navigation ***************/ + nav { + position: fixed; + top: 0; + right: 0; + z-index: 100; + display: flex; + flex-direction: column; + width: 100px; + bottom: 0; + background-color: #000; + align-items: center; + justify-content: center; + } + + nav ul { + list-style: none; + text-align: center; + margin-top: 0; + padding: 0; + } + + nav ul li { + display: block; + margin-bottom: 15px; + + } + + nav ul li:last-child { + + } + + nav a { + display: block; + height: 6px; + width: 6px; + border-radius: 50%; + background-color: #4a4c4d; + } + + nav a.active { + position: relative; + } + + nav a.active::after { + content: ''; + display: block; + position: absolute; + border: 2px solid #0198d1; + width: 16px; + height: 16px; + border-radius: 50%; + top: -5px; + left: -5px; + } + + /****************** Previous Next navigation ***************/ + .go-prev, .go-next { + cursor: pointer; + font-weight: bold; + text-decoration: underline; + } + + .slides-container { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + overflow-y: hidden; + z-index: 10; + } + + .slide { + position: relative; + width: 100%; + height: 100%; + overflow: hidden; + } + + .slide .centered { + width: 60%; + margin: 200px auto 0; + } + + .slide .centered h1 { + text-align: center; + } + + .slide .centered p { + text-align: center; + margin-top: 20px; + font-size: 20px; + } + +} diff --git a/catalog-ui/app/scripts/directives/page-scroller/page-scroller.ts b/catalog-ui/app/scripts/directives/page-scroller/page-scroller.ts new file mode 100644 index 0000000000..bb89f9a55a --- /dev/null +++ b/catalog-ui/app/scripts/directives/page-scroller/page-scroller.ts @@ -0,0 +1,247 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ +/// <reference path="../../references"/> +module Sdc.Directives { + 'use strict'; + + export interface SlideData { + url: string; + id: string; + index: number; + callback: Function; + } + + export interface ISdcPageScrollDirectiveScope extends ng.IScope { + slidesData:Array<SlideData>; + showNav: boolean; + showPreviousNext: boolean; + currentSlide:SlideData; + showCloseButton:boolean; + closeButtonCallback:Function; + startSlideIndex:number; + + onNavButtonClick(slideName):void; + onCloseButtonClick():void; + goToPrevSlide():void; + goToNextSlide():void; + goToSlide(slide:SlideData):void; + onSlideChangeEnd():void; + onMouseWheel(event):void; + onKeyDown(event):void; + onResize(event):void; + gotoSlideIndex(index):void; + } + + export class SdcPageScrollDirective implements ng.IDirective { + + constructor(private $templateCache:ng.ITemplateCacheService) { + + } + + scope = { + slidesData: '=', + showNav: '=', + showPreviousNext: '=', + showCloseButton: '=', + closeButtonCallback: '=', + startSlideIndex: '=?' + }; + + public replace = true; + public restrict = 'E'; + private delayExec:any; + + template = ():string => { + return this.$templateCache.get('/app/scripts/directives/page-scroller/page-scroller.html'); + }; + + link = ($scope:ISdcPageScrollDirectiveScope, $elem:JQuery, attr:any) => { + let isAnimating = false; //Animating flag - is our app animating + let pageHeight = $(window).innerHeight(); //The height of the window + let slidesContainer; + let navButtons; + let slides:any; //Only graph-links that starts with + + //Key codes for up and down arrows on keyboard. We'll be using this to navigate change slides using the keyboard + let keyCodes = { + UP : 38, + DOWN: 40 + }; + + $scope.onCloseButtonClick = ():void => { + if ($scope.closeButtonCallback){ + $scope.closeButtonCallback(); + }; + }; + + // Wait for the dom to load (after ngRepeat). + $scope.$on('onRepeatLast', (scope, element, attrs) => { + slides = $(".slide", slidesContainer); + slidesContainer = $(".slides-container"); + navButtons = $("nav a").filter("[href^='#']"); + + // Adding event listeners + $(window).on("resize", (e) => {$scope.onResize(e);}).resize(); + $(window).on("mousewheel DOMMouseScroll", (e) => {$scope.onMouseWheel(e);}); + $(document).on("keydown", (e) => {$scope.onKeyDown(e);}); + + //Going to the first slide + if ($scope.startSlideIndex){ + $scope.gotoSlideIndex($scope.startSlideIndex); + } else { + $scope.gotoSlideIndex(0); + } + + }); + + $scope.gotoSlideIndex = (index) => { + $scope.goToSlide($scope.slidesData[index]); + }; + + // When a button is clicked - first get the button href, and then slide to the container, if there's such a container + $scope.onNavButtonClick = (slide:SlideData):void => { + $scope.goToSlide(slide); + }; + + // If there's a previous slide, slide to it + $scope.goToPrevSlide = ():void => { + let previousSlide = $scope.slidesData[$scope.currentSlide.index-1]; + if (previousSlide) { + $scope.goToSlide(previousSlide); + } + }; + + // If there's a next slide, slide to it + $scope.goToNextSlide = ():void => { + let nextSlide = $scope.slidesData[$scope.currentSlide.index+1]; + if (nextSlide) { + $scope.goToSlide(nextSlide); + } + }; + + // Actual transition between slides + $scope.goToSlide = (slide:SlideData):void => { + //console.log("start goToSlide"); + //If the slides are not changing and there's such a slide + if(!isAnimating && slide) { + //setting animating flag to true + isAnimating = true; + $scope.currentSlide = slide; + $scope.currentSlide.callback(); + + //Sliding to current slide + let calculatedY = pageHeight * ($scope.currentSlide.index); + //console.log("$scope.currentSlide.index: " + $scope.currentSlide.index + " | calculatedY: " + calculatedY); + + $('.slides-container').animate( + { + scrollTop: calculatedY + 'px' + }, + { + duration: 1000, + specialEasing: { + width: "linear", + height: "easeInOutQuart" + }, + complete: function() { + $scope.onSlideChangeEnd(); + } + } + ); + + //Animating menu items + $(".sdc-page-scroller nav a.active").removeClass("active"); + $(".sdc-page-scroller nav [href='#" + $scope.currentSlide.id + "']").addClass("active"); + } + }; + + // Once the sliding is finished, we need to restore "isAnimating" flag. + // You can also do other things in this function, such as changing page title + $scope.onSlideChangeEnd = ():void => { + + + + isAnimating = false; + }; + + // When user scrolls with the mouse, we have to change slides + $scope.onMouseWheel = (event):void => { + //Normalize event wheel delta + let delta = event.originalEvent.wheelDelta / 30 || -event.originalEvent.detail; + + //If the user scrolled up, it goes to previous slide, otherwise - to next slide + if(delta < -1) { + this.delayAction($scope.goToNextSlide); + } else if(delta > 1) { + this.delayAction($scope.goToPrevSlide); + } + event.preventDefault(); + }; + + // Getting the pressed key. Only if it's up or down arrow, we go to prev or next slide and prevent default behaviour + // This way, if there's text input, the user is still able to fill it + $scope.onKeyDown = (event):void => { + let PRESSED_KEY = event.keyCode; + + if(PRESSED_KEY == keyCodes.UP){ + $scope.goToPrevSlide(); + event.preventDefault(); + } else if(PRESSED_KEY == keyCodes.DOWN){ + $scope.goToNextSlide(); + event.preventDefault(); + } + }; + + // When user resize it's browser we need to know the new height, so we can properly align the current slide + $scope.onResize = (event):void => { + //This will give us the new height of the window + let newPageHeight = $(window).innerHeight(); + + // If the new height is different from the old height ( the browser is resized vertically ), the slides are resized + if(pageHeight !== newPageHeight) { + pageHeight = newPageHeight; + } + }; + }; + + private initSlides = ():void => { + //pageHeight + }; + + private delayAction = (action:Function):void => { + clearTimeout(this.delayExec); + this.delayExec = setTimeout(function () { + action(); + }, 100); + }; + + public static factory = ($templateCache:ng.ITemplateCacheService)=> { + return new SdcPageScrollDirective($templateCache); + }; + + } + + SdcPageScrollDirective.factory.$inject = ['$templateCache']; + +} + + + + diff --git a/catalog-ui/app/scripts/directives/perfect-scrollbar/angular-perfect-scrollbar.ts b/catalog-ui/app/scripts/directives/perfect-scrollbar/angular-perfect-scrollbar.ts new file mode 100644 index 0000000000..b53a059a40 --- /dev/null +++ b/catalog-ui/app/scripts/directives/perfect-scrollbar/angular-perfect-scrollbar.ts @@ -0,0 +1,159 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ +/// <reference path="../../references"/> +module Sdc.Directives { + + 'use strict'; + + export interface IPerfectScrollerScope extends ng.IScope { + //update(event:string): void; + } + + export class PerfectScrollerDirective implements ng.IDirective { + + constructor( + private $templateCache: ng.ITemplateCacheService, + private $parse:any, + private $window:any) { + + } + + replace = true; + restrict = 'EA'; + transclude = true; + + template = (): string => { + return '<div><div ng-transclude></div></div>'; + }; + + link = ($scope:IPerfectScrollerScope, $elem, $attr) => { + let self = this; + let options = {}; + + let psOptions = [ + 'wheelSpeed', 'wheelPropagation', 'minScrollbarLength', 'useBothWheelAxes', + 'useKeyboard', 'suppressScrollX', 'suppressScrollY', 'scrollXMarginOffset', + 'scrollYMarginOffset', 'includePadding'//, 'onScroll', 'scrollDown' + ]; + + for (let i=0, l=psOptions.length; i<l; i++) { + let opt = psOptions[i]; + if ($attr[opt] !== undefined) { + options[opt] = self.$parse($attr[opt])(); + } + } + + $scope.$evalAsync(function() { + $elem.perfectScrollbar(options); + let onScrollHandler = self.$parse($attr.onScroll) + $elem.scroll(function(){ + let scrollTop = $elem.scrollTop() + let scrollHeight = $elem.prop('scrollHeight') - $elem.height() + $scope.$apply(function() { + onScrollHandler($scope, { + scrollTop: scrollTop, + scrollHeight: scrollHeight + }) + }) + }); + }); + + /* + $scope.update = (event:string): void => { + $scope.$evalAsync(function() { + //if ($attr.scrollDown == 'true' && event != 'mouseenter') { + if (event != 'mouseenter') { + setTimeout(function () { + $($elem).scrollTop($($elem).prop("scrollHeight")); + }, 100); + } + $elem.perfectScrollbar('update'); + }); + }; + */ + + // This is necessary when you don't watch anything with the scrollbar + $elem.bind('mouseenter', function(){ + //console.log("mouseenter"); + $elem.perfectScrollbar('update'); + }); + + $elem.bind('mouseleave', function(){ + //console.log("mouseleave"); + setTimeout(function () { + $(window).trigger('mouseup'); + $elem.perfectScrollbar('update'); + }, 10); + }); + + $elem.bind('click', function(){ + //console.log("click"); + // Wait 500 milliseconds until the collapse finish closing and update. + setTimeout(function () { + $elem.perfectScrollbar('update'); + }, 500); + }); + + /** + * Check if the content of the scroller was changed, and if changed update the scroller. + * Because DOMSubtreeModified event is fire many time (while filling the content), I'm checking that + * there is at least 100 milliseconds between DOMSubtreeModified events to update the scrollbar. + * @type {boolean} + */ + let insideDOMSubtreeModified=false; + $elem.bind('DOMSubtreeModified', function(){ + if (insideDOMSubtreeModified==false) { + insideDOMSubtreeModified=true; + setTimeout(function () { + insideDOMSubtreeModified=false; + $elem.perfectScrollbar('update'); + }, 100); + } + }); + + // Possible future improvement - check the type here and use the appropriate watch for non-arrays + if ($attr.refreshOnChange) { + $scope.$watchCollection($attr.refreshOnChange, function() { + $elem.perfectScrollbar('update'); + }); + } + + /* + // this is from a pull request - I am not totally sure what the original issue is but seems harmless + if ($attr.refreshOnResize) { + self.$window.on('resize', function(e){$scope.update(e)}); + } + */ + + $elem.bind('$destroy', function() { + //self.$window.off('resize', function(e){$scope.update(e)}); + $elem.perfectScrollbar('destroy'); + }); + + }; + + public static factory = ($templateCache: ng.ITemplateCacheService, $parse:any, $window:any)=> { + return new PerfectScrollerDirective($templateCache, $parse, $window); + }; + + } + + PerfectScrollerDirective.factory.$inject = ['$templateCache','$parse','$window']; +} diff --git a/catalog-ui/app/scripts/directives/print-graph-screen/print-graph-screen.ts b/catalog-ui/app/scripts/directives/print-graph-screen/print-graph-screen.ts new file mode 100644 index 0000000000..8204928e6f --- /dev/null +++ b/catalog-ui/app/scripts/directives/print-graph-screen/print-graph-screen.ts @@ -0,0 +1,211 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ +/// <reference path="../../references"/> +module Sdc.Directives { + 'use strict'; + + export interface IPrintGraphScreenScope extends ng.IScope { + entity:Models.Components.Component; + } + + + export class PrintGraphScreenDirective implements ng.IDirective { + + constructor( + private $filter: ng.IFilterService, + private sdcMenu:Models.IAppMenu, + private sdcConfig:Models.IAppConfigurtaion, + private urlToBase64Service:Sdc.Services.UrlToBase64Service + ) {} + + scope = { + entity: '=' + }; + restrict = 'A'; + link = (scope:IPrintGraphScreenScope, element:any) => { + + + element.bind('click', function() { + printScreen(); + }); + + + // TODO we need to implement export to PDF in cytoscape + let printScreen = ():void => { + + // + // let pdf :any = new jsPDF('landscape', 'mm', 'a4'); + // pdf.setProperties({ + // title: scope.entity.name, + // subject: 'Design Snapshot for ' + scope.entity.name, + // author: scope.entity.creatorFullName, + // keywords: scope.entity.tags.join(', '), + // creator: scope.entity.creatorFullName + // }); + // + // // A4 measures is 210 × 297 millimeters + // let pdfWidth :number = 297, + // pdfHeight :number = 210, + // leftColumnWidth :number = 80; + // + // //left bar background + // pdf.setDrawColor(0); + // pdf.setFillColor(248, 249, 251); + // pdf.rect(0, 0, leftColumnWidth, pdfHeight, 'F'); + // + // //entity name + // pdf.setFontSize(12); + // pdf.setTextColor(38, 61, 77); + // let splitTitle :any = pdf.splitTextToSize(scope.entity.name, 50); + // pdf.text(22, 15 - (splitTitle.length - 1) * 2, splitTitle); + // + // //line + // pdf.setLineWidth(0.2); + // pdf.setDrawColor(208, 209, 213); + // pdf.line(0, 28, leftColumnWidth, 28); + // + // + // pdf.setFontSize(10); + // let properties :any = getPdfProperties(); + // + // let topOffset :number = 39, lines; + // properties.forEach( (item:any) => { + // if (!item.value) { + // return; + // } + // if (item.title === 'Description:') { + // topOffset += 5; + // } + // + // pdf.setTextColor(38, 61, 77); + // pdf.text(5, topOffset, item.title); + // pdf.setTextColor(102, 102, 102); + // lines = pdf.splitTextToSize(item.value, 49); + // pdf.text(5 + item.offset, topOffset, lines[0]); + // if (lines.length > 1) { + // lines = pdf.splitTextToSize(item.value.substring(lines[0].length + 1), 65); + // if (lines.length > 8) { + // lines = lines.slice(0, 7); + // lines[lines.length - 1] += '...'; + // } + // pdf.text(5, topOffset + 4, lines); + // topOffset += 4 * (lines.length); + // } + // + // topOffset += 6; + // }); + // + // + // //another background in case the text was too long + // let declarationLineOffset :number = 176; + // pdf.setDrawColor(0); + // pdf.setFillColor(248, 249, 251); + // pdf.rect(0, declarationLineOffset, leftColumnWidth, pdfHeight - declarationLineOffset, 'F'); + // //line + // pdf.setLineWidth(0.2); + // pdf.setDrawColor(208, 209, 213); + // pdf.line(0, declarationLineOffset, leftColumnWidth, declarationLineOffset); + // + // //declaration + // pdf.setFontSize(10.5); + // pdf.setTextColor(38, 61, 77); + // pdf.text(5, 185, 'Declaration'); + // pdf.setFontSize(9); + // pdf.setTextColor(102, 102, 102); + // pdf.setFontType('bold'); + // pdf.text(5, 190, this.$filter('translate')('PDF_FILE_DECLARATION_BOLD')); + // pdf.setFontType('normal'); + // pdf.text(5, 194, pdf.splitTextToSize(this.$filter('translate')('PDF_FILE_DECLARATION'), 65)); + // + // //entity icon + // let self = this; + // let addEntityIcon:Function = () => { + // let iconPath:string = self.sdcConfig.imagesPath + '/styles/images/'; + // if (scope.entity.isService()) { + // iconPath += 'service-icons/' + scope.entity.icon + '.png'; + // } else { + // iconPath += 'resource-icons/' + scope.entity.icon + '.png'; + // } + // self.urlToBase64Service.downloadUrl(iconPath, (base64string:string):void => { + // if (base64string) { + // pdf.addImage(base64string, 'JPEG', 5, 7, 15, 15); + // } + // pdf.save(scope.entity.name + '.pdf'); + // }); + // }; + // + // //actual snapshop of canvas + // + // let diagramDiv :any = document.getElementById('myDiagram'); + // let diagram :any = null;// Sdc.Graph.Diagram.fromDiv(diagramDiv), canvasImg = new Image(); + // diagram.startTransaction('print screen'); + // let canvasImgBase64:any = diagram.makeImageData({ + // //scale: 1, + // // size: new Sdc.Graph.Size(pdfHeight * 5, NaN), + // background: 'white', + // type: 'image/jpeg' + // }); + // diagramDiv.firstElementChild.toDataURL(); + // diagram.commitTransaction('print screen'); + // + // canvasImg.onload = () => { + // if (canvasImg.height > 0) { + // let canvasImgRatio:number = Math.min((pdfWidth - leftColumnWidth - 15) / canvasImg.width, pdfHeight / canvasImg.height); + // let canvasImgWidth:number = canvasImg.width * canvasImgRatio, + // canvasImgHeight:number = canvasImg.height * canvasImgRatio; + // let canvasImgOffset:number = (pdfHeight - canvasImgHeight) / 2; + // pdf.addImage(canvasImg, 'JPEG', leftColumnWidth, canvasImgOffset, canvasImgWidth, canvasImgHeight); + // + // addEntityIcon(); + // } + // }; + // + // if(canvasImg.src === 'data:,') { //empty canvas + // addEntityIcon(); + // } else { + // canvasImg.src = canvasImgBase64; + // } + }; + + let getPdfProperties = ():Array<any> => { + return [ + {title: this.$filter('translate')('GENERAL_LABEL_TYPE'), value: scope.entity.getComponentSubType(), offset: 10}, + {title: this.$filter('translate')('GENERAL_LABEL_VERSION'), value: scope.entity.version, offset: 15}, + {title: this.$filter('translate')('GENERAL_LABEL_CATEGORY'), value: scope.entity.categories.length ? scope.entity.categories[0].name : '', offset: 16}, + {title: this.$filter('translate')('GENERAL_LABEL_CREATION_DATE'), value: this.$filter('date')(scope.entity.creationDate, 'MM/dd/yyyy'), offset: 24}, + {title: this.$filter('translate')('GENERAL_LABEL_AUTHOR'), value: scope.entity.creatorFullName, offset: 13}, + {title: this.$filter('translate')('GENERAL_LABEL_CONTACT_ID'), value: scope.entity.contactId, offset: 41}, + {title: this.$filter('translate')('GENERAL_LABEL_STATUS'), value: (<any>this.sdcMenu).LifeCycleStatuses[scope.entity.lifecycleState].text, offset: 13}, + {title: this.$filter('translate')('GENERAL_LABEL_PROJECT_CODE'), value: scope.entity.projectCode, offset: 15}, + {title: this.$filter('translate')('GENERAL_LABEL_DESCRIPTION'), value: scope.entity.description, offset: 20}, + {title: this.$filter('translate')('GENERAL_LABEL_TAGS'), value: scope.entity.tags.join(', '), offset: 10} + ]; + }; + + }; + + public static factory = ($filter:ng.IFilterService, sdcMenu:Models.IAppMenu, sdcConfig:Models.IAppConfigurtaion, urlToBase64Service:Sdc.Services.UrlToBase64Service)=> { + return new PrintGraphScreenDirective($filter, sdcMenu, sdcConfig, urlToBase64Service); + }; + + } + + PrintGraphScreenDirective.factory.$inject = ['$filter', 'sdcMenu', 'sdcConfig', 'Sdc.Services.UrlToBase64Service']; +} diff --git a/catalog-ui/app/scripts/directives/property-types/data-type-fields-structure/data-type-fields-structure.html b/catalog-ui/app/scripts/directives/property-types/data-type-fields-structure/data-type-fields-structure.html new file mode 100644 index 0000000000..b4583fd304 --- /dev/null +++ b/catalog-ui/app/scripts/directives/property-types/data-type-fields-structure/data-type-fields-structure.html @@ -0,0 +1,82 @@ +<div class="data-type-fields-structure"> + <div class="open-close"> + <div class="open-close-button" data-ng-class="{'expand':expand,'collapse':!expand}" data-ng-click="expandAndCollapse()"></div> + <span class="data-type-name">{{typeName.replace("org.openecomp.datatypes.heat.","")}}</span> + </div> + <div data-ng-show="expand" data-ng-repeat="property in dataTypeProperties" class="property"> + <div class="i-sdc-form-item property-name"> + <div tooltips tooltip-content="{{property.name}}"> + <input class="i-sdc-form-input" + type="text" + data-ng-disabled="true" + value="{{property.name}}"/> + </div> + </div> + <!--<div class="property-value">--> + <div data-ng-if="dataTypesService.isDataTypeForDataTypePropertyType(property,types)" class="inner-structure"> + <fields-structure value-obj-ref="(valueObjRef[property.name])" + type-name="property.type" + parent-form-obj="parentFormObj" + fields-prefix-name="fieldsPrefixName+property.name" + read-only="readOnly" + default-value="{{currentTypeDefaultValue[property.name]}}" + types="types"></fields-structure> + </div> + <div data-ng-if="!dataTypesService.isDataTypeForDataTypePropertyType(property,types)" ng-switch="property.type"> + <div ng-switch-when="map"> + <type-map value-obj-ref="valueObjRef[property.name]" + schema-property="property.schema.property" + parent-form-obj="parentFormObj" + fields-prefix-name="fieldsPrefixName+property.name" + read-only="readOnly" + default-value="{{currentTypeDefaultValue[property.name]}}" + types="types"></type-map> + </div> + <div ng-switch-when="list"> + <type-list value-obj-ref="valueObjRef[property.name]" + schema-property="property.schema.property" + parent-form-obj="parentFormObj" + fields-prefix-name="fieldsPrefixName+property.name" + read-only="readOnly" + default-value="{{currentTypeDefaultValue[property.name]}}" + types="types"></type-list> + </div> + <div ng-switch-default class="primitive-value-field"> + <div class="i-sdc-form-item" data-ng-class="{error:(parentFormObj[fieldsPrefixName+property.name].$dirty && parentFormObj[fieldsPrefixName+property.name].$invalid)}"> + <input class="i-sdc-form-input" + data-tests-id="{{fieldsPrefixName+property.name}}" + ng-if="!((property.simpleType||property.type) == 'boolean')" + data-ng-maxlength="100" + data-ng-disabled="readOnly" + maxlength="100" + data-ng-model="valueObjRef[property.name]" + type="text" + name="{{fieldsPrefixName+property.name}}" + data-ng-pattern="getValidationPattern((property.simpleType||property.type))" + data-ng-model-options="{ debounce: 200 }" + data-ng-change="!parentFormObj[fieldsPrefixName+property.name].$error.pattern && ('integer'==property.type && parentFormObj[fieldsPrefixName+property.name].$setValidity('pattern', validateIntRange(valueObjRef[property.name])) || onValueChange(property.name, (property.simpleType||property.type)))" + autofocus /> + <select class="i-sdc-form-select" + data-tests-id="{{fieldsPrefixName+property.name}}" + ng-if="(property.simpleType||property.type) == 'boolean'" + data-ng-disabled="readOnly" + name="{{fieldsPrefixName+property.name}}" + data-ng-change="onValueChange(property.name,'boolean')" + data-ng-model="valueObjRef[property.name]" + data-ng-options="option.v as option.n for option in [{ n: '', v: undefined }, { n: 'false', v: false }, { n: 'true', v: true }]"> + </select> + + <div class="input-error" data-ng-show="parentFormObj[fieldsPrefixName+property.name].$dirty && parentFormObj[fieldsPrefixName+property.name].$invalid"> + <span ng-show="parentFormObj[fieldsPrefixName+property.name].$error.maxlength" translate="VALIDATION_ERROR_MAX_LENGTH" translate-values="{'max': '100' }"></span> + <span ng-show="parentFormObj[fieldsPrefixName+property.name].$error.pattern" translate="PROPERTY_EDIT_PATTERN"></span> + <span ng-show="parentFormObj[fieldsPrefixName+property.name].$error.customValidation" translate="PROPERTY_EDIT_MAP_UNIQUE_KEYS"></span> + </div> + </div> + </div> + </div> + <!--</div>--> + + </div> +</div> + + diff --git a/catalog-ui/app/scripts/directives/property-types/data-type-fields-structure/data-type-fields-structure.less b/catalog-ui/app/scripts/directives/property-types/data-type-fields-structure/data-type-fields-structure.less new file mode 100644 index 0000000000..5c65fdc9dc --- /dev/null +++ b/catalog-ui/app/scripts/directives/property-types/data-type-fields-structure/data-type-fields-structure.less @@ -0,0 +1,90 @@ +.data-type-fields-structure{ + background-color: @tlv_color_v; + padding:10px; + display: table-caption; + .open-close{ + position: relative; + .open-close-button{ + position: absolute; + top: 50%; + margin-top: -7px; + &.expand{ + .sprite-new; + .expand-collapse-minus-icon; + } + &.collapse{ + .sprite-new; + .expand-collapse-plus-icon; + } + } + + } + + + .data-type-name{ + .m_16_m; + margin-left: 22px; + } + + .i-sdc-form-input:disabled{ + .disabled; + } + + .property{ + display: flex; + min-width: 365px; + min-height: 46px; + input[type="text"],select{ + width: 170px; + } + .property-name{ + float: left; + margin-top: 8px; + } + .primitive-value-field{ + float: right; + margin-top: 8px; + margin-left: 10px; + } + .inner-structure{ + display: -webkit-box; + } + } + + [ng-switch-when="map"]{ + margin-top: 8px; + margin-left: 10px; + .map-item{ + border: solid 1px @main_color_o; + min-width: 401px; + min-height: 69px; + float: none !important; + } + .add-map-item{ + width: auto; + float: none; + &:nth-child(1){ + position: relative; + top: 6px; + } + .add-btn{ + float: none; + } + } + + } + + [ng-switch-when="list"]{ + float: left; + margin-top: 8px; + margin-left: 10px; + min-width: 280px; + .dt-list-item { + border: solid 1px @main_color_o; + } + .list-value-items{ + width:280px; + } + } +} + diff --git a/catalog-ui/app/scripts/directives/property-types/data-type-fields-structure/data-type-fields-structure.ts b/catalog-ui/app/scripts/directives/property-types/data-type-fields-structure/data-type-fields-structure.ts new file mode 100644 index 0000000000..94567ca36b --- /dev/null +++ b/catalog-ui/app/scripts/directives/property-types/data-type-fields-structure/data-type-fields-structure.ts @@ -0,0 +1,165 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ +/** + * Created by obarda on 1/27/2016. + */ +/// <reference path="../../../references"/> +module Sdc.Directives { + 'use strict'; + + export interface IDataTypeFieldsStructureScope extends ng.IScope { + parentFormObj:ng.IFormController; + dataTypeProperties:Array<Models.DataTypePropertyModel>; + typeName:string; + valueObjRef:any; + propertyNameValidationPattern: RegExp; + fieldsPrefixName:string; + readOnly:boolean; + currentTypeDefaultValue:any; + types:Models.DataTypesMap; + expandByDefault:boolean; + expand:boolean; + expanded:boolean; + dataTypesService:Sdc.Services.DataTypesService; + + expandAndCollapse():void; + getValidationPattern(type:string):RegExp; + validateIntRange(value:string):boolean; + onValueChange(propertyName:string, type:string):void + } + + + export class DataTypeFieldsStructureDirective implements ng.IDirective { + + constructor(private $templateCache:ng.ITemplateCacheService, + private DataTypesService:Sdc.Services.DataTypesService, + private PropertyNameValidationPattern: RegExp, + private ValidationUtils:Sdc.Utils.ValidationUtils) { + } + + scope = { + valueObjRef: '=', + typeName: '=', + parentFormObj: '=', + fieldsPrefixName: '=', + readOnly: '=', + defaultValue: '@', + types: '=', + expandByDefault: '=' + }; + + restrict = 'E'; + replace = true; + template = ():string => { + return this.$templateCache.get('/app/scripts/directives/property-types/data-type-fields-structure/data-type-fields-structure.html'); + }; + public types=Utils.Constants.PROPERTY_DATA.TYPES; + + //get data type properties array and return object with the properties and their default value + //(for example: get: [{name:"prop1",defaultValue:1 ...},{name:"prop2", defaultValue:"bla bla" ...}] + // return: {prop1: 1, prop2: "bla bla"} + private getDefaultValue = (dataTypeProperties:Array<Models.DataTypePropertyModel>):any => { + let defaultValue = {}; + for(let i=0; i < dataTypeProperties.length; i++){ + if(dataTypeProperties[i].type!='string'){ + if(dataTypeProperties[i].defaultValue){ + defaultValue[dataTypeProperties[i].name] = JSON.parse(dataTypeProperties[i].defaultValue); + } + }else{ + defaultValue[dataTypeProperties[i].name] = dataTypeProperties[i].defaultValue; + } + } + return defaultValue; + }; + + private initDataOnScope = (scope:any, $attr:any):void =>{ + scope.dataTypesService = this.DataTypesService; + scope.dataTypeProperties = this.DataTypesService.getFirsLevelOfDataTypeProperties(scope.typeName,scope.types); + if($attr.defaultValue){ + scope.currentTypeDefaultValue = JSON.parse($attr.defaultValue); + }else{ + scope.currentTypeDefaultValue = this.getDefaultValue(scope.dataTypeProperties); + } + + if(!scope.valueObjRef) { + scope.valueObjRef = {}; + } + + _.forEach(scope.currentTypeDefaultValue, (value, key)=> { + if(!scope.valueObjRef[key]){ + if(typeof scope.currentTypeDefaultValue[key] == 'object'){ + angular.copy(scope.currentTypeDefaultValue[key], scope.valueObjRef[key]); + }else{ + scope.valueObjRef[key] = scope.currentTypeDefaultValue[key]; + } + } + }); + }; + + private rerender = (scope:any):void =>{ + scope.expanded = false; + scope.expand = false; + if(scope.expandByDefault){ + scope.expandAndCollapse(); + } + }; + + link = (scope:IDataTypeFieldsStructureScope, element:any, $attr:any) => { + scope.propertyNameValidationPattern = this.PropertyNameValidationPattern; + + scope.$watchCollection('[typeName,fieldsPrefixName]', (newData:any):void => { + this.rerender(scope); + }); + + + scope.expandAndCollapse = ():void => { + if(!scope.expanded){ + this.initDataOnScope(scope,$attr); + scope.expanded=true; + } + scope.expand=!scope.expand; + }; + + scope.getValidationPattern = (type:string):RegExp => { + return this.ValidationUtils.getValidationPattern(type); + }; + + scope.validateIntRange = (value:string):boolean => { + return !value || this.ValidationUtils.validateIntRange(value); + }; + + scope.onValueChange = (propertyName:string, type:string):void => { + scope.valueObjRef[propertyName] = !angular.isUndefined(scope.valueObjRef[propertyName]) ? scope.valueObjRef[propertyName] : scope.currentTypeDefaultValue[propertyName]; + if(scope.valueObjRef[propertyName] && type != 'string'){ + scope.valueObjRef[propertyName] = JSON.parse(scope.valueObjRef[propertyName]); + } + }; + }; + + public static factory = ($templateCache:ng.ITemplateCacheService, + DataTypesService:Sdc.Services.DataTypesService, + PropertyNameValidationPattern:RegExp, + ValidationUtils:Sdc.Utils.ValidationUtils)=> { + return new DataTypeFieldsStructureDirective($templateCache,DataTypesService,PropertyNameValidationPattern,ValidationUtils); + }; + } + + DataTypeFieldsStructureDirective.factory.$inject = ['$templateCache','Sdc.Services.DataTypesService','PropertyNameValidationPattern','ValidationUtils']; +} diff --git a/catalog-ui/app/scripts/directives/property-types/type-list/type-list-directive.html b/catalog-ui/app/scripts/directives/property-types/type-list/type-list-directive.html new file mode 100644 index 0000000000..410a24e62b --- /dev/null +++ b/catalog-ui/app/scripts/directives/property-types/type-list/type-list-directive.html @@ -0,0 +1,57 @@ +<div> + <div data-ng-if="!isSchemaTypeDataType"> + <div class="i-sdc-form-item list-new-item" data-ng-class="{error:(parentFormObj['listNewItem'+fieldsPrefixName].$dirty && parentFormObj['listNewItem'+fieldsPrefixName].$invalid)}"> + <input class="i-sdc-form-input" + data-tests-id="listNewItem{{fieldsPrefixName}}" + ng-if="!((schemaProperty.simpleType||schemaProperty.type) == 'boolean')" + data-ng-disabled="readOnly" + data-ng-model="listNewItem.value" + type="text" + name="listNewItem{{fieldsPrefixName}}" + data-ng-pattern="getValidationPattern((schemaProperty.simpleType||schemaProperty.type))" + data-ng-model-options="{ debounce: 200 }" + placeholder="Type a value and then click ADD" + data-ng-maxlength="maxLength" + maxlength="{{maxLength}}" + sdc-keyboard-events="" key-enter="schemaProperty.type && !parentFormObj['listNewItem'+fieldsPrefixName].$invalid && listNewItem.value && addListItem" + autofocus /> + <select class="i-sdc-form-select" + data-tests-id="listNewItem{{fieldsPrefixName}}" + ng-if="(schemaProperty.simpleType||schemaProperty.type) == 'boolean'" + data-ng-disabled="readOnly" + name="listNewItem{{fieldsPrefixName}}" + data-ng-model="listNewItem.value"> + <option value="true">true</option> + <option value="false">false</option> + </select> + <div class="input-error" data-ng-show="parentFormObj['listNewItem'+fieldsPrefixName].$dirty && parentFormObj['listNewItem'+fieldsPrefixName].$invalid"> + <span ng-show="parentFormObj['listNewItem'+fieldsPrefixName].$error.pattern" translate="PROPERTY_EDIT_PATTERN"></span> + <span ng-show="parentFormObj['listNewItem'+fieldsPrefixName].$error.maxlength" translate="VALIDATION_ERROR_MAX_LENGTH" translate-values="{'max': '{{maxLength}}' }"></span> + </div> + </div> + <div class="add-btn add-list-item" data-tests-id="add-list-item{{fieldsPrefixName}}" + data-ng-class="{'disabled': readOnly || !schemaProperty.type || parentFormObj['listNewItem'+fieldsPrefixName].$invalid || !listNewItem.value}" data-ng-click="addListItem()">Add</div> + <div class="list-value-items"> + <span class="list-value-item" data-ng-repeat="value in valueObjRef track by $index"> + {{value}} + <span class="delete-list-item sprite-new small-x-button" data-ng-click="deleteListItem($index)"></span> + </span> + </div> + </div> + <div data-ng-if="isSchemaTypeDataType"> + <div class="dt-list"> + <div data-ng-repeat="value in valueObjRef track by $index" class="dt-list-item"> + <span class="delete-dt-list-item" data-ng-click="deleteListItem($index)"></span> + <fields-structure value-obj-ref="valueObjRef[$index]" + type-name="schemaProperty.type" + parent-form-obj="parentFormObj" + fields-prefix-name="fieldsPrefixName+''+$index" + read-only="readOnly" + types="types"></fields-structure> + </div> + <div class="add-btn add-list-item" data-tests-id="add-list-item" + data-ng-class="{'disabled': readOnly}" data-ng-click="listNewItem.value='{}';addListItem();">Add</div> + </div> + + </div> +</div> diff --git a/catalog-ui/app/scripts/directives/property-types/type-list/type-list-directive.less b/catalog-ui/app/scripts/directives/property-types/type-list/type-list-directive.less new file mode 100644 index 0000000000..eb4214e135 --- /dev/null +++ b/catalog-ui/app/scripts/directives/property-types/type-list/type-list-directive.less @@ -0,0 +1,85 @@ +.list-new-item{ + float: left; + width: 50%; + min-width: 221px; + margin-right: 15px; + input{ + min-width: 221px; + } +} + +.list-value-items{ + -webkit-border-radius: 2px; + -moz-border-radius: 2px; + border-radius: 2px; + border: 1px solid @main_color_o; + padding-bottom: 10px; + min-height: 100px; + clear: both; + background-color: white; + .list-value-item{ + display: inline-block; + background-color: @tlv_color_v; + margin: 10px 0 0 10px; + padding-left: 8px; + .delete-list-item{ + margin: 0 6px 0 10px; + .hand; + } + } +} + +.add-btn { + .f-color.a; + .f-type._14_m; + .hand; + + &.add-list-item { + float: left; + margin-top: 5px; + width: 44px; + } + + &:before { + .sprite-new; + .plus-icon; + margin-right: 5px; + content: ""; + + } + &:hover { + .f-color.b; + &:before { + .sprite-new; + .plus-icon-hover; + } + } + +} + +.dt-list{ + display: table-caption; + .dt-list-item { + border-radius: 3px; + background-color: @tlv_color_v; + display: inline-block; + .delete-dt-list-item{ + float: right; + position: relative; + top: 5px; + right: 5px; + .sprite-new; + .delete-icon; + &:hover{ + .delete-icon-hover; + } + } + .data-type-name{ + margin-right: 16px; + } + } + &>.add-list-item{ + float:none; + } +} + diff --git a/catalog-ui/app/scripts/directives/property-types/type-list/type-list-directive.ts b/catalog-ui/app/scripts/directives/property-types/type-list/type-list-directive.ts new file mode 100644 index 0000000000..ce5ee1ffa6 --- /dev/null +++ b/catalog-ui/app/scripts/directives/property-types/type-list/type-list-directive.ts @@ -0,0 +1,130 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ +/** + * Created by rcohen on 9/15/2016. + */ +/// <reference path="../../../references"/> +module Sdc.Directives { + 'use strict'; + /// import Model = go.Model; + + export interface ITypeListScope extends ng.IScope { + parentFormObj:ng.IFormController; + schemaProperty:Models.SchemaProperty; + isSchemaTypeDataType:boolean; + valueObjRef:any; + propertyNameValidationPattern: RegExp; + fieldsPrefixName:string; + readOnly:boolean; + listDefaultValue:any; + types:Models.DataTypesMap; + listNewItem:any; + maxLength:number; + + getValidationPattern(type:string):RegExp; + validateIntRange(value:string):boolean; + addListItem():void; + deleteListItem(listItemIndex:number):void + } + + + export class TypeListDirective implements ng.IDirective { + + constructor(private $templateCache:ng.ITemplateCacheService, + private DataTypesService:Sdc.Services.DataTypesService, + private PropertyNameValidationPattern: RegExp, + private ValidationUtils:Sdc.Utils.ValidationUtils) { + } + + scope = { + valueObjRef: '=',//ref to list object in the parent value object + schemaProperty: '=',//get the schema.property object + parentFormObj: '=',//ref to parent form (get angular form object) + fieldsPrefixName: '=',//prefix for form fields names + readOnly: '=',//is form read only + defaultValue: '@',//this list default value + types: '=',//data types list + maxLength: '=' + }; + + restrict = 'E'; + replace = true; + template = ():string => { + return this.$templateCache.get('/app/scripts/directives/property-types/type-list/type-list-directive.html'); + }; + + link = (scope:ITypeListScope, element:any, $attr:any) => { + scope.propertyNameValidationPattern = this.PropertyNameValidationPattern; + + //reset valueObjRef when schema type is changed + scope.$watchCollection('schemaProperty.type', (newData:any):void => { + scope.isSchemaTypeDataType = this.DataTypesService.isDataTypeForSchemaType(scope.schemaProperty,scope.types); + //insert 1 empty item dt by default + if(scope.isSchemaTypeDataType && (!scope.valueObjRef||!scope.valueObjRef.length)){ + scope.valueObjRef = scope.valueObjRef ||[]; + scope.valueObjRef.push({}); + } + }); + + //when user brows between properties in "edit property form" + scope.$watchCollection('fieldsPrefixName', (newData:any):void => { + scope.listNewItem={value:''}; + + if($attr.defaultValue){ + scope.listDefaultValue = JSON.parse($attr.defaultValue); + } + }); + + scope.getValidationPattern = (type:string):RegExp => { + return this.ValidationUtils.getValidationPattern(type); + }; + + scope.validateIntRange = (value:string):boolean => { + return !value || this.ValidationUtils.validateIntRange(value); + }; + + scope.addListItem = ():void => { + scope.valueObjRef = scope.valueObjRef ||[]; + let newVal = ((scope.schemaProperty.simpleType||scope.schemaProperty.type)==Utils.Constants.PROPERTY_TYPES.STRING?scope.listNewItem.value:JSON.parse(scope.listNewItem.value)); + scope.valueObjRef.push(newVal); + scope.listNewItem.value = ""; + }; + + scope.deleteListItem = (listItemIndex:number):void => { + scope.valueObjRef.splice(listItemIndex,1); + if (!scope.valueObjRef.length) { + if (scope.listDefaultValue ) { + angular.copy(scope.listDefaultValue, scope.valueObjRef); + } + } + }; + }; + + public static factory = ($templateCache:ng.ITemplateCacheService, + DataTypesService:Sdc.Services.DataTypesService, + PropertyNameValidationPattern:RegExp, + ValidationUtils:Sdc.Utils.ValidationUtils)=> { + return new TypeListDirective($templateCache,DataTypesService,PropertyNameValidationPattern,ValidationUtils); + }; + } + + TypeListDirective.factory.$inject = ['$templateCache','Sdc.Services.DataTypesService','PropertyNameValidationPattern','ValidationUtils']; +} + diff --git a/catalog-ui/app/scripts/directives/property-types/type-map/type-map-directive.html b/catalog-ui/app/scripts/directives/property-types/type-map/type-map-directive.html new file mode 100644 index 0000000000..ed82b840dc --- /dev/null +++ b/catalog-ui/app/scripts/directives/property-types/type-map/type-map-directive.html @@ -0,0 +1,70 @@ +<div> + <div data-ng-repeat="i in getNumber(mapKeys.length) track by $index" class="map-item" data-ng-class="{'primitive-value-map':!isSchemaTypeDataType}"> + <div class="i-sdc-form-item map-item-field" data-ng-class="{error:(parentFormObj['mapKey'+fieldsPrefixName+$index].$dirty && parentFormObj['mapKey'+fieldsPrefixName+$index].$invalid)}"> + <label class="i-sdc-form-label required">Key</label> + <input class="i-sdc-form-input" + data-tests-id="mapKey{{fieldsPrefixName}}{{$index}}" + data-ng-model="mapKeys[$index]" + type="text" + name="mapKey{{fieldsPrefixName}}{{$index}}" + data-ng-pattern="propertyNameValidationPattern" + data-ng-model-options="{ debounce: 200 }" + data-ng-change="changeKeyOfMap(mapKeys[$index], $index,'mapKey'+fieldsPrefixName+$index);$event.stopPropagation();" + data-ng-disabled="readOnly" + data-required + autofocus/> + <div class="input-error" data-ng-show="parentFormObj['mapKey'+fieldsPrefixName+$index].$dirty && parentFormObj['mapKey'+fieldsPrefixName+$index].$invalid"> + <span ng-show="parentFormObj['mapKey'+fieldsPrefixName+$index].$error.keyExist" translate="PROPERTY_EDIT_MAP_UNIQUE_KEYS"></span> + <span ng-show="parentFormObj['mapKey'+fieldsPrefixName+$index].$error.required" translate="VALIDATION_ERROR_REQUIRED" translate-values="{'field': 'Key' }"></span> + </div> + </div> + <div data-ng-if="!isSchemaTypeDataType" class="i-sdc-form-item map-item-field" data-ng-class="{error:(parentFormObj['mapValue'+fieldsPrefixName+$index].$dirty && parentFormObj['mapValue'+fieldsPrefixName+$index].$invalid)}"> + <label class="i-sdc-form-label required">Value</label> + <input class="i-sdc-form-input" + ng-if="!((schemaProperty.simpleType||schemaProperty.type) == 'boolean')" + data-ng-disabled="readOnly" + data-ng-model="valueObjRef[mapKeys[$index]]" + type="text" + name="mapValue{{fieldsPrefixName}}{{$index}}" + data-tests-id="mapValue{{fieldsPrefixName}}{{$index}}" + data-ng-pattern="getValidationPattern((schemaProperty.simpleType||schemaProperty.type))" + data-ng-change="!parentFormObj['mapValue'+fieldsPrefixName+$index].$error.pattern && parseToCorrectType(valueObjRef, key, (schemaProperty.simpleType||schemaProperty.type))" + data-ng-model-options="{ debounce: 200 }" + data-ng-maxlength="maxLength" + maxlength="{{maxLength}}" + data-required + autofocus /> + <select class="i-sdc-form-select" + data-tests-id="mapValue{{fieldsPrefixName}}{{$index}}" + ng-if="(schemaProperty.simpleType||schemaProperty.type) == 'boolean'" + data-ng-disabled="readOnly" + name="mapValue{{fieldsPrefixName}}{{$index}}" + data-ng-model="valueObjRef[mapKeys[$index]]" + data-required> + <option value="true">true</option> + <option value="false">false</option> + </select> + <div class="input-error" data-ng-show="parentFormObj['mapValue'+fieldsPrefixName+$index].$dirty && parentFormObj['mapValue'+fieldsPrefixName+$index].$invalid"> + <span ng-show="parentFormObj['mapValue'+fieldsPrefixName+$index].$error.required" translate="VALIDATION_ERROR_REQUIRED" translate-values="{'field': 'Value' }"></span> + <span ng-show="parentFormObj['mapValue'+fieldsPrefixName+$index].$error.pattern" translate="PROPERTY_EDIT_PATTERN"></span> + <span ng-show="parentFormObj['mapValue'+fieldsPrefixName+$index].$error.maxlength" translate="VALIDATION_ERROR_MAX_LENGTH" translate-values="{'max': '{{maxLength}}' }"></span> + </div> + </div> + <div data-ng-if="isSchemaTypeDataType" class="i-sdc-form-item map-item-field"> + <label class="i-sdc-form-label">Value</label> + <fields-structure value-obj-ref="valueObjRef[mapKeys[$index]]" + type-name="schemaProperty.type" + parent-form-obj="parentFormObj" + fields-prefix-name="'mapValue'+fieldsPrefixName+''+$index" + read-only="readOnly" + types="types" + ></fields-structure> + </div> + <span ng-click="deleteMapItem($index)" class="delete-map-item" data-tests-id="delete-map-item{{fieldsPrefixName}}{{$index}}" data-ng-class="{'disabled': readOnly}"></span> + </div> + <div class="add-map-item" data-ng-class="{'schema-data-type':isSchemaTypeDataType}"> + <div class="add-btn" data-tests-id="add-map-item" + data-ng-class="{'disabled': readOnly || !schemaProperty.type || mapKeys.indexOf('')>-1}" data-ng-click="addMapItemFields()">Add</div> + </div> +</div> + diff --git a/catalog-ui/app/scripts/directives/property-types/type-map/type-map-directive.less b/catalog-ui/app/scripts/directives/property-types/type-map/type-map-directive.less new file mode 100644 index 0000000000..2480b626f2 --- /dev/null +++ b/catalog-ui/app/scripts/directives/property-types/type-map/type-map-directive.less @@ -0,0 +1,83 @@ +.add-map-item{ + &:nth-child(odd){ + float: right; + } + &:nth-child(1){ + float: none; + .add-btn{ + float: none; + } + } + width: 400px; + .add-btn{ + width: 44px; + float: right; + } + &.schema-data-type{ + float:none; + .add-btn{ + float: none; + } + } +} + +.add-btn { + .f-color.a; + .f-type._14_m; + .hand; + + &:before { + .sprite-new; + .plus-icon; + margin-right: 5px; + content: ""; + + } + &:hover { + .f-color.b; + &:before { + .sprite-new; + .plus-icon-hover; + } + } + +} + +.map-item{ + min-width: 389px; + min-height: 65px; + background-color: @tlv_color_v; + border-radius: 3px; + margin-bottom: 8px; + float: left; + display: flex; + &:nth-child(even).primitive-value-map{ + float: right; + } + .delete-map-item { + float: right; + position: relative; + top: 5px; + right: 5px; + .sprite-new; + .delete-icon; + &:hover{ + .delete-icon-hover; + } + } + .map-item-field { + margin: 7px 12px !important; + float: left; + min-width: 170px; + min-height: 50px; + select{ + width:171px; + } + input[type="text"]{ + width: 170px; + } + &>.data-type-fields-structure{ + padding: 0; + } + } +} diff --git a/catalog-ui/app/scripts/directives/property-types/type-map/type-map-directive.ts b/catalog-ui/app/scripts/directives/property-types/type-map/type-map-directive.ts new file mode 100644 index 0000000000..d94ccf3886 --- /dev/null +++ b/catalog-ui/app/scripts/directives/property-types/type-map/type-map-directive.ts @@ -0,0 +1,157 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ +/** + * Created by rcohen on 9/15/2016. + */ +/// <reference path="../../../references"/> +module Sdc.Directives { + 'use strict'; + + export interface ITypeMapScope extends ng.IScope { + parentFormObj:ng.IFormController; + schemaProperty:Models.SchemaProperty; + isSchemaTypeDataType:boolean; + valueObjRef:any; + mapKeys:Array<string>;//array of map keys + propertyNameValidationPattern: RegExp; + fieldsPrefixName:string; + readOnly:boolean; + mapDefaultValue:any; + types:Models.DataTypesMap; + maxLength:number; + + getValidationPattern(type:string):RegExp; + validateIntRange(value:string):boolean; + changeKeyOfMap(newKey:string, index:number, fieldName:string):void; + deleteMapItem(index:number):void; + addMapItemFields():void; + parseToCorrectType(objectOfValues:any, locationInObj:string, type:string):void; + getNumber(num:number):Array<any>; + } + + + export class TypeMapDirective implements ng.IDirective { + + constructor(private $templateCache:ng.ITemplateCacheService, + private DataTypesService:Sdc.Services.DataTypesService, + private PropertyNameValidationPattern: RegExp, + private ValidationUtils:Sdc.Utils.ValidationUtils, + private $timeout: ng.ITimeoutService) { + } + + scope = { + valueObjRef: '=',//ref to map object in the parent value object + schemaProperty: '=',//get the schema.property object + parentFormObj: '=',//ref to parent form (get angular form object) + fieldsPrefixName: '=',//prefix for form fields names + readOnly: '=',//is form read only + defaultValue: '@',//this map default value + types: '=',//data types list + maxLength: '=' + }; + + restrict = 'E'; + replace = true; + template = ():string => { + return this.$templateCache.get('/app/scripts/directives/property-types/type-map/type-map-directive.html'); + }; + + link = (scope:ITypeMapScope, element:any, $attr:any) => { + scope.propertyNameValidationPattern = this.PropertyNameValidationPattern; + + //reset valueObjRef and mapKeys when schema type is changed + scope.$watchCollection('schemaProperty.type', (newData:any):void => { + scope.isSchemaTypeDataType = this.DataTypesService.isDataTypeForSchemaType(scope.schemaProperty,scope.types); + if(scope.valueObjRef){ + scope.mapKeys = Object.keys(scope.valueObjRef); + } + }); + + //when user brows between properties in "edit property form" + scope.$watchCollection('fieldsPrefixName', (newData:any):void => { + if(!scope.valueObjRef) { + scope.valueObjRef={}; + } + scope.mapKeys = Object.keys(scope.valueObjRef); + + if($attr.defaultValue){ + scope.mapDefaultValue = JSON.parse($attr.defaultValue); + } + }); + + //return dummy array in order to prevent rendering map-keys ng-repeat again when a map key is changed + scope.getNumber = (num:number):Array<any> => { + return new Array(num); + }; + + scope.getValidationPattern = (type:string):RegExp => { + return this.ValidationUtils.getValidationPattern(type); + }; + + scope.validateIntRange = (value:string):boolean => { + return !value || this.ValidationUtils.validateIntRange(value); + }; + + scope.changeKeyOfMap = (newKey:string, index:number, fieldName:string) : void => { + let oldKey = Object.keys(scope.valueObjRef)[index]; + if(Object.keys(scope.valueObjRef).indexOf(newKey)>-1){ + scope.parentFormObj[fieldName].$setValidity('keyExist', false); + }else{ + scope.parentFormObj[fieldName].$setValidity('keyExist', true); + if(!scope.parentFormObj[fieldName].$invalid){ + angular.copy(JSON.parse(JSON.stringify(scope.valueObjRef).replace('"'+oldKey+'":', '"'+newKey+'":')),scope.valueObjRef);//update key + } + } + }; + + scope.deleteMapItem=(index:number):void=>{ + delete scope.valueObjRef[scope.mapKeys[index]]; + scope.mapKeys.splice(index,1); + if (!scope.mapKeys.length) {//only when user removes all pairs of key-value fields - put the default + if ( scope.mapDefaultValue ) { + angular.copy(scope.mapDefaultValue, scope.valueObjRef); + scope.mapKeys = Object.keys(scope.valueObjRef); + } + } + }; + + scope.addMapItemFields = ():void => { + scope.valueObjRef['']= null; + scope.mapKeys = Object.keys(scope.valueObjRef); + }; + + scope.parseToCorrectType = (objectOfValues:any, locationInObj:string, type:string):void => { + if(objectOfValues[locationInObj] && type != Utils.Constants.PROPERTY_TYPES.STRING){ + objectOfValues[locationInObj] = JSON.parse(objectOfValues[locationInObj]); + } + } + }; + + public static factory = ($templateCache:ng.ITemplateCacheService, + DataTypesService:Sdc.Services.DataTypesService, + PropertyNameValidationPattern:RegExp, + ValidationUtils:Sdc.Utils.ValidationUtils, + $timeout: ng.ITimeoutService)=> { + return new TypeMapDirective($templateCache,DataTypesService,PropertyNameValidationPattern,ValidationUtils,$timeout); + }; + } + + TypeMapDirective.factory.$inject = ['$templateCache','Sdc.Services.DataTypesService','PropertyNameValidationPattern','ValidationUtils','$timeout']; +} diff --git a/catalog-ui/app/scripts/directives/punch-out/punch-out.ts b/catalog-ui/app/scripts/directives/punch-out/punch-out.ts new file mode 100644 index 0000000000..f00b7971a9 --- /dev/null +++ b/catalog-ui/app/scripts/directives/punch-out/punch-out.ts @@ -0,0 +1,99 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ +/// <reference path="../../references"/> +module Sdc.Directives { + 'use strict'; + export interface IPunchOutScope extends ng.IScope { + name: string; + data: any; + user: Models.IUserProperties; + onEvent: Function; + } + + export class PunchOutDirective implements ng.IDirective { + + constructor( + private sdcConfig: Sdc.Models.IAppConfigurtaion) {} + + scope = { + name: '=', + data: '=', + user: '=', + onEvent: '&' + }; + + replace = false; + restrict = 'E'; + + link = (scope: IPunchOutScope, element: ng.IAugmentedJQuery):void => { + // global registry object + let PunchOutRegistry = window['PunchOutRegistry']; + + let render = ():void => { + let cookieConfig = this.sdcConfig.cookie; + let props = { + name: scope.name, + options: { + data: scope.data, + apiRoot: this.sdcConfig.api.root, + apiHeaders: { + userId: { + name: cookieConfig.userIdSuffix, + value: scope.user.userId + }, + userFirstName: { + name: cookieConfig.userFirstName, + value: scope.user.firstName + }, + userLastName: { + name: cookieConfig.userLastName, + value: scope.user.lastName + }, + userEmail: { + name: cookieConfig.userEmail, + value: scope.user.email + } + } + }, + onEvent: (...args) => { + scope.$apply(() => { + scope.onEvent().apply(null, args); + }); + } + }; + PunchOutRegistry.render(props, element[0]); + }; + + let unmount = ():void => { + PunchOutRegistry.unmount(element[0]); + }; + + scope.$watch('data', render); + element.on('$destroy', unmount); + }; + + public static factory = (sdcConfig: Sdc.Models.IAppConfigurtaion) => { + return new PunchOutDirective(sdcConfig); + }; + + } + + PunchOutDirective.factory.$inject = ['sdcConfig']; +} diff --git a/catalog-ui/app/scripts/directives/sdc-tabs/sdc-single-tab/sdc-single-tab-directive.ts b/catalog-ui/app/scripts/directives/sdc-tabs/sdc-single-tab/sdc-single-tab-directive.ts new file mode 100644 index 0000000000..26390a7501 --- /dev/null +++ b/catalog-ui/app/scripts/directives/sdc-tabs/sdc-single-tab/sdc-single-tab-directive.ts @@ -0,0 +1,67 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ +/// <reference path="../../../references"/> +module Sdc.Directives { + 'use strict'; + + export class SdcSingleTabDirective implements ng.IDirective { + + constructor(private $compile:ng.ICompileService, private $parse:ng.IParseService) { + } + restrict = 'E'; + + link = (scope, elem:any, attrs:any, ctrl:any) => { + if(!elem.attr('inner-sdc-single-tab')) { + let name = this.$parse(elem.attr('ctrl'))(scope); + elem = elem.removeAttr('ctrl'); + elem.attr('inner-sdc-single-tab', name); + this.$compile(elem)(scope); + } + }; + + public static factory = ($compile:ng.ICompileService, $parse:ng.IParseService)=> { + return new SdcSingleTabDirective($compile, $parse); + }; + } + + export class InnerSdcSingleTabDirective implements ng.IDirective { + + constructor(private $templateCache:ng.ITemplateCacheService) { + } + + scope = { + singleTab: "=", + isViewOnly: "=" + }; + + replace = true; + restrict = 'A'; + controller = '@'; + template = '<div ng-include src="singleTab.templateUrl"></div>'; + + public static factory = ($templateCache:ng.ITemplateCacheService)=> { + return new InnerSdcSingleTabDirective($templateCache); + }; + } + + SdcSingleTabDirective.factory.$inject = ['$compile', '$parse']; + InnerSdcSingleTabDirective.factory.$inject = ['$templateCache']; + +} diff --git a/catalog-ui/app/scripts/directives/sdc-tabs/sdc-single-tab/sdc-single-tab.less b/catalog-ui/app/scripts/directives/sdc-tabs/sdc-single-tab/sdc-single-tab.less new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/catalog-ui/app/scripts/directives/sdc-tabs/sdc-single-tab/sdc-single-tab.less @@ -0,0 +1 @@ + diff --git a/catalog-ui/app/scripts/directives/sdc-tabs/sdc-tabs-directive-view.html b/catalog-ui/app/scripts/directives/sdc-tabs/sdc-tabs-directive-view.html new file mode 100644 index 0000000000..d51d221922 --- /dev/null +++ b/catalog-ui/app/scripts/directives/sdc-tabs/sdc-tabs-directive-view.html @@ -0,0 +1,17 @@ +<div class="sdc-tabs-body"> + <div class="sdc-tabs" ng-class="{'not-active': !isActive}"> + <div class="sdc-tab-arrow" ng-click="isActive = !isActive"> + <span class="sprite-new close-open-left-arrow" ng-class="{'close-open-right-arrow': !isActive}"></span> + </div> + <div ng-repeat="tab in tabs track by $index"> + <div class="sdc-tab" ng-click="onTabSelected(tab)" data-tests-id="{{tab.name}}-tab" ng-mouseenter="hover = true" + ng-mouseleave="hover = false" + ng-class="{'last-tab':$last, 'first-tab': $first, 'selected' :tab.name === selectedTab.name }"> + <div class="sdc-tab-icon sprite-new {{tab.icon}}" ng-class="{'selected' :tab.name === selectedTab.name, 'hover': hover}"></div> + </div> + </div> + </div> + <div class="sdc-single-tab-content" ng-if="isActive"> + <sdc-single-tab class="sdc-single-tab-content-body" ng-if="selectedTab" ctrl="selectedTab.controller" data-dests-id="selected-tab" single-tab="selectedTab" is-view-only="isViewOnly"></sdc-single-tab> + </div> +</div> diff --git a/catalog-ui/app/scripts/directives/sdc-tabs/sdc-tabs-directive.ts b/catalog-ui/app/scripts/directives/sdc-tabs/sdc-tabs-directive.ts new file mode 100644 index 0000000000..91d1744ae5 --- /dev/null +++ b/catalog-ui/app/scripts/directives/sdc-tabs/sdc-tabs-directive.ts @@ -0,0 +1,69 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ +/** + * Created by obarda on 7/28/2016. + */ +/// <reference path="../../references"/> +module Sdc.Directives { + 'use strict'; + + export interface ISdcTabsDirectiveScope extends ng.IScope { + tabs:Array<Models.Tab>; + selectedTab: Models.Tab; + isActive: boolean; + onTabSelected(selectedTab: Models.Tab); + } + + export class SdcTabsDirective implements ng.IDirective { + + constructor(private $templateCache:ng.ITemplateCacheService) { + } + + scope = { + tabs: "=", + selectedTab: "=?", + isViewOnly: "=" + }; + + replace = true; + restrict = 'E'; + template = ():string => { + return this.$templateCache.get('/app/scripts/directives/sdc-tabs/sdc-tabs-directive-view.html'); + }; + + link = (scope:ISdcTabsDirectiveScope) => { + scope.isActive = true; + + if(!scope.selectedTab){ + scope.selectedTab = scope.tabs[0]; + } + + scope.onTabSelected = (selectedTab: Models.Tab) => { + scope.selectedTab = selectedTab; + } + }; + + public static factory = ($templateCache:ng.ITemplateCacheService)=> { + return new SdcTabsDirective($templateCache); + }; + } + + SdcTabsDirective.factory.$inject = ['$templateCache']; +} diff --git a/catalog-ui/app/scripts/directives/sdc-tabs/sdc-tabs.less b/catalog-ui/app/scripts/directives/sdc-tabs/sdc-tabs.less new file mode 100644 index 0000000000..ad390010ed --- /dev/null +++ b/catalog-ui/app/scripts/directives/sdc-tabs/sdc-tabs.less @@ -0,0 +1,68 @@ +.sdc-tabs-body { + height: 100%; + width: 330px; + position: absolute; + .sdc-tabs { + display: inline-block; + width: 40px; + vertical-align: top; + position: relative; + z-index: 99; + right: 332px; + .sdc-tab-arrow { + cursor: pointer; + width: 40px; + height: 20px; + background-color: @tlv_color_u; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.17); + text-align: center; + padding: 1px 4px 0px 0px; + + &:hover { + background-color: @main_color_o; + } + + } + .sdc-tab { + cursor: pointer; + width: 40px; + height: 43px; + background-color: @tlv_color_u; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.17); + text-align: center; + + .sdc-tab-icon { + margin-top: 12px; + } + } + .selected { + background-color: @tlv_color_t; + } + + .last-tab { + border-bottom-left-radius: 12px; + } + } + + .not-active { + // position: absolute; + right: 41px; + } + + .sdc-single-tab-content { + padding: 15px 0px 0px 0px; + width: 290px; + background-color: @tlv_color_t; + height: 100%; + display: inline-block; + bottom: 0; + top: 0; + position: absolute; + box-shadow: 0.3px 1px 3px rgba(24, 24, 25, 0.42); + right: 331px; + .sdc-single-tab-content-body { + height: 100%; + display: flex; + } + } +} diff --git a/catalog-ui/app/scripts/directives/structure-tree/structure-tree-directive.html b/catalog-ui/app/scripts/directives/structure-tree/structure-tree-directive.html new file mode 100644 index 0000000000..7d8a883b33 --- /dev/null +++ b/catalog-ui/app/scripts/directives/structure-tree/structure-tree-directive.html @@ -0,0 +1,54 @@ +<div class="structure-tree"> + <div class="component-container"> + <div class="{{component.iconSprite}} small" ng-class="structureTree.serviceRoot.icon"></div> + <div class="component-container-text" tooltip-class="tooltip-custom break-word-tooltip" tooltips tooltip-content="​{{structureTree.serviceRoot.name}}"> {{structureTree.serviceRoot.name}}</div> + </div> + <ul> + <li data-ng-repeat="firstLevelResourcesInstances in structureTree.serviceRoot.resourceInstancesList"> + <div class="component-container"> + <div class="small {{firstLevelResourcesInstances.icon}}" ng-class="{'sprite-resource-icons': !component.isProduct(), 'sprite-services-icons': component.isProduct()}"> + <div data-ng-class="{'non-certified':!firstLevelResourcesInstances.certified}" + tooltips tooltip-side="top" tooltip-content="Not certified"> + </div> + </div> + <div class="component-container-text" tooltips tooltip-content="{{firstLevelResourcesInstances.name}}" > {{firstLevelResourcesInstances.name}} </div> + </div> + <ul> + <li data-ng-repeat="secondLevelResourcesInstances in firstLevelResourcesInstances.resourceInstancesList"> + <div class="component-container"> + <div class="sprite-resource-icons small" ng-class="secondLevelResourcesInstances.icon"> + <div data-ng-class="{'non-certified':!secondLevelResourcesInstances.certified}" + tooltips tooltip-side="top" tooltip-content="Not certified"> + </div> + </div> + <div class="component-container-text" tooltips tooltip-content="{{secondLevelResourcesInstances.name}}"> {{secondLevelResourcesInstances.name}} </div> + </div> + <ul> + <li data-ng-repeat="thirdLevelResourcesInstances in secondLevelResourcesInstances.resourceInstancesList"> + <div class="component-container"> + <div class="sprite-resource-icons small" ng-class="thirdLevelResourcesInstances.icon"> + <div data-ng-class="{'non-certified':!thirdLevelResourcesInstances.certified}" + tooltips tooltip-side="top" tooltip-content="Not certified"> + </div> + </div> + <div class="component-container-text" tooltips tooltip-content="{{thirdLevelResourcesInstances.name}}" > {{thirdLevelResourcesInstances.name}} </div> + </div> + <ul> + <li data-ng-repeat="forthLevelResourcesInstances in thirdLevelResourcesInstances.resourceInstancesList"> + <div class="component-container"> + <div class="sprite-resource-icons small" ng-class="forthLevelResourcesInstances.icon"> + <div data-ng-class="{'non-certified':!forthLevelResourcesInstances.certified}" + tooltips tooltip-side="top" tooltip-content="Not certified"> + </div> + </div> + <div class="component-container-text" tooltips tooltip-content="{{forthLevelResourcesInstances.name}}"> {{forthLevelResourcesInstances.name}} </div> + </div> + </li> + </ul> + </li> + </ul> + </li> + </ul> + </li> + </ul> +</div> diff --git a/catalog-ui/app/scripts/directives/structure-tree/structure-tree-directive.less b/catalog-ui/app/scripts/directives/structure-tree/structure-tree-directive.less new file mode 100644 index 0000000000..094c3f70ba --- /dev/null +++ b/catalog-ui/app/scripts/directives/structure-tree/structure-tree-directive.less @@ -0,0 +1,68 @@ +.structure-tree{ + padding: 9px 0px 10px 30px; + position: relative; + ul{ + position: relative; + list-style: none; + padding-left:25px; + ::before{ + content: ""; + position: absolute; + left: -27px; + } + ::after{ + content: ""; + position: absolute; + left: -27px; + } + li{ + position: relative; + &::before{ + border-top: 1px solid #666666; + top: 20px; + width: 10px; + height: 0; + } + &::after{ + border-left: 1px solid #666666; + height: 100%; + width: 0px; + top: -2px; + } + &:last-child::after{ + height: 23px + } + } + } + .component-container{ + display: inline-block; + margin: 6px 0px 0px -16px; + } + .component-container-icon{ + display: inline-block; + } + .component-container-text{ + padding-left: 8px; + float: right; + + text-overflow: ellipsis; + max-width:120px; + display: inline-block; + white-space: nowrap; + font-size: 13px; + color: #666666;; + overflow: hidden; + line-height: 28px; + float: none; + } + + .non-certified{ + position: relative; + left: 18px; + bottom: 8px; + .sprite; + .s-sdc-state-non-certified; + display:block; + } + +} diff --git a/catalog-ui/app/scripts/directives/structure-tree/structure-tree-directive.ts b/catalog-ui/app/scripts/directives/structure-tree/structure-tree-directive.ts new file mode 100644 index 0000000000..1edce6f36e --- /dev/null +++ b/catalog-ui/app/scripts/directives/structure-tree/structure-tree-directive.ts @@ -0,0 +1,197 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ +/// <reference path="../../references"/> +module Sdc.Directives { + 'use strict'; + + + export interface IStructureTreeScope extends ng.IScope { + + component: Models.Components.Component; + structureTree: StructureTree; + } + + class StructureTree { + + serviceRoot:ResourceInstanceNode; + + constructor(private uniqueId:string, private resourceInstanceName:string, private resourceInstanceIcon:string, private certified:boolean) { + this.serviceRoot = new ResourceInstanceNode(uniqueId, resourceInstanceName, resourceInstanceIcon, certified); + } + + } + + class ResourceInstanceNode { + id:string; + icon:string; + name:string; + resourceInstancesList:Array<ResourceInstanceNode>; + isAlreadyInTree:boolean; + certified:boolean; + + + constructor(private uniqueId:string, private resourceInstanceName:string, private resourceInstanceIcon:string, certified:boolean) { + this.id = uniqueId; + this.name = resourceInstanceName; + this.icon = resourceInstanceIcon; + this.resourceInstancesList = []; + this.isAlreadyInTree = false; + this.certified = certified; + } + } + + export class StructureTreeDirective implements ng.IDirective { + + + constructor(private $templateCache:ng.ITemplateCacheService) { + } + + scope = { + component: '=', + }; + restrict = 'E'; + template = ():string => { + return this.$templateCache.get('/app/scripts/directives/structure-tree/structure-tree-directive.html'); + }; + + link = (scope:IStructureTreeScope, $elem:any) => { + + let RESOURCE_INSTANCE_LIST:string = "resourceInstancesChildesList"; + let resourceInstanceMap:Utils.Dictionary<string, ResourceInstanceNode>; + let relations:Array<Models.RelationshipModel>; + //************* Start Building Tree Functions *******************// + + //remove unnecessary instances + let initResourceInstanceMap = ():void => { + + resourceInstanceMap = new Utils.Dictionary<string, ResourceInstanceNode>(); + + _.forEach(scope.component.componentInstances, (resourceInstance:Models.ComponentsInstances.ComponentInstance)=> { + if (_.some(Object.keys(resourceInstance.capabilities), (key:string)=> { + return 'tosca.capabilities.container' == key.toLowerCase(); + }) || _.some(Object.keys(resourceInstance.requirements),(key:string)=> { + return 'tosca.capabilities.container' == key.toLowerCase(); + })) { + + let isCertified = 0 === (parseFloat(resourceInstance.componentVersion) % 1); + let node:ResourceInstanceNode = new ResourceInstanceNode(resourceInstance.uniqueId, + resourceInstance.name, + resourceInstance.icon, + isCertified); + resourceInstanceMap.setValue(resourceInstance.uniqueId, node); + } + }); + }; + + //remove unnecessary relations + let initRelations = ():void => { + relations = _.filter(scope.component.componentInstancesRelations, (relation:Models.RelationshipModel)=> { + return resourceInstanceMap.containsKey(relation.fromNode) && resourceInstanceMap.containsKey(relation.toNode); + }); + }; + + let buildTree = ():void => { + if (scope.component) { + scope.structureTree = new StructureTree(scope.component.uniqueId, scope.component.name, scope.component.icon, 'CERTIFIED' === scope.component.lifecycleState); + initResourceInstanceMap(); + initRelations(); + + let parentNodesList = _.groupBy(relations, (node:any)=> { + return node.fromNode; + }); + + for (let parent in parentNodesList) { + _.forEach(parentNodesList[parent], (childNode)=> { + parentNodesList[parent][RESOURCE_INSTANCE_LIST] = []; + parentNodesList[parent][RESOURCE_INSTANCE_LIST].push(mergeAllSubtrees(childNode, parentNodesList)); + }); + } + + //add the resourceInstanceList for the service root node + for (let parent in parentNodesList) { + let resourceInstanceNode:ResourceInstanceNode = resourceInstanceMap.getValue(parent); + resourceInstanceNode.resourceInstancesList = parentNodesList[parent]; + resourceInstanceNode.resourceInstancesList = parentNodesList[parent][RESOURCE_INSTANCE_LIST]; + resourceInstanceNode.isAlreadyInTree = true; + scope.structureTree.serviceRoot.resourceInstancesList.push(resourceInstanceNode); + } + + // Add all node that have no connection to the rootNode + resourceInstanceMap.forEach((key:string, value:ResourceInstanceNode) => { + if (!value.isAlreadyInTree) { + scope.structureTree.serviceRoot.resourceInstancesList.push(value); + } + }); + } + }; + + //this recursion is merging all the subtrees + let mergeAllSubtrees = (connectionData:any, parentNodesList:any):ResourceInstanceNode => { + let resourceInstanceNode:ResourceInstanceNode = resourceInstanceMap.getValue(connectionData.toNode); + resourceInstanceNode.isAlreadyInTree = true; + if (parentNodesList[resourceInstanceNode.id]) { + if (parentNodesList[resourceInstanceNode.id][RESOURCE_INSTANCE_LIST]) { + resourceInstanceNode.resourceInstancesList = parentNodesList[resourceInstanceNode.id][RESOURCE_INSTANCE_LIST]; + } + else { + _.forEach(parentNodesList[resourceInstanceNode.id], (children)=> { + resourceInstanceNode.resourceInstancesList.push(mergeAllSubtrees(children, parentNodesList)); + }); + } + delete parentNodesList[resourceInstanceNode.id]; + } + return resourceInstanceNode; + }; + //************* End Building Tree Functions *******************// + + //************* Start Watchers *******************// + scope.$watch('component.name', ():void => { + if (scope.structureTree) + scope.structureTree.serviceRoot.name = scope.component.name; + }); + + scope.$watch('component.icon', ():void => { + if (scope.structureTree) + scope.structureTree.serviceRoot.icon = scope.component.icon; + }); + + scope.$watchCollection('component.componentInstancesRelations', ():void => { + buildTree(); + }); + + scope.$watchCollection('component.componentInstances', ():void => { + buildTree(); + }); + + //************* End Watchers *******************// + + buildTree(); + + }; + + + public static factory = ($templateCache:ng.ITemplateCacheService) => { + return new StructureTreeDirective($templateCache); + }; + } + + StructureTreeDirective.factory.$inject = ['$templateCache']; + +} diff --git a/catalog-ui/app/scripts/directives/tag/tag-directive.html b/catalog-ui/app/scripts/directives/tag/tag-directive.html new file mode 100644 index 0000000000..28c22a7978 --- /dev/null +++ b/catalog-ui/app/scripts/directives/tag/tag-directive.html @@ -0,0 +1,10 @@ +<div class="sdc-tag"> + <div class="tag" data-tests-id="i-sdc-tag-text" sdc-smart-tooltip data-ng-bind="tagData.tag"></div> + <div class="category" data-ng-hide="hideTooltip===true"> + <span class="relation-categoty-icon" data-tooltips data-tooltip-side="bottom" data-tooltip="'<span class='tag-tooltip-wrap'>{{tagData.tooltip}}</span>'" data-tooltip-enable="false"></span> + </div> + + <div class="delete" data-ng-if="!hideDelete && !sdcDisable" data-ng-click="delete()" data-tests-id="i-sdc-tag-delete"> + <span class="delete-icon"></span> + </div> +</div> diff --git a/catalog-ui/app/scripts/directives/tag/tag-directive.less b/catalog-ui/app/scripts/directives/tag/tag-directive.less new file mode 100644 index 0000000000..f72e366ac6 --- /dev/null +++ b/catalog-ui/app/scripts/directives/tag/tag-directive.less @@ -0,0 +1,51 @@ +.sdc-tag{ + + background-color:#F2F2F2 ; + .border-radius(4px); + min-width:150px; + height:30px; + display: flex; + align-items: center; + padding: 0 10px; + margin: 2px; + + .tag{ + display: inline-block; + } + + .category{ + margin-right: 4px; + margin-left: 25px; + width: 25px; + + } + .relation-categoty-icon{ + .sprite; + .sprite.relation-icon; + .hand; + vertical-align: middle; + + } + + .relation-categoty-icon:hover{ + .sprite; + .sprite.relation-icon-hover; + } + + .delete{ + + } + .delete-icon{ + .sprite; + .sprite.x-btn-black; + .hand; + vertical-align: middle; + } +} + +.tag-tooltip-wrap { + background-color: rgba(80, 99, 113, 0.9); + position: relative; + display: inline-block; + margin: -5px -14px 0px -14px; +} diff --git a/catalog-ui/app/scripts/directives/tag/tag-directive.ts b/catalog-ui/app/scripts/directives/tag/tag-directive.ts new file mode 100644 index 0000000000..64d245e242 --- /dev/null +++ b/catalog-ui/app/scripts/directives/tag/tag-directive.ts @@ -0,0 +1,71 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ +/// <reference path="../../references"/> +module Sdc.Directives { + 'use strict'; + + export class TagData { + tag:string; + tooltip:string; + id: string; + } + + export interface ITagScope extends ng.IScope { + tagData: TagData; + onDelete: Function; + delete:Function; + hideTooltip:boolean; + hideDelete:boolean; + sdcDisable: boolean; + } + + export class TagDirective implements ng.IDirective { + + constructor(private $templateCache:ng.ITemplateCacheService) { + } + + scope = { + tagData: '=', + onDelete: '&', + hideTooltip: '=', + hideDelete: '=', + sdcDisable: '=' + }; + + replace = true; + restrict = 'EA'; + template = ():string => { + return this.$templateCache.get('/app/scripts/directives/tag/tag-directive.html'); + }; + + link = (scope:ITagScope) => { + scope.delete = ()=>{ + scope.onDelete({'uniqueId':scope.tagData.id}); + } + }; + + public static factory = ($templateCache:ng.ITemplateCacheService)=> { + return new TagDirective($templateCache); + }; + + } + + TagDirective.factory.$inject = ['$templateCache']; +} diff --git a/catalog-ui/app/scripts/directives/tutorial/image-template.html b/catalog-ui/app/scripts/directives/tutorial/image-template.html new file mode 100644 index 0000000000..7e7f7af356 --- /dev/null +++ b/catalog-ui/app/scripts/directives/tutorial/image-template.html @@ -0,0 +1,7 @@ +<perfect-scrollbar include-padding="true" class="sdc-tutorial-container-content sdc-tutorial-image-template"> + <div class="{{pageObject.data.imageClass}}"></div> + <div class="sdc-tutorial-image-template-text"> + <h1 translate="{{pageObject.data.title}}"></h1> + <p class="sdc-welcome-page-description2" translate="{{pageObject.data.description}}"></p> + </div> +</perfect-scrollbar> diff --git a/catalog-ui/app/scripts/directives/tutorial/text-template.html b/catalog-ui/app/scripts/directives/tutorial/text-template.html new file mode 100644 index 0000000000..dc1173be64 --- /dev/null +++ b/catalog-ui/app/scripts/directives/tutorial/text-template.html @@ -0,0 +1,4 @@ +<perfect-scrollbar include-padding="true" class="sdc-tutorial-container-content sdc-tutorial-text-template"> + <h1 translate="{{pageObject.data.title}}"></h1> + <p class="sdc-welcome-page-description2" translate="{{pageObject.data.description}}"></p> +</perfect-scrollbar> diff --git a/catalog-ui/app/scripts/directives/tutorial/tutorial-directive.html b/catalog-ui/app/scripts/directives/tutorial/tutorial-directive.html new file mode 100644 index 0000000000..191752fc1f --- /dev/null +++ b/catalog-ui/app/scripts/directives/tutorial/tutorial-directive.html @@ -0,0 +1,22 @@ +<div class="sdc-tutorial-page" data-ng-if="showTutorial"> + <div class="sdc-tutorial-container-wrapper"> + <div class="sdc-tutorial-skip" translate="{{isFirstTime?'TUTRIAL_GENERAL_SKIP_BUTTON':'TUTRIAL_GENERAL_CLOSE_BUTTON'}}" data-ng-click="closeTutorial()"></div> + <div class="sdc-tutorial-container"> + <div class="sdc-tutorial-container-tabs"> + <div class="sdc-tutorial-container-tab" data-ng-repeat="tab in tabs" data-ng-class="{'selected': tab.id===pageObject.tab}"> + <span translate="{{tab.name}}" data-ng-click="initPage(tab.defaultPage)"></span> + </div> + </div> + <ng-include src="templateUrl"></ng-include> + </div> + + <div class="sdc-tutorial-footer"> + <div class="sdc-tutorial-footer-prev-button"><span data-ng-show="hasPrevious()" translate="TUTRIAL_GENERAL_PREVIOUS_BUTTON" data-ng-click="previous()"></span></div> + <div class="sdc-tutorial-footer-page-counter"><span class="selected" data-ng-bind="currentPageIndex+1"></span>/<span class="total" data-ng-bind="totalPages"></span></div> + <div class="sdc-tutorial-footer-next-button"> + <span data-ng-if="hasNext()" translate="TUTRIAL_GENERAL_NEXT_BUTTON" data-ng-click="next()"></span> + <span data-ng-if="(currentPageIndex+1) === totalPages" translate="TUTRIAL_GENERAL_NEXT_BUTTON_END" data-ng-click="closeAndShowLastPage()"></span> + </div> + </div> + </div> +</div> diff --git a/catalog-ui/app/scripts/directives/tutorial/tutorial-directive.less b/catalog-ui/app/scripts/directives/tutorial/tutorial-directive.less new file mode 100644 index 0000000000..410a54e9c1 --- /dev/null +++ b/catalog-ui/app/scripts/directives/tutorial/tutorial-directive.less @@ -0,0 +1,213 @@ +.sdc-tutorial-page { + + width: 100%; + height: 100%; + background-color: rgba(0,0,0,0.8); + + display: flex; + align-items: center; + justify-content: center; + position: absolute; + top: 0; + left: 0; + z-index: 3000; + + .sdc-tutorial-container-wrapper { + display: flex; + flex-direction: column; + } + + .sdc-tutorial-container { + .bg_c; + width: 830px; + height: 466px; + box-shadow: 1px 2px 2px 0px rgba(0, 0, 0, 0.35); + } + + .sdc-tutorial-container-tabs { + height: 56px; + display: flex; + flex-direction: row; + } + + .sdc-tutorial-container-tab { + .a_6; + flex-grow: 1; + align-items: center; + justify-content: center; + display: flex; + height: 56px; + position: relative; + opacity: 0.8; + + span { + .hand; + } + + &::after { + content: ''; + display: block; + border-right: solid 1px ; + border-color: rgba(59, 123, 155, 0.31); + height: 28px; + right: 0; + position: absolute; + top: 14px; //(56-28)/2 + width: 1px; + } + + &:last-child:after { + display: none; + } + + &.selected { + opacity: 1; + .bold; + } + + } + + .sdc-tutorial-container-content { + .bg_a; + .perfect-scrollbar; + display: flex; + align-items: center; + height: 410px; + } + + .sdc-tutorial-skip { + .c_1; + .hand; + text-align: right; + margin-bottom: 9px; + } + + .sdc-tutorial-footer { + .c_4; + margin-top: 9px; + + .sdc-tutorial-footer-prev-button { + float: left; + position: relative; + padding-left: 14px; + .noselect; + + span { + .hand; + &::before { + content: '<'; + display: block; + position: absolute; + left: 0; + top: 0; + } + } + } + + .sdc-tutorial-footer-page-counter { + .e_3; + position: absolute; + left: 50%; + margin-top: 2px; + cursor: default; + .noselect; + + .selected { + .c_3; + .bold; + margin-right: 2px; + } + + .total { + margin-left: 2px; + } + } + + .sdc-tutorial-footer-next-button { + float: right; + position: relative; + padding-right: 14px; + .noselect; + + span { + .hand; + + &::after { + content: '>'; + display: block; + position: absolute; + right: 0; + top: 0; + } + } + } + + } + +} + +///////////////// TEXT TEMPLATE +.sdc-tutorial-text-template { + + padding: 20px 65px; + + h1 { + .c_15; + margin-top: 0; + } + + p { + .c_10; + } +} + +///////////////// IMAGE TEMPLATE +.sdc-tutorial-image-template { + + .sdc-tutorial-image-template-text { + padding: 16px 38px; + height: 118px; + h1 { + .c_11; + margin: 0 0 4px 0; + } + + p { + .c_4; + font-weight: 300; + line-height: 21px; + } + + } + + .sdc-tutorial-page-2-image { background: transparent url('../../../styles/images/tutorial/2.png') no-repeat 0 0; width: 830px; height: 292px;} + .sdc-tutorial-page-3-image { background: transparent url('../../../styles/images/tutorial/3.png') no-repeat 0 0; width: 830px; height: 292px;} + .sdc-tutorial-page-4-image { background: transparent url('../../../styles/images/tutorial/4.png') no-repeat 0 0; width: 830px; height: 292px;} + .sdc-tutorial-page-5-image { background: transparent url('../../../styles/images/tutorial/5.png') no-repeat 0 0; width: 830px; height: 292px;} + .sdc-tutorial-page-6-image { background: transparent url('../../../styles/images/tutorial/6.png') no-repeat 0 0; width: 830px; height: 292px;} + .sdc-tutorial-page-7-image { background: transparent url('../../../styles/images/tutorial/7.png') no-repeat 0 0; width: 830px; height: 292px;} + .sdc-tutorial-page-8-image { background: transparent url('../../../styles/images/tutorial/8.png') no-repeat 0 0; width: 830px; height: 292px;} + + .sdc-tutorial-page-10-image { background: transparent url('../../../styles/images/tutorial/10.png') no-repeat 0 0; width: 830px; height: 292px;} + .sdc-tutorial-page-11-image { background: transparent url('../../../styles/images/tutorial/11.png') no-repeat 0 0; width: 830px; height: 292px;} + + .sdc-tutorial-page-13-image { background: transparent url('../../../styles/images/tutorial/13.png') no-repeat 0 0; width: 830px; height: 292px;} + .sdc-tutorial-page-14-image { background: transparent url('../../../styles/images/tutorial/14.png') no-repeat 0 0; width: 830px; height: 292px;} + .sdc-tutorial-page-15-image { background: transparent url('../../../styles/images/tutorial/15.png') no-repeat 0 0; width: 830px; height: 292px;} + .sdc-tutorial-page-16-image { background: transparent url('../../../styles/images/tutorial/16.png') no-repeat 0 0; width: 830px; height: 292px;} + .sdc-tutorial-page-17-image { background: transparent url('../../../styles/images/tutorial/17.png') no-repeat 0 0; width: 830px; height: 292px;} + .sdc-tutorial-page-18-image { background: transparent url('../../../styles/images/tutorial/18.png') no-repeat 0 0; width: 830px; height: 292px;} + .sdc-tutorial-page-19-image { background: transparent url('../../../styles/images/tutorial/19.png') no-repeat 0 0; width: 830px; height: 292px;} + .sdc-tutorial-page-20-image { background: transparent url('../../../styles/images/tutorial/20.png') no-repeat 0 0; width: 830px; height: 292px;} + .sdc-tutorial-page-21-image { background: transparent url('../../../styles/images/tutorial/21.png') no-repeat 0 0; width: 830px; height: 292px;} + .sdc-tutorial-page-22-image { background: transparent url('../../../styles/images/tutorial/22.png') no-repeat 0 0; width: 830px; height: 292px;} + .sdc-tutorial-page-23-image { background: transparent url('../../../styles/images/tutorial/23.png') no-repeat 0 0; width: 830px; height: 292px;} + .sdc-tutorial-page-24-image { background: transparent url('../../../styles/images/tutorial/24.png') no-repeat 0 0; width: 830px; height: 292px;} + .sdc-tutorial-page-25-image { background: transparent url('../../../styles/images/tutorial/25.png') no-repeat 0 0; width: 830px; height: 292px;} + .sdc-tutorial-page-26-image { background: transparent url('../../../styles/images/tutorial/26.png') no-repeat 0 0; width: 830px; height: 292px;} + .sdc-tutorial-page-27-image { background: transparent url('../../../styles/images/tutorial/27.png') no-repeat 0 0; width: 830px; height: 292px;} + .sdc-tutorial-page-28-image { background: transparent url('../../../styles/images/tutorial/28.png') no-repeat 0 0; width: 830px; height: 292px;} + .sdc-tutorial-page-29-image { background: transparent url('../../../styles/images/tutorial/29.png') no-repeat 0 0; width: 830px; height: 292px;} + .sdc-tutorial-page-30-image { background: transparent url('../../../styles/images/tutorial/30.png') no-repeat 0 0; width: 830px; height: 292px;} + +} diff --git a/catalog-ui/app/scripts/directives/tutorial/tutorial-directive.ts b/catalog-ui/app/scripts/directives/tutorial/tutorial-directive.ts new file mode 100644 index 0000000000..7df35cade9 --- /dev/null +++ b/catalog-ui/app/scripts/directives/tutorial/tutorial-directive.ts @@ -0,0 +1,147 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ +/// <reference path="../../references"/> +module Sdc.Directives { + 'use strict'; + export interface ITutorialScope extends ng.IScope { + showTutorial:boolean; + isFirstTime:boolean; + templateUrl:string; + totalPages: number; + currentPageIndex: number; + page:number; + tabs:Array<string>; + tutorialData:any; + pageObject:any; + + initPage:Function; + next:Function; + previous:Function; + hasNext():boolean; + hasPrevious():boolean; + closeTutorial:Function; + closeAndShowLastPage:Function; + } + + export class TutorialDirective implements ng.IDirective { + + constructor( + private $templateCache:ng.ITemplateCacheService, + private sdcConfig:Models.IAppConfigurtaion, + private $state:ng.ui.IStateService + ) { + } + + scope = { + page: '=', + showTutorial: '=', + isFirstTime: '=' + }; + + replace = false; + restrict = 'EA'; + template = ():string => { + return this.$templateCache.get('/app/scripts/directives/tutorial/tutorial-directive.html'); + }; + + link = (scope:ITutorialScope, $elem:any) => { + + let findPageIndex:Function = (pageId:number):number=> { + for (let i:number=0;i<scope.totalPages;i++){ + if (scope.tutorialData.pages[i].id===pageId){ + return i; + } + } + return -1; + } + + let showCurrentPage:Function = ():void=> { + scope.pageObject = scope.tutorialData.pages[scope.currentPageIndex]; + scope.templateUrl = '/app/scripts/directives/tutorial/' + scope.pageObject.template + '.html'; + } + + scope.tutorialData = this.sdcConfig.tutorial; + + scope.closeTutorial = ()=> { + scope.showTutorial = false; + if(scope.isFirstTime){ + scope.isFirstTime=false; + } + } + + scope.closeAndShowLastPage = ()=> { + if(scope.isFirstTime){ + this.$state.go('dashboard.tutorial-end'); + } + scope.closeTutorial(); + } + + let init:Function = ():void => { + scope.tabs = scope.tutorialData.tabs; + scope.totalPages = scope.tutorialData.pages.length; + scope.initPage(scope.page); + + } + + scope.initPage = (pageId) => { + scope.currentPageIndex = findPageIndex(pageId); + showCurrentPage(); + } + + scope.next = ():void => { + if (scope.hasNext()){ + scope.currentPageIndex++; + showCurrentPage(); + } + } + + scope.previous = ():void => { + if (scope.hasPrevious()){ + scope.currentPageIndex--; + showCurrentPage(); + } + } + + scope.hasNext = ():boolean => { + return (scope.currentPageIndex+1) < scope.totalPages; + } + + scope.hasPrevious = ():boolean => { + return scope.currentPageIndex>0; + } + + angular.element(document).ready(function () { + init(); + }); + + scope.$watch('showTutorial', (showTutorial:any):void => { + scope.initPage(scope.page); + }); + + }; + + public static factory = ($templateCache:ng.ITemplateCacheService, sdcConfig:Models.IAppConfigurtaion, $state:ng.ui.IStateService)=> { + return new TutorialDirective($templateCache, sdcConfig, $state); + }; + + } + + TutorialDirective.factory.$inject = ['$templateCache', 'sdcConfig', '$state']; +} diff --git a/catalog-ui/app/scripts/directives/user-header-details/user-header-details-directive.html b/catalog-ui/app/scripts/directives/user-header-details/user-header-details-directive.html new file mode 100644 index 0000000000..1c99a18ab5 --- /dev/null +++ b/catalog-ui/app/scripts/directives/user-header-details/user-header-details-directive.html @@ -0,0 +1,9 @@ +<div class="w-sdc-header-user-container" data-tests-id="ffff"> + <div class="w-sdc-header-user-icon"></div> + <div class="w-sdc-header-user-details"> + <div sdc-smart-tooltip class="w-sdc-header-user-name" data-ng-bind="user.getName()"></div> + <div class="w-sdc-header-user-role" data-ng-bind="user.getRoleToView()"></div> + <div class="w-sdc-header-user-last-login" data-ng-show="user.getLastLogin()!==''">Last Login: {{user.getLastLogin() | date: 'MMM dd hh:mm a' : 'UTC'}} UTC</div> + </div> + <!--<div class="w-sdc-header-logout-icon"></div>--> +</div> diff --git a/catalog-ui/app/scripts/directives/user-header-details/user-header-details-directive.less b/catalog-ui/app/scripts/directives/user-header-details/user-header-details-directive.less new file mode 100644 index 0000000000..a14db7c6ee --- /dev/null +++ b/catalog-ui/app/scripts/directives/user-header-details/user-header-details-directive.less @@ -0,0 +1,62 @@ +.w-sdc-header-user-container { + .b_7; + width: 400px; + .flex-fixed(400px); + padding: 0 23px; + display: flex; + justify-content: flex-end; +} + +.w-sdc-header-user-icon { + background: no-repeat url('../../../styles/images/anonymous.jpg'); + border-radius: 50%; + height: 47px; + width: 47px; + background-size: cover; + border: solid 2px @color_m; + .flex-fixed(47px); +} + +.w-sdc-header-user-details { + padding: 4px 4px 4px 14px; + .vcenter; +} + +.w-sdc-header-user-name { + max-width: 160px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + vertical-align: bottom; + + .bold; + display: inline-block; +} + +.w-sdc-header-user-role { + .bold; + display: inline-block; + margin-left: 6px; + + &:before { + content: ''; + margin-right: 8px; + border-left: 1px solid @color_m; + } +} + +.w-sdc-header-user-last-login { + .font-type._3; + display: block; +} + +.w-sdc-header-logout-icon { + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAAUCAYAAACAl21KAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDE0IDc5LjE1Njc5NywgMjAxNC8wOC8yMC0wOTo1MzowMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTQgKFdpbmRvd3MpIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjE5RjgxM0QxMDgzQTExRTVBQzA2OTg5ODlFNDVBNTZDIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjE5RjgxM0QyMDgzQTExRTVBQzA2OTg5ODlFNDVBNTZDIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MTlGODEzQ0YwODNBMTFFNUFDMDY5ODk4OUU0NUE1NkMiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6MTlGODEzRDAwODNBMTFFNUFDMDY5ODk4OUU0NUE1NkMiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz6Riq1bAAAA20lEQVR42mJ0CUv+z0Aa+AvEertXzrmGLMgCpZcD8S4iDfoDxHfQBWEGnQLasICBAsDEQCUwcAa5hqeEAHEKNVykB8SzgYZVUGpQPRDPAOJ2oGHNZBsEjF1QussC4mlAXAM0rAM5+tHDwQJIVRBh7m0gLgeql6ZarLHgcP4JIBWAJ+YYgdQUIPYH4k6g+gpyop8RGj6gcGoGGYLTRQRAIxBnAHEl0JAOvF4jAC4BcSrQkDkEw4hA9K8ZGpkW5jUzYGwkkFCwrQJ68Rc2gyKhmNii9hwQoxS1AAEGAO5PP/+ClIDAAAAAAElFTkSuQmCC'); + height: 20px; + width: 18px; + position: absolute; + right: 20px; + top: 29px; +} + + diff --git a/catalog-ui/app/scripts/directives/user-header-details/user-header-details-directive.ts b/catalog-ui/app/scripts/directives/user-header-details/user-header-details-directive.ts new file mode 100644 index 0000000000..46c43a266b --- /dev/null +++ b/catalog-ui/app/scripts/directives/user-header-details/user-header-details-directive.ts @@ -0,0 +1,72 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ +/// <reference path="../../references"/> +module Sdc.Directives { + 'use strict'; + export interface IUserHeaderDetailsScope extends ng.IScope { + name: string; + role: string; + iconUrl: string; + UserResourceClass:Services.IUserResourceClass; + user: Models.IUser; + sdcConfig:Models.IAppConfigurtaion; + initUser:Function; + } + + export class UserHeaderDetailsDirective implements ng.IDirective { + + constructor(private $templateCache:ng.ITemplateCacheService, private $http:ng.IHttpService, private sdcConfig:Models.IAppConfigurtaion, private UserResourceClass:Services.IUserResourceClass) { + } + + scope = { + iconUrl: '=?' + }; + + replace = true; + restrict = 'E'; + template = ():string => { + return this.$templateCache.get('/app/scripts/directives/user-header-details/user-header-details-directive.html'); + }; + + link = (scope:IUserHeaderDetailsScope) => { + + scope.initUser = ():void => { + let defaultUserId:string; + let user:Services.IUserResource = this.UserResourceClass.getLoggedinUser(); + if (!user) { + defaultUserId = this.$http.defaults.headers.common[this.sdcConfig.cookie.userIdSuffix]; + user = this.UserResourceClass.get({id: defaultUserId}, ():void => { + scope.user = new Models.User(user); + }); + } else { + scope.user = new Models.User(user); + } + }; + scope.initUser(); + }; + + public static factory = ($templateCache:ng.ITemplateCacheService, $http:ng.IHttpService, sdcConfig:Models.IAppConfigurtaion, UserResourceClass:Services.IUserResourceClass)=> { + return new UserHeaderDetailsDirective($templateCache, $http, sdcConfig, UserResourceClass); + }; + + } + + UserHeaderDetailsDirective.factory.$inject = ['$templateCache', '$http', 'sdcConfig', 'Sdc.Services.UserResourceService']; +} diff --git a/catalog-ui/app/scripts/directives/utils/expand-collapse-menu-box/expand-collaps-menu-box.ts b/catalog-ui/app/scripts/directives/utils/expand-collapse-menu-box/expand-collaps-menu-box.ts new file mode 100644 index 0000000000..9756ff9e49 --- /dev/null +++ b/catalog-ui/app/scripts/directives/utils/expand-collapse-menu-box/expand-collaps-menu-box.ts @@ -0,0 +1,66 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ +/// <reference path="../../../references"/> +module Sdc.Directives { + 'use strict'; + export interface IExpandCollapseMenuBoxDirectiveScope extends ng.IScope { + menuItemsGroup: Utils.MenuItemGroup; + menuTitle: string; + parentScope: ng.IScope; + onMenuItemClick(menuItem: Utils.MenuItem):void; + } + + export class ExpandCollapseMenuBoxDirective implements ng.IDirective { + + constructor(private $templateCache:ng.ITemplateCacheService) { + } + + scope = { + menuTitle: '@', + menuItemsGroup: '=', + parentScope: '=' + }; + + public replace = false; + public restrict = 'AE'; + public transclude = true; + + template = ():string => { + return this.$templateCache.get('/app/scripts/directives/utils/expand-collapse-menu-box/expand-collapse-menu-box.html'); + }; + + link = (scope:IExpandCollapseMenuBoxDirectiveScope, $elem:any) => { + scope.onMenuItemClick = (menuItem: Utils.MenuItem):void => { + let onSuccess = ():void => { + scope.menuItemsGroup.selectedIndex = scope.menuItemsGroup.menuItems.indexOf(menuItem); + }; + let onFailed = ():void => {}; + scope.parentScope[menuItem.action](menuItem.state).then(onSuccess, onFailed); + } + }; + + public static factory = ($templateCache:ng.ITemplateCacheService)=> { + return new ExpandCollapseMenuBoxDirective($templateCache); + }; + + } + + ExpandCollapseMenuBoxDirective.factory.$inject = ['$templateCache']; +} diff --git a/catalog-ui/app/scripts/directives/utils/expand-collapse-menu-box/expand-collapse-menu-box.html b/catalog-ui/app/scripts/directives/utils/expand-collapse-menu-box/expand-collapse-menu-box.html new file mode 100644 index 0000000000..bbd7e59e7c --- /dev/null +++ b/catalog-ui/app/scripts/directives/utils/expand-collapse-menu-box/expand-collapse-menu-box.html @@ -0,0 +1,15 @@ +<div class="expand-collapse-menu-box"> + <expand-collapse expanded-selector=".w-sdc-designer-sidebar-section-content" class="expand-collapse-menu-box-title"> + <div class="expand-collapse-menu-box-title-icon"></div> + <span class="w-sdc-designer-sidebar-section-title-text" data-ng-bind="menuTitle" tooltips tooltip-content="{{menuTitle}}"></span> + </expand-collapse> + + <div class="w-sdc-designer-sidebar-section-content" > + <div class="i-sdc-designer-sidebar-section-content-item expand-collapse-menu-box-item" + ng-class="{'selected': $index == menuItemsGroup.selectedIndex}" ng-repeat="(key, menuItem) in menuItemsGroup.menuItems track by $index"> + <div class="expand-collapse-menu-box-item-text" ng-click="onMenuItemClick(menuItem)" ng-class="{'disabled': menuItem.isDisabled }" data-tests-id="{{menuItem.text}}step" >{{menuItem.text}}</div> + </div> + </div> + +</div> + diff --git a/catalog-ui/app/scripts/directives/utils/expand-collapse-menu-box/expand-collapse-menu-box.less b/catalog-ui/app/scripts/directives/utils/expand-collapse-menu-box/expand-collapse-menu-box.less new file mode 100644 index 0000000000..d8ceeaea71 --- /dev/null +++ b/catalog-ui/app/scripts/directives/utils/expand-collapse-menu-box/expand-collapse-menu-box.less @@ -0,0 +1,55 @@ +.expand-collapse-menu-box { + line-height: 20px; + padding: 13px 0px 5px 10px; + background-color: @func_color_r; + margin: 3px 3px 5px 0px; + + + .expand-collapse-menu-box-title { + .f-type._18_m; + color: @main_color_m; + font-weight: bold; + .hand; + .w-sdc-designer-sidebar-section-title-text{ + max-width: 185px; + overflow: hidden; + text-overflow: ellipsis; + display: inline-block; + white-space: nowrap; + } + + &.expanded { + .expand-collapse-menu-box-title-icon { + transform: rotate(180deg); + } + } + } + .expand-collapse-menu-box-title-icon { + .hand; + .sprite-new; + .arrow-up; + margin-right: 6px; + transition: .3s all; + position: relative; + + } + .w-sdc-designer-sidebar-section-content { + overflow: hidden; + padding-top: 13px; + .expand-collapse-menu-box-item { + .hand; + padding-left: 14px; + margin: 0px 0px 10px 10px; + font-family: @font-omnes-medium; + color: @main_color_m; + + line-height: 18px; + &.selected { + padding-left: 10px; + font-weight: bold; + border-left: 4px solid @main_color_a; + } + + } + } +} diff --git a/catalog-ui/app/scripts/directives/utils/expand-collapse/expand-collapse.html b/catalog-ui/app/scripts/directives/utils/expand-collapse/expand-collapse.html new file mode 100644 index 0000000000..a2358ea2b7 --- /dev/null +++ b/catalog-ui/app/scripts/directives/utils/expand-collapse/expand-collapse.html @@ -0,0 +1 @@ +<ng-transclude></ng-transclude> diff --git a/catalog-ui/app/scripts/directives/utils/expand-collapse/expand-collapse.less b/catalog-ui/app/scripts/directives/utils/expand-collapse/expand-collapse.less new file mode 100644 index 0000000000..d0d8fa3251 --- /dev/null +++ b/catalog-ui/app/scripts/directives/utils/expand-collapse/expand-collapse.less @@ -0,0 +1,10 @@ +.ellipsis-directive-more-less { + .a_9; + .bold; + .hand; + float: right; + margin-right: 10px; + line-height: 23px; + text-decoration: underline; + text-align: left; +} diff --git a/catalog-ui/app/scripts/directives/utils/expand-collapse/expand-collapse.ts b/catalog-ui/app/scripts/directives/utils/expand-collapse/expand-collapse.ts new file mode 100644 index 0000000000..b294da6c13 --- /dev/null +++ b/catalog-ui/app/scripts/directives/utils/expand-collapse/expand-collapse.ts @@ -0,0 +1,136 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ +/// <reference path="../../../references"/> +module Sdc.Directives { + 'use strict'; + export interface IExpandCollapseScope extends ng.IScope { + toggle(): void; + collapsed: boolean; + expandedSelector: string; + content:string; + isCloseOnInit:boolean; + loadDataFunction: Function; + isLoadingData: boolean; + } + + export class ExpandCollapseDirective implements ng.IDirective { + + constructor(private $templateCache:ng.ITemplateCacheService) { + } + + scope = { + expandedSelector: '@', + loadDataFunction: '&?', + isCloseOnInit: '=?' + }; + + public replace = false; + public restrict = 'AE'; + public transclude = true; + + template = ():string => { + return this.$templateCache.get('/app/scripts/directives/utils/expand-collapse/expand-collapse.html'); + }; + + link = (scope:IExpandCollapseScope, $elem:any) => { + scope.collapsed = false; + scope.isLoadingData = false; + $elem.addClass('expanded'); + + + if(scope.isCloseOnInit) { + window.setTimeout(function () { + toggle(); + },0); + } + + $elem.click(function(){ + toggle(); + }); + + let expand = ():void => { + $elem.addClass('expanded'); + scope.collapsed = false; + + let element = $(scope.expandedSelector)[0]; + let prevWidth = element.style.height; + element.style.height = 'auto'; + let endWidth = getComputedStyle(element).height; + element.style.height = prevWidth; + element.offsetHeight; // force repaint + element.style.transition = 'height .3s ease-in-out'; + element.style.height = endWidth; + element.hidden = false; + element.addEventListener('transitionend', function transitionEnd(event) { + if (event['propertyName'] == 'height') { + element.style.transition = ''; + element.style.height = 'auto'; + element.removeEventListener('transitionend', transitionEnd, false); + } + }, false) + }; + + let collapse = ():void => { + $elem.removeClass('expanded'); + scope.collapsed = true; + + let element = $(scope.expandedSelector)[0]; + element.style.height = getComputedStyle(element).height; + element.style.transition = 'height .5s ease-in-out'; + element.offsetHeight; // force repaint + element.style.height = '0px'; + element.hidden = true; + }; + + let toggle = ():void => { + if (scope.collapsed === true){ + if(scope.loadDataFunction) { + scope.isLoadingData = true; + let onSuccess = () => { + window.setTimeout(function () { + expand(); + scope.isLoadingData = false; + },0); + }; + scope.loadDataFunction().then(onSuccess); + } + else { + if(scope.isLoadingData === false) { + expand(); + } + } + + } else { + if(scope.isLoadingData === false) { + collapse(); + } + } + } + + }; + + public static factory = ($templateCache:ng.ITemplateCacheService)=> { + return new ExpandCollapseDirective($templateCache); + }; + + } + + ExpandCollapseDirective.factory.$inject = ['$templateCache']; +} diff --git a/catalog-ui/app/scripts/directives/utils/page-selector/page-selector.html b/catalog-ui/app/scripts/directives/utils/page-selector/page-selector.html new file mode 100644 index 0000000000..4fbea447e2 --- /dev/null +++ b/catalog-ui/app/scripts/directives/utils/page-selector/page-selector.html @@ -0,0 +1,9 @@ +<div class="i-sdc-left-sidebar-page-nav"> + <ul data-ng-class="{'expanded': expanded===true}"> + <li data-ng-repeat="item in list | filter:exceptSelectedComparator" + data-ng-click="expanded=false" + class="sidebar-page-nav-item" + ui-sref="{{item.url}}">{{item.name}}</li> + </ul> + <div class="sidebar-page-nav-item-selected" data-ng-click="openCollapse()">{{selected}}<span data-ng-class="{'expanded': expanded===true}"></span></div> +</div> diff --git a/catalog-ui/app/scripts/directives/utils/page-selector/page-selector.less b/catalog-ui/app/scripts/directives/utils/page-selector/page-selector.less new file mode 100644 index 0000000000..da70218263 --- /dev/null +++ b/catalog-ui/app/scripts/directives/utils/page-selector/page-selector.less @@ -0,0 +1,51 @@ +.i-sdc-left-sidebar-page-nav { + + height: 64px; + + .sidebar-page-nav-item-selected, + .sidebar-page-nav-item { + .i_11; + background-color: #e0e5e9; + width: 100%; + height: 64px; + border-bottom: solid 1px #cccccc; + line-height: 64px; + text-align: center; + cursor: pointer; + vertical-align: middle; + list-style: none; + padding: 0; + margin: 0; + } + + .sidebar-page-nav-item-selected { + z-index: 1010; + position: absolute; + top: 0px; + } + + .sidebar-page-nav-item-selected span { + .sprite; + .sprite.table-arrow; + position: absolute; + top: 28px; + margin-left: 10px; + + &.expanded { + .sprite; + .sprite.table-arrow.opened; + top: 30px; + } + } + + ul { + position: absolute; + top: 0px; + padding: 0; + width: 100%; + z-index: 99; + visibility: hidden; //Need this and not display none, so I can use the function: getComputedStyle + .box-shadow(0px 4px 2px -2px rgba(0, 0, 0, 0.36)); + } + +} diff --git a/catalog-ui/app/scripts/directives/utils/page-selector/page-selector.ts b/catalog-ui/app/scripts/directives/utils/page-selector/page-selector.ts new file mode 100644 index 0000000000..c185fe1c15 --- /dev/null +++ b/catalog-ui/app/scripts/directives/utils/page-selector/page-selector.ts @@ -0,0 +1,106 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ +/// <reference path="../../../references"/> +module Sdc.Directives { + 'use strict'; + + class ListItem { + name; + url; + } + + export interface IPageSelectorScope extends ng.IScope { + selected:string; + expanded: boolean; + list:Array<ListItem>; + exceptSelectedComparator(actual, expected):boolean; + openCollapse(); + } + + export class PageSelectorDirective implements ng.IDirective { + + constructor(private $templateCache:ng.ITemplateCacheService) { + } + + scope = { + list: '=', + selected: '@', + }; + + public replace = true; + public restrict = 'E'; + public transclude = false; + + private ulElement:HTMLElement; + private itemHeight:number = 64; + + private getUlHeight = ():number => { + let tmp:string = getComputedStyle(this.ulElement).height; + //console.log("tmp: " + tmp); + let ulHeight:number = parseInt(tmp.substr(0,tmp.length-2)); + //console.log("ulHeight: " + ulHeight); + return ulHeight; + }; + + template = ():string => { + return this.$templateCache.get('/app/scripts/directives/utils/page-selector/page-selector.html'); + }; + + link = (scope:IPageSelectorScope, $elem:any) => { + scope.expanded=false; + + window.setTimeout(() => { + this.ulElement = angular.element(".i-sdc-left-sidebar-page-nav ul")[0]; + console.log("this.ulElement: " + this.ulElement); + console.log("this.itemHeight: " + this.itemHeight); + this.ulElement.style.top = (this.itemHeight - this.getUlHeight() - 5) + 'px'; + this.ulElement.style.visibility = 'visible'; + },10); + + this.ulElement = angular.element(".i-sdc-left-sidebar-page-nav ul")[0]; + + scope.exceptSelectedComparator = (actual) => { + if (actual.name===scope.selected) { + return false; + } + return true; + }; + + scope.openCollapse = ():void => { + scope.expanded=!scope.expanded; + if (scope.expanded===true) { + this.ulElement.style.transition = 'top 0.4s ease-out'; + this.ulElement.style.top = this.itemHeight + 'px'; + } else { + this.ulElement.style.transition = 'top 0.4s ease-in'; + this.ulElement.style.top = (this.itemHeight - this.getUlHeight() - 5) + 'px'; + } + }; + + }; + + public static factory = ($templateCache:ng.ITemplateCacheService)=> { + return new PageSelectorDirective($templateCache); + }; + + } + + PageSelectorDirective.factory.$inject = ['$templateCache']; +} diff --git a/catalog-ui/app/scripts/directives/utils/sdc-keyboard-events/sdc-keyboard-events.ts b/catalog-ui/app/scripts/directives/utils/sdc-keyboard-events/sdc-keyboard-events.ts new file mode 100644 index 0000000000..9e61caa812 --- /dev/null +++ b/catalog-ui/app/scripts/directives/utils/sdc-keyboard-events/sdc-keyboard-events.ts @@ -0,0 +1,106 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ +/// <reference path="../../../references"/> +module Sdc.Directives { + 'use strict'; + + export interface ISdcKeyboardEventsScope extends ng.IScope { + keyEnter:Function; + keyShift:Function; + keyCtrl:Function; + keyEscape:Function; + keySpace:Function; + } + + export class SdcKeyboardEventsDirective implements ng.IDirective { + + constructor() { + } + + scope = { + keyEnter: '=', + keyShift: '=', + keyCtrl: '=', + keyEscape: '=', + keySpace: '=' + }; + + public replace = false; + public restrict = 'A'; + public transclude = false; + + link = (scope:ISdcKeyboardEventsScope, element:ng.IAugmentedJQuery, attrs:angular.IAttributes) => { + + element.bind("keydown keypress", function (event) { + //console.log(event.which); + switch (event.which) { + case 13: // enter key + scope.$apply(function (){ + if (scope.keyEnter) { + scope.keyEnter(); + event.preventDefault(); + } + }); + break; + case 16: // shift key + scope.$apply(function (){ + if (scope.keyShift) { + scope.keyShift(); + event.preventDefault(); + } + }); + break; + case 17: // ctrl key + scope.$apply(function (){ + if (scope.keyCtrl) { + scope.keyCtrl(); + event.preventDefault(); + } + }); + break; + case 27: // escape key + scope.$apply(function (){ + if (scope.keyEscape) { + scope.keyEscape(); + event.preventDefault(); + } + }); + break; + case 32: // space key + scope.$apply(function (){ + if (scope.keySpace) { + scope.keySpace(); + event.preventDefault(); + } + }); + break; + } + }); + + }; + + public static factory = ()=> { + return new SdcKeyboardEventsDirective(); + }; + + } + + SdcKeyboardEventsDirective.factory.$inject = []; +} diff --git a/catalog-ui/app/scripts/directives/utils/sdc-tags/sdc-tags.html b/catalog-ui/app/scripts/directives/utils/sdc-tags/sdc-tags.html new file mode 100644 index 0000000000..fb1ada69c3 --- /dev/null +++ b/catalog-ui/app/scripts/directives/utils/sdc-tags/sdc-tags.html @@ -0,0 +1,27 @@ +<div class="tags-box" > + <input type="text" + name="{{elementName}}" + class="new-tag-input" + data-ng-class="{'view-mode':sdcDisabled}" + data-ng-change="validateName()" + data-ng-model="newTag" + data-ng-maxlength="50" + data-ng-pattern="pattern" + data-tests-id="i-sdc-tag-input" + maxlength="50" + sdc-keyboard-events + key-enter="addTag" + + /> + <perfect-scrollbar class="perfect-scrollbar tags-wrapper" data-ng-class="{'view-mode':sdcDisabled}" include-padding="true"> + <div data-tests-id="i-sdc-tags-wrapper" > + <div class="group-tag" data-ng-show="specialTag"> + <sdc-tag data-hide-tooltip="true" data-hide-delete="true" + data-tag-data="{tag: specialTag, id: specialTag }"></sdc-tag> + </div> + <div class="group-tag" ng-repeat="tag in tags track by $index"> + <sdc-tag ng-if="tag != specialTag" data-on-delete="deleteTag(tag)" sdc-disable="sdcDisabled" data-hide-delete="sdcDisabled" data-hide-tooltip="true" data-tag-data="{tag: tag, id: tag }"></sdc-tag> + </div> + </div> + </perfect-scrollbar> +</div> diff --git a/catalog-ui/app/scripts/directives/utils/sdc-tags/sdc-tags.less b/catalog-ui/app/scripts/directives/utils/sdc-tags/sdc-tags.less new file mode 100644 index 0000000000..942196e663 --- /dev/null +++ b/catalog-ui/app/scripts/directives/utils/sdc-tags/sdc-tags.less @@ -0,0 +1,61 @@ +.tags-box { + + height: 297px; + .bg_c; + + .perfect-scrollbar { + height: 265px; + } + + .new-tag-input { + display: block; + + -webkit-border-bottom-left-radius: 0 !important; + -moz-border-radius-bottomleft: 0 !important; + -khtml-border-bottom-left-radius: 0 !important; + border-bottom-left-radius: 0 !important; + + -webkit-border-bottom-right-radius: 0 !important; + -moz-border-radius-bottomright: 0 !important; + -khtml-border-bottom-right-radius: 0 !important; + border-bottom-right-radius: 0 !important; + + border: solid 1px #d8d8d8; + width: 100%; + height: 30px; + line-height: 30px; + padding: 2px 10px; + outline: none; + } + + .tags-wrapper { + padding: 10px; + .border-radius-bottom-left(2px); + .border-radius-bottom-right(2px); + border: solid 1px #d8d8d8; + border-top: none; + + .group-tag { + display: inline-block; + + .sdc-tag { + border: solid 1px @main_color_n; + background-color: @main_color_p; + min-width: auto; + .tag { + margin-right: 10px; + } + } + } + &.view-mode .group-tag { + opacity: 1; + background-color: #f8f8f8 !important; + .sdc-tag { + background: none; + border-color: @main_color_o; + } + } + } + +} + diff --git a/catalog-ui/app/scripts/directives/utils/sdc-tags/sdc-tags.ts b/catalog-ui/app/scripts/directives/utils/sdc-tags/sdc-tags.ts new file mode 100644 index 0000000000..3f4147c920 --- /dev/null +++ b/catalog-ui/app/scripts/directives/utils/sdc-tags/sdc-tags.ts @@ -0,0 +1,97 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ +/// <reference path="../../../references"/> +module Sdc.Directives { + 'use strict'; + + export interface ISdcTagsScope extends ng.IScope { + tags:Array<string>; + specialTag:string; + newTag:string; + formElement:ng.IFormController; + elementName:string; + pattern:any; + sdcDisabled:boolean; + maxTags:number; + deleteTag(tag:string):void; + addTag(tag:string):void; + validateName():void; + } + + export class SdcTagsDirective implements ng.IDirective { + + constructor(private $templateCache:ng.ITemplateCacheService) { + } + + scope = { + tags: '=', + specialTag: '=', + pattern: '=', + sdcDisabled: '=', + formElement: '=', + elementName: '@', + maxTags: '@' + }; + + public replace = false; + public restrict = 'E'; + public transclude = false; + + template = ():string => { + return this.$templateCache.get('/app/scripts/directives/utils/sdc-tags/sdc-tags.html'); + }; + + link = (scope:ISdcTagsScope, element:ng.INgModelController) => { + + scope.deleteTag = (tag:string):void => { + scope.tags.splice(scope.tags.indexOf(tag),1); + }; + + scope.addTag = ():void => { + let valid = scope.formElement[scope.elementName].$valid; + if (valid && + scope.tags.length<scope.maxTags && + scope.newTag && + scope.newTag!=='' && + scope.tags.indexOf(scope.newTag)===-1 && + scope.newTag!==scope.specialTag) { + scope.tags.push(scope.newTag); + scope.newTag=''; + } + }; + + scope.validateName = ():void => { + if (scope.tags.indexOf(scope.newTag)>-1) { + scope.formElement[scope.elementName].$setValidity('nameExist', false); + }else{ + scope.formElement[scope.elementName].$setValidity('nameExist', true); + } + } + + }; + + public static factory = ($templateCache:ng.ITemplateCacheService)=> { + return new SdcTagsDirective($templateCache); + }; + + } + + SdcTagsDirective.factory.$inject = ['$templateCache']; +} diff --git a/catalog-ui/app/scripts/directives/utils/sdc_error_tooltip/sdc_error_tooltip.html b/catalog-ui/app/scripts/directives/utils/sdc_error_tooltip/sdc_error_tooltip.html new file mode 100644 index 0000000000..376381b8af --- /dev/null +++ b/catalog-ui/app/scripts/directives/utils/sdc_error_tooltip/sdc_error_tooltip.html @@ -0,0 +1,6 @@ +<div class="i-sdc-form-item-error-message" style="display: none;"> + <span class="i-sdc-form-item-error-icon-open"></span> + <ng-transclude> + + </ng-transclude> +</div> diff --git a/catalog-ui/app/scripts/directives/utils/sdc_error_tooltip/sdc_error_tooltip.ts b/catalog-ui/app/scripts/directives/utils/sdc_error_tooltip/sdc_error_tooltip.ts new file mode 100644 index 0000000000..dc30ea7f41 --- /dev/null +++ b/catalog-ui/app/scripts/directives/utils/sdc_error_tooltip/sdc_error_tooltip.ts @@ -0,0 +1,109 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ +/// <reference path="../../../references"/> +module Sdc.Directives { + 'use strict'; + + export interface ISdcErrorTooltipScope extends ng.IScope { + alignToSelector: string; + topMargin: string; + } + + export class SdcErrorTooltipDirective implements ng.IDirective { + + constructor(private $templateCache:ng.ITemplateCacheService) { + } + + scope = { + alignToSelector: '@', // Jquery selector to align to + topMargin: '@' // The margin from the top, in case there is label or not the top margin is different. + }; + + public replace = false; + public restrict = 'E'; + public transclude = true; + + template = ():string => { + return this.$templateCache.get('/app/scripts/directives/utils/sdc_error_tooltip/sdc_error_tooltip.html'); + }; + + link = (scope:ISdcErrorTooltipScope, $elem:any) => { + let _self = this; + + $elem.addClass("i-sdc-form-item-error-icon"); + + // Calculate the position of the elements after they loaded to the dom. + window.setTimeout(function(){ + _self.calculatePosition(scope, $elem); + },100); + + $elem.bind('mouseover', function(){ + $(".i-sdc-form-item-error-message",$elem).css("display", "block"); + }); + + $elem.bind('mouseleave', function(){ + $(".i-sdc-form-item-error-message",$elem).css("display", "none"); + }); + + } + + private calculatePosition(scope:ISdcErrorTooltipScope, $elem:any):void { + let leftMargin = 13; + let topMargin = scope.topMargin? parseInt(scope.topMargin) : 10; + + if (scope.alignToSelector) { + // Set the position of the error, in case user add align-to-selector attribute + let jObj = $(scope.alignToSelector); + if (jObj.length > 0) { + let height1 = jObj.outerHeight(); + $elem.css('left', jObj.position().left + jObj.outerWidth() + leftMargin); + //$elem.css('top', jObj.position().top + topMargin + (height1 / 2)); + $elem.css('top', jObj.position().top + (height1 / 2) - 5); // Label margin is: 2 + } + } else { + // Set the position of the error, according to the input element. + let inputElm = $elem.siblings('input'); + let textareaElm = $elem.siblings('textarea'); + let selectElm = $elem.siblings('select'); + if (inputElm.length > 0) { + $elem.css('left', inputElm.outerWidth() + leftMargin); + $elem.css('top', inputElm.position().top + topMargin); + } else if (textareaElm.length > 0) { + $elem.css('left', textareaElm.outerWidth() + leftMargin); + let height2 = textareaElm.outerHeight(); + let elmHeight2 = $elem.outerHeight(); + //let top = textareaElm.position().top; + $elem.css('bottom', (height2 - (elmHeight2 / 2)) / 2); + } else if (selectElm.length > 0) { + $elem.css('left', selectElm.outerWidth() + leftMargin); + $elem.css('top', selectElm.position().top + topMargin); + } + } + } + + public static factory = ($templateCache:ng.ITemplateCacheService)=> { + return new SdcErrorTooltipDirective($templateCache); + }; + + } + + SdcErrorTooltipDirective.factory.$inject = ['$templateCache']; + +} diff --git a/catalog-ui/app/scripts/directives/utils/sdc_messages/sdc-message.ts b/catalog-ui/app/scripts/directives/utils/sdc_messages/sdc-message.ts new file mode 100644 index 0000000000..d41ef1ce04 --- /dev/null +++ b/catalog-ui/app/scripts/directives/utils/sdc_messages/sdc-message.ts @@ -0,0 +1,179 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ +/// <reference path="../../../references"/> +module Sdc.Directives { + 'use strict'; + export interface ISdcMessageScope extends ng.IScope { + sdcTranslate: string; + sdcTranslateValues:string; + sdcAlign:string; + } + + export class SdcMessageDirective implements ng.IDirective { + + constructor(private $animate:any, private $filter:any, private $parse:any) { + } + + scope = { + field: '=', + required: '@', + pattern: '@', + sdcTranslate: '@', + sdcTranslateValues: '@', + sdcAlign: '@' + }; + + public terminal = true; + public restrict = 'A'; + public transclude = 'element'; + public require = '^^sdcMessages'; + + link = (scope:ISdcMessageScope, $element:any, $attrs:any,sdcMessagesCtrl:any, $transclude:any) => { + let self = this; + + let commentNode = $element[0]; + + let records; + let staticExp = $attrs.sdcMessage || $attrs.when; + let dynamicExp = $attrs.sdcMessageExp || $attrs.whenExp; + let assignRecords = function(items) { + records = items + ? (angular.isArray(items) + ? items + : items.split(/[\s,]+/)) + : null; + sdcMessagesCtrl.reRender(); + }; + + if (dynamicExp) { + assignRecords(scope.$eval(dynamicExp)); + scope.$watchCollection(dynamicExp, assignRecords); + } else { + assignRecords(staticExp); + } + + let currentElement, messageCtrl; + sdcMessagesCtrl.register(commentNode, messageCtrl = { + test: function (name) { + return self.contains(records, name); + }, + attach: function () { + if (!currentElement) { + $transclude(scope, function (elm) { + + self.$animate.enter(elm, null, $element); + currentElement = elm; + + elm.addClass("i-sdc-form-item-error-message"); + + //$compile + let text; + if (scope.sdcTranslate) { + text = self.$filter('translate')(scope.sdcTranslate, scope.sdcTranslateValues); + } else { + //TODO: Need to handle this + //let t = elm.html(); + //let t = angular.element("<span>" + elm.html() + "</span>"); + //text = self.$parse(t); + } + + //scope.sdcTranslateValues + elm.html(text); + + elm.prepend("<span class='error'></span>"); + + // Adding OK to close the message + //let okElm = $('<span />').attr('class', 'ok').html('OK'); + //okElm.click(function(e){ + // messageCtrl.detach(); + //}); + //elm.append(okElm); + + // Handle the position + if (scope.sdcAlign){ + let choosenElm = $(scope.sdcAlign); + if (choosenElm.length > 0) { + let height1 = choosenElm.outerHeight(); + let elmHeight1 = elm.outerHeight(); + elm.css('left', choosenElm.outerWidth()); + elm.css('bottom', (height1 - (elmHeight1 / 2)) / 2); + } + } else { + // Set the position of the error, according to the input element. + let inputElm = elm.parent().siblings('input'); + let textareaElm = elm.parent().siblings('textarea'); + let selectElm = elm.parent().siblings('select'); + if (inputElm.length > 0) { + elm.css('left', inputElm.outerWidth()); + elm.css('top', inputElm.position().top); + } else if (textareaElm.length > 0) { + elm.css('left', textareaElm.outerWidth()); + let height = textareaElm.outerHeight(); + let elmHeight = elm.outerHeight(); + //let top = textareaElm.position().top; + elm.css('bottom', (height - (elmHeight / 2)) / 2); + } else if (selectElm.length > 0) { + elm.css('left', selectElm.outerWidth()); + elm.css('top', selectElm.position().top); + } + } + + // Each time we attach this node to a message we get a new id that we can match + // when we are destroying the node later. + let $$attachId = currentElement.$$attachId = sdcMessagesCtrl.getAttachId(); + + // in the event that the parent element is destroyed + // by any other structural directive then it's time + // to deregister the message from the controller + currentElement.on('$destroy', function () { + if (currentElement && currentElement.$$attachId === $$attachId) { + sdcMessagesCtrl.deregister(commentNode); + messageCtrl.detach(); + } + }); + }); + } + }, + detach: function () { + if (currentElement) { + let elm = currentElement; + currentElement = null; + self.$animate.leave(elm); + } + } + }); + } + + contains = (collection, key):any => { + if (collection) { + return angular.isArray(collection) + ? collection.indexOf(key) >= 0 + : collection.hasOwnProperty(key); + } + } + + public static factory = ($animate:any, $filter:any, $parse:any)=> { + return new SdcMessageDirective($animate, $filter, $parse); + }; + + } + + SdcMessageDirective.factory.$inject = ['$animate', '$filter', '$parse']; +} diff --git a/catalog-ui/app/scripts/directives/utils/sdc_messages/sdc-messages.less b/catalog-ui/app/scripts/directives/utils/sdc_messages/sdc-messages.less new file mode 100644 index 0000000000..d8dfdbb73b --- /dev/null +++ b/catalog-ui/app/scripts/directives/utils/sdc_messages/sdc-messages.less @@ -0,0 +1,10 @@ +.ellipsis-directive-more-less { + .a_9; + .bold; + .hand; + float: right; + margin-right: 17px; + line-height: 23px; + text-decoration: underline; + text-align: left; +} diff --git a/catalog-ui/app/scripts/directives/utils/sdc_messages/sdc-messages.ts b/catalog-ui/app/scripts/directives/utils/sdc_messages/sdc-messages.ts new file mode 100644 index 0000000000..f8b435b1fa --- /dev/null +++ b/catalog-ui/app/scripts/directives/utils/sdc_messages/sdc-messages.ts @@ -0,0 +1,245 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ +/// <reference path="../../../references"/> +module Sdc.Directives { + 'use strict'; + export interface ISdcMessagesScope extends ng.IScope { + sdcMessages: any; + editForm:ng.IFormController; + } + + export class SdcMessagesDirective implements ng.IDirective { + + constructor() {} + + scope = { + sdcMessages: '=' + }; + + public restrict = 'AE'; + public require = 'sdcMessages'; + public controller = SdcMessagesController; + + /*template = ():string => { + return this.$templateCache.get('/app/scripts/directives/utils/sdc-messages/sdc-messages.html'); + }; + + public static factory = ($templateCache:ng.ITemplateCacheService)=> { + return new SdcMessagesDirective($templateCache); + };*/ + + public static factory = ()=> { + return new SdcMessagesDirective(); + } + + } + + export class SdcMessagesController { + + messages:any; + getAttachId:Function; + render:any; + reRender:Function; + register:Function; + deregister:Function; + head:any; + + static '$inject' = [ + '$element', + '$scope', + '$attrs', + '$animate' + ]; + + constructor(private $element:JQuery, + private $scope:ISdcMessagesScope, + private $attrs:ng.IAttributes, + private $animate:any + ) { + + this.init(); + + } + + init=():void => { + let self = this; + + let ACTIVE_CLASS:string = 'ng-active'; + let INACTIVE_CLASS:string = 'ng-inactive'; + + let ctrl = this; + let latestKey = 0; + let nextAttachId = 0; + + this.getAttachId = function getAttachId() { return nextAttachId++; }; + + let messages = this.messages = {}; + let renderLater, cachedCollection; + + this.render = function(collection) { + collection = collection || {}; + + renderLater = false; + cachedCollection = collection; + + // this is true if the attribute is empty or if the attribute value is truthy + let multiple = self.isAttrTruthy(self.$scope, self.$attrs['sdcMessagesMultiple']) || self.isAttrTruthy(self.$scope, self.$attrs['multiple']); + + let unmatchedMessages = []; + let matchedKeys = {}; + let messageItem = ctrl.head; + let messageFound = false; + let totalMessages = 0; + + // we use != instead of !== to allow for both undefined and null values + while (messageItem != null) { + totalMessages++; + let messageCtrl = messageItem.message; + + let messageUsed = false; + if (!messageFound) { + _.each(collection, function(value, key) { + if (!messageUsed && self.truthy(value) && messageCtrl.test(key)) { + // this is to prevent the same error name from showing up twice + if (matchedKeys[key]) return; + matchedKeys[key] = true; + + messageUsed = true; + messageCtrl.attach(); + } + }); + } + + if (messageUsed) { + // unless we want to display multiple messages then we should + // set a flag here to avoid displaying the next message in the list + messageFound = !multiple; + } else { + unmatchedMessages.push(messageCtrl); + } + + messageItem = messageItem.next; + } + + _.each(unmatchedMessages, function(messageCtrl) { + messageCtrl.detach(); + }); + + unmatchedMessages.length !== totalMessages + ? ctrl.$animate.setClass(self.$element, ACTIVE_CLASS, INACTIVE_CLASS) + : ctrl.$animate.setClass(self.$element, INACTIVE_CLASS, ACTIVE_CLASS); + }; + + self.$scope.$watchCollection('sdcMessages' || self.$attrs['for'], function(newVal:any, oldVal:any){ + ctrl.render(newVal); + }); + + this.reRender = function() { + if (!renderLater) { + renderLater = true; + self.$scope.$evalAsync(function() { + if (renderLater) { + cachedCollection && ctrl.render(cachedCollection); + } + }); + } + }; + + this.register = function(comment, messageCtrl) { + let nextKey = latestKey.toString(); + messages[nextKey] = { + message: messageCtrl + }; + insertMessageNode(self.$element[0], comment, nextKey); + comment.$$sdcMessageNode = nextKey; + latestKey++; + + ctrl.reRender(); + }; + + this.deregister = function(comment) { + let key = comment.$$sdcMessageNode; + delete comment.$$sdcMessageNode; + removeMessageNode(self.$element[0], comment, key); + delete messages[key]; + ctrl.reRender(); + }; + + function findPreviousMessage(parent, comment) { + let prevNode = comment; + let parentLookup = []; + while (prevNode && prevNode !== parent) { + let prevKey = prevNode.$$sdcMessageNode; + if (prevKey && prevKey.length) { + return messages[prevKey]; + } + + // dive deeper into the DOM and examine its children for any sdcMessage + // comments that may be in an element that appears deeper in the list + if (prevNode.childNodes.length && parentLookup.indexOf(prevNode) == -1) { + parentLookup.push(prevNode); + prevNode = prevNode.childNodes[prevNode.childNodes.length - 1]; + } else { + prevNode = prevNode.previousSibling || prevNode.parentNode; + } + } + } + + function insertMessageNode(parent, comment, key) { + let messageNode = messages[key]; + if (!ctrl.head) { + ctrl.head = messageNode; + } else { + let match = findPreviousMessage(parent, comment); + if (match) { + messageNode.next = match.next; + match.next = messageNode; + } else { + messageNode.next = ctrl.head; + ctrl.head = messageNode; + } + } + } + + function removeMessageNode(parent, comment, key) { + let messageNode = messages[key]; + + let match = findPreviousMessage(parent, comment); + if (match) { + match.next = messageNode.next; + } else { + ctrl.head = messageNode.next; + } + } + } + + isAttrTruthy = (scope, attr):any => { + return (angular.isString(attr) && attr.length === 0) || //empty attribute + this.truthy(scope.$eval(attr)); + } + + truthy = (val):any => { + return angular.isString(val) ? val.length : !!val; + } + + } + + SdcMessagesDirective.factory.$inject = ['$templateCache','$animate']; +} diff --git a/catalog-ui/app/scripts/directives/utils/sdc_messages/sdc_messages.html b/catalog-ui/app/scripts/directives/utils/sdc_messages/sdc_messages.html new file mode 100644 index 0000000000..09b1cad4d2 --- /dev/null +++ b/catalog-ui/app/scripts/directives/utils/sdc_messages/sdc_messages.html @@ -0,0 +1 @@ +<span>aaa</span> diff --git a/catalog-ui/app/scripts/directives/utils/smart-tooltip/smart-tooltip.ts b/catalog-ui/app/scripts/directives/utils/smart-tooltip/smart-tooltip.ts new file mode 100644 index 0000000000..49a57245e7 --- /dev/null +++ b/catalog-ui/app/scripts/directives/utils/smart-tooltip/smart-tooltip.ts @@ -0,0 +1,85 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ +/// <reference path="../../../references"/> +module Sdc.Directives { + 'use strict'; + + export interface ISmartTooltipScope extends ng.IScope { + sdcSmartToolip; + } + + export class SmartTooltipDirective implements ng.IDirective { + + constructor(private $templateCache:ng.ITemplateCacheService, + private $compile:ng.ICompileService) { + } + + public replace = false; + public restrict = 'A'; + public transclude = false; + + public link = (scope:ISmartTooltipScope, $elem:ng.IAugmentedJQuery, $attrs:angular.IAttributes) => { + + if ($elem[0].hasAttribute('style')===false){ + $elem[0].setAttribute("style", "overflow: hidden; white-space: nowrap; text-overflow: ellipsis;"); + } else { + let styles = $elem.attr('style'); + $elem[0].setAttribute("style", styles + ";overflow: hidden; white-space: nowrap; text-overflow: ellipsis;"); + } + + $elem.bind('mouseenter', () => { + if($elem[0].offsetWidth < $elem[0].scrollWidth && !$elem.attr('tooltips')){ + $attrs.$set('tooltips', 'tooltips'); + if ($attrs['sdcSmartTooltip'] && $attrs['sdcSmartTooltip'].length>0){ + $elem.attr('tooltip-content', $attrs['sdcSmartTooltip']); + } else { + $attrs.$set('tooltip-content', $elem.text()); + } + + //One possible problem arises when the ngIf is placed on the root element of the template. + //ngIf removes the node and places a comment in it's place. Then it watches over the expression and adds/removes the actual HTML element as necessary. + //The problem seems to be that if it is placed on the root element of the template, then a single comment is what is left from the + //whole template (even if only temporarily), which gets ignored (I am not sure if this is browser-specific behaviour), resulting in an empty template. + + // Remove ng-if attribute and its value (if we reach here, we pass ng-if (ng-if===true), so we can remove it). + $elem.removeAttr('ng-if'); + $elem.removeAttr('data-ng-if'); + + // Remove me (the directive from the element) + let template = $elem[0].outerHTML; + template = template.replace('sdc-smart-tooltip=""',''); + template = template.replace('sdc-smart-tooltip="' + $elem.text() + '"',''); + //console.log(template); + + let el = this.$compile(template)(scope); + console.log(el); + $elem.replaceWith(el); + } + }); + }; + + public static factory = ($templateCache:ng.ITemplateCacheService, $compile:ng.ICompileService)=> { + return new SmartTooltipDirective($templateCache, $compile); + }; + + } + + SmartTooltipDirective.factory.$inject = ['$templateCache', '$compile']; +} diff --git a/catalog-ui/app/scripts/directives/utils/wizard_steps/sdc-wizard-steps.html b/catalog-ui/app/scripts/directives/utils/wizard_steps/sdc-wizard-steps.html new file mode 100644 index 0000000000..0c9b97a58c --- /dev/null +++ b/catalog-ui/app/scripts/directives/utils/wizard_steps/sdc-wizard-steps.html @@ -0,0 +1,16 @@ +<ul class="sdc-wizard-step"> + <li class="step" data-ng-repeat="step in steps track by $index"> + <div class="step-wrapper"> + <button class="step-index" + data-ng-click="controllerStepClicked(step.name)" + data-ng-class="{'selected': step.selected===true, 'valid': step.valid===true, 'disabled': !step.enabled || step.enabled===false}"> + {{$index+1}} + </button> + <span class="step-name" + data-ng-class="{'selected': step.selected===true, 'valid': step.valid===true, 'disabled': !step.enabled || step.enabled===false}">{{step.name}} + </span> + </div> + <div class="step-seperator"></div> + </li> +</ul> + diff --git a/catalog-ui/app/scripts/directives/utils/wizard_steps/sdc-wizard-steps.less b/catalog-ui/app/scripts/directives/utils/wizard_steps/sdc-wizard-steps.less new file mode 100644 index 0000000000..8b777923a0 --- /dev/null +++ b/catalog-ui/app/scripts/directives/utils/wizard_steps/sdc-wizard-steps.less @@ -0,0 +1,69 @@ +@circle-radius: 18px; +@gap: 70px; +@gap-width: 2px; +@valid-width: 2px; + +ul.sdc-wizard-step { + padding: 0; + margin: 0; + + li.step { + position: relative; + list-style: none; + + .step-wrapper { + line-height: @circle-radius*2; + height: @circle-radius*2; + margin-bottom: @gap; + + button.step-index { + ._w-sdc-wizard-step-btn(@circle-radius); + z-index: 99; + display: inline-block; + + &.valid { + display: inline-block; + } + + } + + span.step-name { + .b_7; + line-height: @circle-radius; + display: inline-block; + word-wrap: break-word; + width: calc(~"100%" - @circle-radius*2 + 4); + vertical-align: middle; + padding-left: 10px; + white-space: normal; + + &.selected { + .a_7; + font-weight: bold; + } + + &.disabled { + border: none; + background-color: transparent; + } + + } + } + + .step-seperator { + border-right: @gap-width solid @color_n; + height: @gap + @circle-radius*2; + position: absolute; + top: @circle-radius*2-@circle-radius; + left: @circle-radius - @gap-width/2; + } + + } + + li.step:last-child { + .step-seperator { + display: none; + } + } + +} diff --git a/catalog-ui/app/scripts/directives/utils/wizard_steps/sdc-wizard-steps.ts b/catalog-ui/app/scripts/directives/utils/wizard_steps/sdc-wizard-steps.ts new file mode 100644 index 0000000000..9cad36ab78 --- /dev/null +++ b/catalog-ui/app/scripts/directives/utils/wizard_steps/sdc-wizard-steps.ts @@ -0,0 +1,139 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. 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. + * ============LICENSE_END========================================================= + */ +/// <reference path="../../../references"/> +module Sdc.Directives { + + 'use strict'; + + export interface IWizardStep { + name: string; + selected?: boolean; + valid?:boolean; + enabled?:boolean; + callback: Function; + } + + export interface ISdcWizardStepScope extends ng.IScope { + steps:Array<IWizardStep>; + control:any; + internalControl:any; + + stepClicked(stepName:string):void; + controllerStepClicked(stepName:string):void; + + setStepValidity(stepName:string, valid:boolean):void; + controllerSetStepValidity(step:IWizardStep, valid:boolean):void; + } + + export interface SdcWizardStepMethods { + unSelectAllSteps():void; + selectStep(step:IWizardStep):void; + } + + export class SdcWizardStepDirective implements ng.IDirective { + + constructor(private $templateCache:ng.ITemplateCacheService) { + } + + scope = { + steps: '=', + control: '=' + }; + + public replace = false; + public restrict = 'E'; + public transclude = true; + public controller = SdcWizardStepDirectiveController; + + template = ():string => { + return this.$templateCache.get('/app/scripts/directives/utils/wizard_steps/sdc-wizard-steps.html'); + }; + + link = (scope:ISdcWizardStepScope, $elem:JQuery, attr:any, controller:SdcWizardStepDirectiveController) => { + scope.internalControl = scope.control || {}; + scope.internalControl.stepClicked = (step:string):void => { + scope.controllerStepClicked(step); + }; + + scope.internalControl.setStepValidity = (step:IWizardStep, valid:boolean):void => { + scope.controllerSetStepValidity(step, valid); + }; + } + + public static factory = ($templateCache:ng.ITemplateCacheService)=> { + return new SdcWizardStepDirective($templateCache); + }; + + } + + SdcWizardStepDirective.factory.$inject = ['$templateCache']; + + export class SdcWizardStepDirectiveController { + static $inject = ['$element', '$scope']; + + methods:SdcWizardStepMethods = <SdcWizardStepMethods>{}; + + constructor(public $element: JQuery, + public $scope: ISdcWizardStepScope) { + + this.initMethods(); + this.initScope(); + } + + private initScope = ():void => { + + this.$scope.controllerStepClicked = (stepName:string):void => { + let selectedStep:IWizardStep = <IWizardStep>_.find(this.$scope.steps, function (item) { + return item.name === stepName; + }); + + if (selectedStep && selectedStep.enabled===true){ + let result:boolean = selectedStep.callback(); + if (result===true){ + this.methods.unSelectAllSteps(); + this.methods.selectStep(selectedStep); + } + } + }; + + this.$scope.controllerSetStepValidity = (step:IWizardStep, valid:boolean):void => { + step.valid=valid; + }; + + }; + + private initMethods = ():void => { + + this.methods.unSelectAllSteps = ():void => { + this.$scope.steps.forEach(function (step) { + step.selected = false; + }); + } + + this.methods.selectStep = (step:IWizardStep):void => { + if (step.enabled===true){ + step.selected=true; + } + } + }; + + } + +} |