aboutsummaryrefslogtreecommitdiffstats
path: root/catalog-ui/app/scripts/directives
diff options
context:
space:
mode:
Diffstat (limited to 'catalog-ui/app/scripts/directives')
-rw-r--r--catalog-ui/app/scripts/directives/clicked-outside/clicked-outside-directive.ts131
-rw-r--r--catalog-ui/app/scripts/directives/custom-validation/custom-validation.ts55
-rw-r--r--catalog-ui/app/scripts/directives/download-artifact/download-artifact.ts141
-rw-r--r--catalog-ui/app/scripts/directives/ecomp-header/ecomp-header.html73
-rw-r--r--catalog-ui/app/scripts/directives/ecomp-header/ecomp-header.less296
-rw-r--r--catalog-ui/app/scripts/directives/ecomp-header/ecomp-header.ts235
-rw-r--r--catalog-ui/app/scripts/directives/edit-name-popover/edit-module-name-popover.html31
-rw-r--r--catalog-ui/app/scripts/directives/edit-name-popover/edit-name-popover-directive.ts98
-rw-r--r--catalog-ui/app/scripts/directives/edit-name-popover/edit-name-popover-view.html1
-rw-r--r--catalog-ui/app/scripts/directives/edit-name-popover/edit-name-popover.less71
-rw-r--r--catalog-ui/app/scripts/directives/elements/checkbox/checkbox.html13
-rw-r--r--catalog-ui/app/scripts/directives/elements/checkbox/checkbox.less35
-rw-r--r--catalog-ui/app/scripts/directives/elements/checkbox/checkbox.ts66
-rw-r--r--catalog-ui/app/scripts/directives/elements/radiobutton/radiobutton.html5
-rw-r--r--catalog-ui/app/scripts/directives/elements/radiobutton/radiobutton.less0
-rw-r--r--catalog-ui/app/scripts/directives/elements/radiobutton/radiobutton.ts71
-rw-r--r--catalog-ui/app/scripts/directives/ellipsis/ellipsis-directive.html7
-rw-r--r--catalog-ui/app/scripts/directives/ellipsis/ellipsis-directive.less10
-rw-r--r--catalog-ui/app/scripts/directives/ellipsis/ellipsis-directive.ts80
-rw-r--r--catalog-ui/app/scripts/directives/events/on-last-repeat/on-last-repeat.ts61
-rw-r--r--catalog-ui/app/scripts/directives/file-opener/file-opener.html3
-rw-r--r--catalog-ui/app/scripts/directives/file-opener/file-opener.ts77
-rw-r--r--catalog-ui/app/scripts/directives/file-type/file-type.ts66
-rw-r--r--catalog-ui/app/scripts/directives/file-upload/file-upload.html22
-rw-r--r--catalog-ui/app/scripts/directives/file-upload/file-upload.less75
-rw-r--r--catalog-ui/app/scripts/directives/file-upload/file-upload.ts134
-rw-r--r--catalog-ui/app/scripts/directives/graphs-v2/common/common-graph-utils.ts361
-rw-r--r--catalog-ui/app/scripts/directives/graphs-v2/common/style/component-instances-nodes-style.ts259
-rw-r--r--catalog-ui/app/scripts/directives/graphs-v2/common/style/module-node-style.ts92
-rw-r--r--catalog-ui/app/scripts/directives/graphs-v2/composition-graph/composition-graph.directive.ts555
-rw-r--r--catalog-ui/app/scripts/directives/graphs-v2/composition-graph/composition-graph.html22
-rw-r--r--catalog-ui/app/scripts/directives/graphs-v2/composition-graph/composition-graph.less14
-rw-r--r--catalog-ui/app/scripts/directives/graphs-v2/composition-graph/utils/composition-graph-general-utils.ts243
-rw-r--r--catalog-ui/app/scripts/directives/graphs-v2/composition-graph/utils/composition-graph-links-utils.ts347
-rw-r--r--catalog-ui/app/scripts/directives/graphs-v2/composition-graph/utils/composition-graph-nodes-utils.ts220
-rw-r--r--catalog-ui/app/scripts/directives/graphs-v2/composition-graph/utils/match-capability-requierment-utils.ts265
-rw-r--r--catalog-ui/app/scripts/directives/graphs-v2/deployment-graph/deployment-graph.directive.ts114
-rw-r--r--catalog-ui/app/scripts/directives/graphs-v2/deployment-graph/deployment-graph.html2
-rw-r--r--catalog-ui/app/scripts/directives/graphs-v2/deployment-graph/deployment-graph.less14
-rw-r--r--catalog-ui/app/scripts/directives/graphs-v2/deployment-graph/deployment-utils/deployment-graph-general-utils.ts24
-rw-r--r--catalog-ui/app/scripts/directives/graphs-v2/image-creator/image-creator.service.ts46
-rw-r--r--catalog-ui/app/scripts/directives/graphs-v2/palette/interfaces/i-dragdrop-event.d.ts7
-rw-r--r--catalog-ui/app/scripts/directives/graphs-v2/palette/palette.directive.ts327
-rw-r--r--catalog-ui/app/scripts/directives/graphs-v2/palette/palette.html59
-rw-r--r--catalog-ui/app/scripts/directives/graphs-v2/palette/palette.less92
-rw-r--r--catalog-ui/app/scripts/directives/graphs-v2/relation-menu/relation-menu.html63
-rw-r--r--catalog-ui/app/scripts/directives/graphs-v2/relation-menu/relation-menu.less118
-rw-r--r--catalog-ui/app/scripts/directives/graphs-v2/relation-menu/relation-menu.ts113
-rw-r--r--catalog-ui/app/scripts/directives/info-tooltip/info-tooltip.html10
-rw-r--r--catalog-ui/app/scripts/directives/info-tooltip/info-tooltip.less39
-rw-r--r--catalog-ui/app/scripts/directives/info-tooltip/info-tooltip.ts60
-rw-r--r--catalog-ui/app/scripts/directives/invalid-characters/invalid-characters.ts72
-rw-r--r--catalog-ui/app/scripts/directives/layout/top-nav/top-nav.html54
-rw-r--r--catalog-ui/app/scripts/directives/layout/top-nav/top-nav.less218
-rw-r--r--catalog-ui/app/scripts/directives/layout/top-nav/top-nav.ts155
-rw-r--r--catalog-ui/app/scripts/directives/layout/top-progress/top-progress.html22
-rw-r--r--catalog-ui/app/scripts/directives/layout/top-progress/top-progress.less58
-rw-r--r--catalog-ui/app/scripts/directives/layout/top-progress/top-progress.ts57
-rw-r--r--catalog-ui/app/scripts/directives/loader/loader-directive.html4
-rw-r--r--catalog-ui/app/scripts/directives/loader/loader-directive.less74
-rw-r--r--catalog-ui/app/scripts/directives/loader/loader-directive.ts155
-rw-r--r--catalog-ui/app/scripts/directives/modal/sdc-modal.html18
-rw-r--r--catalog-ui/app/scripts/directives/modal/sdc-modal.less10
-rw-r--r--catalog-ui/app/scripts/directives/modal/sdc-modal.ts103
-rw-r--r--catalog-ui/app/scripts/directives/page-scroller/page-scroller.html22
-rw-r--r--catalog-ui/app/scripts/directives/page-scroller/page-scroller.less98
-rw-r--r--catalog-ui/app/scripts/directives/page-scroller/page-scroller.ts247
-rw-r--r--catalog-ui/app/scripts/directives/perfect-scrollbar/angular-perfect-scrollbar.ts159
-rw-r--r--catalog-ui/app/scripts/directives/print-graph-screen/print-graph-screen.ts211
-rw-r--r--catalog-ui/app/scripts/directives/property-types/data-type-fields-structure/data-type-fields-structure.html82
-rw-r--r--catalog-ui/app/scripts/directives/property-types/data-type-fields-structure/data-type-fields-structure.less90
-rw-r--r--catalog-ui/app/scripts/directives/property-types/data-type-fields-structure/data-type-fields-structure.ts165
-rw-r--r--catalog-ui/app/scripts/directives/property-types/type-list/type-list-directive.html57
-rw-r--r--catalog-ui/app/scripts/directives/property-types/type-list/type-list-directive.less85
-rw-r--r--catalog-ui/app/scripts/directives/property-types/type-list/type-list-directive.ts130
-rw-r--r--catalog-ui/app/scripts/directives/property-types/type-map/type-map-directive.html70
-rw-r--r--catalog-ui/app/scripts/directives/property-types/type-map/type-map-directive.less83
-rw-r--r--catalog-ui/app/scripts/directives/property-types/type-map/type-map-directive.ts157
-rw-r--r--catalog-ui/app/scripts/directives/punch-out/punch-out.ts99
-rw-r--r--catalog-ui/app/scripts/directives/sdc-tabs/sdc-single-tab/sdc-single-tab-directive.ts67
-rw-r--r--catalog-ui/app/scripts/directives/sdc-tabs/sdc-single-tab/sdc-single-tab.less1
-rw-r--r--catalog-ui/app/scripts/directives/sdc-tabs/sdc-tabs-directive-view.html17
-rw-r--r--catalog-ui/app/scripts/directives/sdc-tabs/sdc-tabs-directive.ts69
-rw-r--r--catalog-ui/app/scripts/directives/sdc-tabs/sdc-tabs.less68
-rw-r--r--catalog-ui/app/scripts/directives/structure-tree/structure-tree-directive.html54
-rw-r--r--catalog-ui/app/scripts/directives/structure-tree/structure-tree-directive.less68
-rw-r--r--catalog-ui/app/scripts/directives/structure-tree/structure-tree-directive.ts197
-rw-r--r--catalog-ui/app/scripts/directives/tag/tag-directive.html10
-rw-r--r--catalog-ui/app/scripts/directives/tag/tag-directive.less51
-rw-r--r--catalog-ui/app/scripts/directives/tag/tag-directive.ts71
-rw-r--r--catalog-ui/app/scripts/directives/tutorial/image-template.html7
-rw-r--r--catalog-ui/app/scripts/directives/tutorial/text-template.html4
-rw-r--r--catalog-ui/app/scripts/directives/tutorial/tutorial-directive.html22
-rw-r--r--catalog-ui/app/scripts/directives/tutorial/tutorial-directive.less213
-rw-r--r--catalog-ui/app/scripts/directives/tutorial/tutorial-directive.ts147
-rw-r--r--catalog-ui/app/scripts/directives/user-header-details/user-header-details-directive.html9
-rw-r--r--catalog-ui/app/scripts/directives/user-header-details/user-header-details-directive.less62
-rw-r--r--catalog-ui/app/scripts/directives/user-header-details/user-header-details-directive.ts72
-rw-r--r--catalog-ui/app/scripts/directives/utils/expand-collapse-menu-box/expand-collaps-menu-box.ts66
-rw-r--r--catalog-ui/app/scripts/directives/utils/expand-collapse-menu-box/expand-collapse-menu-box.html15
-rw-r--r--catalog-ui/app/scripts/directives/utils/expand-collapse-menu-box/expand-collapse-menu-box.less55
-rw-r--r--catalog-ui/app/scripts/directives/utils/expand-collapse/expand-collapse.html1
-rw-r--r--catalog-ui/app/scripts/directives/utils/expand-collapse/expand-collapse.less10
-rw-r--r--catalog-ui/app/scripts/directives/utils/expand-collapse/expand-collapse.ts136
-rw-r--r--catalog-ui/app/scripts/directives/utils/page-selector/page-selector.html9
-rw-r--r--catalog-ui/app/scripts/directives/utils/page-selector/page-selector.less51
-rw-r--r--catalog-ui/app/scripts/directives/utils/page-selector/page-selector.ts106
-rw-r--r--catalog-ui/app/scripts/directives/utils/sdc-keyboard-events/sdc-keyboard-events.ts106
-rw-r--r--catalog-ui/app/scripts/directives/utils/sdc-tags/sdc-tags.html27
-rw-r--r--catalog-ui/app/scripts/directives/utils/sdc-tags/sdc-tags.less61
-rw-r--r--catalog-ui/app/scripts/directives/utils/sdc-tags/sdc-tags.ts97
-rw-r--r--catalog-ui/app/scripts/directives/utils/sdc_error_tooltip/sdc_error_tooltip.html6
-rw-r--r--catalog-ui/app/scripts/directives/utils/sdc_error_tooltip/sdc_error_tooltip.ts109
-rw-r--r--catalog-ui/app/scripts/directives/utils/sdc_messages/sdc-message.ts179
-rw-r--r--catalog-ui/app/scripts/directives/utils/sdc_messages/sdc-messages.less10
-rw-r--r--catalog-ui/app/scripts/directives/utils/sdc_messages/sdc-messages.ts245
-rw-r--r--catalog-ui/app/scripts/directives/utils/sdc_messages/sdc_messages.html1
-rw-r--r--catalog-ui/app/scripts/directives/utils/smart-tooltip/smart-tooltip.ts85
-rw-r--r--catalog-ui/app/scripts/directives/utils/wizard_steps/sdc-wizard-steps.html16
-rw-r--r--catalog-ui/app/scripts/directives/utils/wizard_steps/sdc-wizard-steps.less69
-rw-r--r--catalog-ui/app/scripts/directives/utils/wizard_steps/sdc-wizard-steps.ts139
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 &nbsp; hh:mm a' : 'UTC'}}&nbsp;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>&#8203;{{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}}&nbsp;%</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="&#8203;{{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 &nbsp; hh:mm a' : 'UTC'}}&nbsp;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;
+ }
+ }
+ };
+
+ }
+
+}