summaryrefslogtreecommitdiffstats
path: root/catalog-ui/src/app/view-models/workspace
diff options
context:
space:
mode:
authorMichael Lando <ml636r@att.com>2017-06-09 03:19:04 +0300
committerMichael Lando <ml636r@att.com>2017-06-09 03:19:04 +0300
commited64b5edff15e702493df21aa3230b81593e6133 (patch)
treea4cb01fdaccc34930a8db403a3097c0d1e40914b /catalog-ui/src/app/view-models/workspace
parent280f8015d06af1f41a3ef12e8300801c7a5e0d54 (diff)
[SDC-29] catalog 1707 rebase commit.
Change-Id: I43c3dc5cf44abf5da817649bc738938a3e8388c1 Signed-off-by: Michael Lando <ml636r@att.com>
Diffstat (limited to 'catalog-ui/src/app/view-models/workspace')
-rw-r--r--catalog-ui/src/app/view-models/workspace/conformance-level-modal/conformance-level-modal-view-model.ts29
-rw-r--r--catalog-ui/src/app/view-models/workspace/conformance-level-modal/conformance-level-modal-view.html22
-rw-r--r--catalog-ui/src/app/view-models/workspace/conformance-level-modal/conformance-level-modal.less3
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/activity-log/activity-log.html85
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/activity-log/activity-log.less83
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/activity-log/activity-log.ts103
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/attributes/attributes-view-model.ts80
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/attributes/attributes-view.html52
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/attributes/attributes.less54
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/composition/composition-view-model.ts242
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/composition/composition-view.html95
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/composition/composition.less873
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/artifacts/artifacts-view-model.ts301
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/artifacts/artifacts-view.html67
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/artifacts/artifacts.less172
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/details/details-view-model.ts132
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/details/details-view.html136
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/details/details.less72
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/properties-and-attributes/properties-view-model.ts217
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/properties-and-attributes/properties-view.html117
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/properties-and-attributes/properties.less38
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/relations/relations-view-model.ts156
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/relations/relations-view.html61
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/relations/relations.less14
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/structure/structure-view.html13
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/structure/structure-view.ts14
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/deployment-artifacts/deployment-artifacts-description-popover.html23
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/deployment-artifacts/deployment-artifacts-view-model.ts276
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/deployment-artifacts/deployment-artifacts-view.html126
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/deployment-artifacts/deployment-artifacts.less196
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/deployment/deployment-view-model.ts127
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/deployment/deployment-view.html10
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/deployment/deployment.less33
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/distribution/disribution-status-modal/disribution-status-modal-view-model.ts83
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/distribution/disribution-status-modal/disribution-status-modal-view.html130
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/distribution/disribution-status-modal/disribution-status-modal.less40
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/distribution/distribution-view-model.ts111
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/distribution/distribution-view.html181
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/distribution/distribution.less362
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/general/general-view-model.ts349
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/general/general-view.html354
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/general/general.less64
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/icons/icons-view-model.ts111
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/icons/icons-view.html26
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/icons/icons.less65
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/information-artifacts/information-artifacts-view-model.ts138
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/information-artifacts/information-artifacts-view.html66
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/information-artifacts/information-artifacts.less57
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/inputs/inputs.less225
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/inputs/resource-input/resource-inputs-view-model.ts117
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/inputs/resource-input/resource-inputs-view.html86
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/inputs/resource-input/resource-inputs.less9
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/inputs/service-input/service-inputs-view-model.ts378
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/inputs/service-input/service-inputs-view.html134
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/inputs/service-input/service-inputs.less47
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/management-workflow/management-workflow-view-model.ts127
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/management-workflow/management-workflow-view.html3
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/network-call-flow/network-call-flow-view-model.ts79
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/network-call-flow/network-call-flow-view.html3
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/product-hierarchy/product-hierarchy-view-model.ts109
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/product-hierarchy/product-hierarchy-view.html40
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/product-hierarchy/product-hierarchy.less130
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/properties/properties-view-model.ts92
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/properties/properties-view.html59
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/properties/properties.less115
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/req-and-capabilities/req-and-capabilities-view-model.ts147
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/req-and-capabilities/req-and-capabilities-view.html143
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/req-and-capabilities/req-and-capabilities.less196
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/tosca-artifacts/tosca-artifacts-view-model.ts84
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/tosca-artifacts/tosca-artifacts-view.html49
-rw-r--r--catalog-ui/src/app/view-models/workspace/tabs/tosca-artifacts/tosca-artifacts.less78
-rw-r--r--catalog-ui/src/app/view-models/workspace/workspace-view-model.ts718
-rw-r--r--catalog-ui/src/app/view-models/workspace/workspace-view.html78
-rw-r--r--catalog-ui/src/app/view-models/workspace/workspace.less150
74 files changed, 9555 insertions, 0 deletions
diff --git a/catalog-ui/src/app/view-models/workspace/conformance-level-modal/conformance-level-modal-view-model.ts b/catalog-ui/src/app/view-models/workspace/conformance-level-modal/conformance-level-modal-view-model.ts
new file mode 100644
index 0000000000..61a83c88f7
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/conformance-level-modal/conformance-level-modal-view-model.ts
@@ -0,0 +1,29 @@
+'use strict';
+
+export interface IConformanceLevelModalModelScope {
+ footerButtons:Array<any>;
+ modalInstance:ng.ui.bootstrap.IModalServiceInstance;
+}
+
+export class ConformanceLevelModalViewModel {
+
+ static '$inject' = ['$scope', '$uibModalInstance'];
+
+ constructor(private $scope:IConformanceLevelModalModelScope,
+ private $uibModalInstance:ng.ui.bootstrap.IModalServiceInstance) {
+
+ this.initScope();
+ }
+
+ private initScope = ():void => {
+
+ this.$scope.modalInstance = this.$uibModalInstance;
+
+ this.$scope.footerButtons = [
+ {'name': 'Continue', 'css': 'grey', 'callback': this.$uibModalInstance.close},
+ {'name': 'Reject', 'css': 'blue', 'callback': this.$uibModalInstance.dismiss}
+ ];
+
+ };
+
+}
diff --git a/catalog-ui/src/app/view-models/workspace/conformance-level-modal/conformance-level-modal-view.html b/catalog-ui/src/app/view-models/workspace/conformance-level-modal/conformance-level-modal-view.html
new file mode 100644
index 0000000000..3577e4d77b
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/conformance-level-modal/conformance-level-modal-view.html
@@ -0,0 +1,22 @@
+<sdc-modal modal="modalInstance"
+ type="classic"
+ class="w-sdc-modal modal-type-alert conformance-level-modal"
+ header="Warning"
+ buttons="footerButtons"
+ show-close-button="false">
+
+ <perfect-scrollbar include-padding="true">
+ <div class="w-sdc-modal-body-content" data-tests-id="message">
+ <p>
+ You are about to distribute a service with models and artifacts created with an <b>older version of the platform</b>.
+ For such service, new properties, metadata and requirements needed by ECOMP components will not be available.
+ </p><p>
+ It is highly recommended that you upgrade the service models and artifacts.
+ </p><p>
+ Click "Continue" if you need to distribute the current service version.<br />
+ Click "Reject" if you need to stop the distribution and manually upgrade the service.
+ </p>
+ </div>
+ </perfect-scrollbar>
+
+</sdc-modal>
diff --git a/catalog-ui/src/app/view-models/workspace/conformance-level-modal/conformance-level-modal.less b/catalog-ui/src/app/view-models/workspace/conformance-level-modal/conformance-level-modal.less
new file mode 100644
index 0000000000..7f195ade83
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/conformance-level-modal/conformance-level-modal.less
@@ -0,0 +1,3 @@
+.conformance-level-modal{
+
+}
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/activity-log/activity-log.html b/catalog-ui/src/app/view-models/workspace/tabs/activity-log/activity-log.html
new file mode 100644
index 0000000000..23c08f6ec6
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/activity-log/activity-log.html
@@ -0,0 +1,85 @@
+<div class="activity-log">
+
+ <div class="title-wrapper">
+ <div class="top-search">
+ <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>
+
+ <div class="table-container-flex">
+ <div class="table" data-ng-class="{'view-mode': isViewMode()}">
+
+ <!-- Table headers -->
+ <div class="head flex-container">
+ <div class="table-header head-row hand flex-item" ng-repeat="header in tableHeadersList track by $index" data-ng-click="sort(header.property)">{{header.title}}
+ <span data-ng-show="sortBy === header.property" class="table-header-sort-arrow" data-ng-class="{'down': reverse, 'up':!reverse}"> </span>
+ </div>
+ </div>
+
+ <!-- Table body -->
+ <div class="body">
+ <perfect-scrollbar suppress-scroll-x="true" scroll-y-margin-offset="0" include-padding="true" class="scrollbar-container">
+
+ <!-- In case the logs are empty -->
+ <div data-ng-if="!activityLog || activityLog.length===0" class="no-row-text">
+ There are no logs to display
+ </div>
+
+ <!-- Loop on logs list -->
+ <div data-ng-repeat="item in activityLog | filter: searchBind | orderBy:sortBy:reverse track by $index"
+ data-ng-init="item.dateFormat = ( item.TIMESTAMP.replace(' UTC', '') | stringToDateFilter | date: 'MM/dd/yyyy':'UTC')+' | '+(item.TIMESTAMP.replace(' UTC', '') | stringToDateFilter | date: 'shortTime':'UTC' )"
+ class="flex-container data-row"
+ data-ng-class="{'selected': component === selectedComponent}"
+ data-ng-click="doSelectComponent(component);"
+ >
+
+ <!-- Date -->
+ <div class="table-col-general flex-item" sdc-smart-tooltip>
+ {{item.dateFormat}}
+ </div>
+
+ <!-- Action -->
+ <div class="table-col-general flex-item" sdc-smart-tooltip>
+ {{item.ACTION}}
+ </div>
+
+ <!-- Comment -->
+ <div class="table-col-general flex-item" sdc-smart-tooltip>
+ {{item.COMMENT}}
+ </div>
+
+ <!-- Username -->
+ <div class="table-col-general flex-item" sdc-smart-tooltip>
+ {{item.MODIFIER}}
+ </div>
+
+ <!-- Status -->
+ <div class="table-col-general flex-item" sdc-smart-tooltip>
+ {{item.STATUS}}
+ <span data-ng-class="{'success': item.STATUS>='200' && item.STATUS<='204','error': item.STATUS<'200' || item.STATUS>='300'}"></span>
+ </div>
+
+ </div>
+
+ </perfect-scrollbar>
+ </div><!-- End table body -->
+ </div><!-- End table -->
+ </div><!-- End table-container-flex -->
+
+</div>
+
+
+
+<!--<div ng-repeat="activityDate in activityDateArray " class="w-sdc-component-viewer-right-activity-log" >
+ <div class="w-sdc-component-viewer-right-activity-log-date" >{{activityDate | date: 'longDate'}}</div>
+ <div ng-repeat="activity in activityLog[activityDate] | orderBy: '-TIMESTAMP'">
+ <div class="w-sdc-component-viewer-right-activity-log-time">{{activity.TIMESTAMP.replace(" UTC", '') | stringToDateFilter | date: 'mediumTime':'UTC'}}</div>
+ <div class="w-sdc-component-viewer-right-activity-log-content">{{"Action: " + parseAction(activity.ACTION) + " Performed by: " + activity.MODIFIER + " Status: " + activity.STATUS}}</div>
+ </div>
+</div>-->
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/activity-log/activity-log.less b/catalog-ui/src/app/view-models/workspace/tabs/activity-log/activity-log.less
new file mode 100644
index 0000000000..61bb3e9f01
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/activity-log/activity-log.less
@@ -0,0 +1,83 @@
+.activity-log {
+
+ margin-top: 30px;
+
+ .title-wrapper {
+ display: flex;
+ justify-content: flex-end;
+ }
+
+ .table-container-flex .table .body .scrollbar-container {
+ max-height: 448px;
+ }
+
+ .view-mode {
+ background-color: @main_color_p;
+ }
+
+ .table{
+ height: 490px;
+ margin-bottom: 0;
+ }
+
+ .table-container-flex {
+ margin-top: 10px;
+
+ .flex-item:nth-child(1) { width: 200px; }
+ .flex-item:nth-child(2) { flex-grow: 20; }
+ .flex-item:nth-child(3) { flex-grow: 30; }
+ .flex-item:nth-child(4) { flex-grow: 20; }
+ .flex-item:nth-child(5) { width: 80px; }
+
+ .success {
+ position: absolute;
+ top: 11px;
+ right: 20px;
+ .sprite-new;
+ .sdc-success;
+ }
+
+ .error {
+ position: absolute;
+ top: 11px;
+ right: 20px;
+ .sprite-new;
+ .sdc-error;
+ }
+
+ }
+
+ .data-row {
+ position: relative;
+ }
+
+ .top-search {
+ float: right;
+ position: relative;
+
+ input.search-text {
+ .border-radius(2px);
+ width: 245px;
+ height: 32px;
+ line-height: 32px;
+ border: 1px solid @main_color_o;
+ margin: 0;
+ 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 */
+ }
+
+ .magnification {
+ position: absolute;
+ top: 10px;
+ right: 10px;
+ }
+
+ }
+
+}
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/activity-log/activity-log.ts b/catalog-ui/src/app/view-models/workspace/tabs/activity-log/activity-log.ts
new file mode 100644
index 0000000000..ed583dc4c0
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/activity-log/activity-log.ts
@@ -0,0 +1,103 @@
+'use strict';
+import {IWorkspaceViewModelScope} from "app/view-models/workspace/workspace-view-model";
+import {Activity} from "app/models";
+import {ActivityLogService} from "app/services";
+
+export interface IActivityLogViewModelScope extends IWorkspaceViewModelScope {
+ activityDateArray:Array<any>; //this is in order to sort the dates
+ activityLog:Array<Activity>;
+ preVersion:string;
+
+ tableHeadersList:Array<any>;
+ reverse:boolean;
+ sortBy:string;
+ searchBind:string;
+
+ getActivityLog(uniqueId:string):void;
+ onVersionChanged(version:any):void;
+ parseAction(action:string):string;
+ sort(sortBy:string):void;
+}
+
+export class ActivityLogViewModel {
+
+ static '$inject' = [
+ '$scope',
+ '$state',
+ 'Sdc.Services.ActivityLogService'
+ ];
+
+ constructor(private $scope:IActivityLogViewModelScope,
+ private $state:ng.ui.IStateService,
+ private activityLogService:ActivityLogService) {
+
+ this.initScope();
+ this.$scope.setValidState(true);
+ this.initSortedTableScope();
+ this.$scope.updateSelectedMenuItem();
+
+ // Set default sorting
+ this.$scope.sortBy = 'logDate';
+ }
+
+ private initScope():void {
+
+ this.$scope.preVersion = this.$scope.component.version;
+
+ this.$scope.onVersionChanged = (version:any):void => {
+ if (version.versionNumber != this.$scope.component.version) {
+ this.$scope.isLoading = true;
+ this.$scope.getActivityLog(version.versionId);
+ }
+ };
+
+ this.$scope.getActivityLog = (uniqueId:any):void => {
+
+ let onError = (response) => {
+ this.$scope.isLoading = false;
+ console.info('onFaild', response);
+
+ };
+
+ let onSuccess = (response:Array<Activity>) => {
+ this.$scope.activityLog = _.sortBy(response, function (o) {
+ return o.TIMESTAMP;
+ }); //response; //
+ this.$scope.isLoading = false;
+ };
+
+ this.$scope.isLoading = true;
+ if (this.$scope.component.isResource()) {
+ this.activityLogService.getActivityLogService('resources', uniqueId).then(onSuccess, onError);
+ }
+ if (this.$scope.component.isService()) {
+ this.activityLogService.getActivityLogService('services', uniqueId).then(onSuccess, onError);
+ }
+
+ };
+
+ if (!this.$scope.activityLog || this.$scope.preVersion != this.$scope.component.version) {
+ this.$scope.getActivityLog(this.$scope.component.uniqueId);
+ }
+
+ this.$scope.parseAction = (action:string) => {
+ return action ? action.split(/(?=[A-Z])/).join(' ') : '';
+ };
+
+ }
+
+ private initSortedTableScope = ():void => {
+ this.$scope.tableHeadersList = [
+ {title: 'Date', property: 'logDate'},
+ {title: 'Action', property: 'logAction'},
+ {title: 'Comment', property: 'logComment'},
+ {title: 'Username', property: 'logUsername'},
+ {title: 'Status', property: 'logStatus'}
+ ];
+
+ this.$scope.sort = (sortBy:string):void => {
+ this.$scope.reverse = (this.$scope.sortBy === sortBy) ? !this.$scope.reverse : false;
+ this.$scope.sortBy = sortBy;
+ };
+ };
+}
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/attributes/attributes-view-model.ts b/catalog-ui/src/app/view-models/workspace/tabs/attributes/attributes-view-model.ts
new file mode 100644
index 0000000000..d8a60444be
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/attributes/attributes-view-model.ts
@@ -0,0 +1,80 @@
+'use strict';
+import {IWorkspaceViewModelScope} from "app/view-models/workspace/workspace-view-model";
+import {Component, AttributeModel} from "app/models";
+import {ModalsHandler} from "app/utils";
+import {ComponentServiceNg2} from "../../../../ng2/services/component-services/component.service";
+import {ComponentGenericResponse} from "../../../../ng2/services/responses/component-generic-response";
+
+interface IAttributesViewModelScope extends IWorkspaceViewModelScope {
+ tableHeadersList:Array<any>;
+ reverse:boolean;
+ sortBy:string;
+
+ addOrUpdateAttribute(attribute?:AttributeModel):void;
+ delete(attribute:AttributeModel):void;
+ sort(sortBy:string):void;
+}
+
+export class AttributesViewModel {
+
+ static '$inject' = [
+ '$scope',
+ '$filter',
+ '$uibModal',
+ 'ModalsHandler',
+ 'ComponentServiceNg2'
+ ];
+
+
+ constructor(private $scope:IAttributesViewModelScope,
+ private $filter:ng.IFilterService,
+ private $uibModal:ng.ui.bootstrap.IModalService,
+ private ModalsHandler:ModalsHandler,
+ private ComponentServiceNg2: ComponentServiceNg2) {
+
+ this.initComponentAttributes();
+ this.$scope.updateSelectedMenuItem();
+ }
+
+ private initComponentAttributes = () => {
+ if(this.$scope.component.attributes) {
+ this.initScope();
+ } else {
+ this.ComponentServiceNg2.getComponentAttributes(this.$scope.component).subscribe((response:ComponentGenericResponse) => {
+ this.$scope.component.attributes = response.attributes;
+ this.initScope();
+ });
+ }
+ }
+
+
+ private initScope = ():void => {
+
+ this.$scope.sortBy = 'name';
+ this.$scope.reverse = false;
+ this.$scope.setValidState(true);
+ this.$scope.tableHeadersList = [
+ {title: 'Name', property: 'name'},
+ {title: 'Type', property: 'type'},
+ {title: 'Default Value', property: 'defaultValue'}
+ ];
+ this.$scope.sort = (sortBy:string):void => {
+ this.$scope.reverse = (this.$scope.sortBy === sortBy) ? !this.$scope.reverse : false;
+ this.$scope.sortBy = sortBy;
+ };
+
+ this.$scope.addOrUpdateAttribute = (attribute?:AttributeModel):void => {
+ this.ModalsHandler.openEditAttributeModal(attribute ? attribute : new AttributeModel(), this.$scope.component);
+ };
+
+ this.$scope.delete = (attribute:AttributeModel):void => {
+
+ let onOk = ():void => {
+ this.$scope.component.deleteAttribute(attribute.uniqueId);
+ };
+ let title:string = this.$filter('translate')("ATTRIBUTE_VIEW_DELETE_MODAL_TITLE");
+ let message:string = this.$filter('translate')("ATTRIBUTE_VIEW_DELETE_MODAL_TEXT", "{'name': '" + attribute.name + "'}");
+ this.ModalsHandler.openConfirmationModal(title, message, false).then(onOk);
+ };
+ }
+}
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/attributes/attributes-view.html b/catalog-ui/src/app/view-models/workspace/tabs/attributes/attributes-view.html
new file mode 100644
index 0000000000..59ba933a0a
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/attributes/attributes-view.html
@@ -0,0 +1,52 @@
+<div class="workspace-attributes">
+ <div class="add-btn" data-tests-id="add-attribute-button" ng-if="!isViewMode()"
+ data-ng-class="{'disabled': isDisableMode()}" data-ng-click="addOrUpdateAttribute()" data-tests-id="add-attribute-button">Add</div>
+ <div class="table-container-flex">
+ <div class="table" data-ng-class="{'view-mode': isViewMode()}">
+ <div class="head flex-container">
+ <div class="table-header head-row hand flex-item" data-ng-repeat="header in tableHeadersList track by $index" data-ng-click="sort(header.property)">{{header.title}}
+ <span data-ng-if="sortBy === header.property" class="table-header-sort-arrow" data-ng-class="{'down': reverse, 'up':!reverse}"> </span>
+ </div>
+ <div class="table-no-text-header head-row flex-item" ng-if="!isViewMode()"></div>
+ <!--div class="table-no-text-header head-row flex-item"></div-->
+ </div>
+
+ <div class="body">
+ <perfect-scrollbar scroll-y-margin-offset="0" include-padding="true" class="scrollbar-container">
+ <div data-ng-if="component.attributes.length === 0" class="no-row-text" data-ng-class="{'disabled': isDisableMode()}">
+ There are no attributes to display <br>
+ <span ng-if="!isViewMode()"> click <a data-ng-click="addOrUpdateAttribute()">here</a> to add one </span>
+
+ </div>
+ <div data-ng-repeat-start="attribute in component.attributes | orderBy:sortBy:reverse track by $index"
+ class="flex-container data-row" data-ng-class="{'selected': attribute.selected}"
+ data-ng-click="attribute.selected = !attribute.selected" data-tests-id="attributes-table-row">
+
+ <div class="table-col-general flex-item text">
+ <span class="sprite table-arrow" data-ng-class="{'opened': attribute.selected}"></span>
+ <span data-tests-id="{{attribute.name}}" tooltips tooltip-content="{{attribute.name}}">{{attribute.name}}</span>
+
+ </div>
+
+ <div class="table-col-general flex-item text" data-tests-id="{{attribute.type}}" data-ng-bind="attribute.type"></div>
+
+ <div class="table-col-general flex-item text">
+ <span tooltips tooltip-content="{{attribute.defaultValue}}" data-tests-id="{{attribute.defaultValue}}" data-ng-bind="attribute.defaultValue"></span>
+ </div>
+
+ <div class="table-btn-col flex-item" ng-if="!isViewMode()">
+ <button class="table-edit-btn" data-tests-id="edit_{{attribute.name}}" data-ng-show="attribute.parentUniqueId==component.uniqueId"
+ data-ng-click="addOrUpdateAttribute(attribute); $event.stopPropagation();" data-ng-class="{'disabled': isViewMode()}"> </button>
+ <button class="table-delete-btn" data-tests-id="delete_{{attribute.name}}" data-ng-show="attribute.parentUniqueId==component.uniqueId"
+ data-ng-click="delete(attribute); $event.stopPropagation();" data-ng-class="{'disabled': isViewMode()}"> </button>
+ </div>
+ </div>
+ <div data-ng-repeat-end="" data-ng-if="attribute.selected && attribute.description" class="item-opened" data-ng-bind="attribute.description">
+ </div>
+ </perfect-scrollbar>
+ </div>
+
+ </div>
+ </div>
+
+</div>
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/attributes/attributes.less b/catalog-ui/src/app/view-models/workspace/tabs/attributes/attributes.less
new file mode 100644
index 0000000000..ffd28afce4
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/attributes/attributes.less
@@ -0,0 +1,54 @@
+.workspace-attributes {
+
+ width: 93%;
+ display: inline-block;
+ .w-sdc-classic-btn {
+ float: right;
+ margin-bottom: 10px;
+ }
+
+ .table{
+ height:490px;
+ margin-bottom: 0;
+ }
+
+ .table-container-flex {
+ margin-top: 27px;
+
+ .text{
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: inline-block;
+ white-space: nowrap;
+ }
+
+ .flex-item:nth-child(1) {
+ flex-grow: 15;
+
+ .hand;
+ span.table-arrow {
+ margin-right: 7px;
+ }
+ }
+
+ .flex-item:nth-child(2) {
+ flex-grow: 6;
+ }
+
+ .flex-item:nth-child(3) {
+ flex-grow: 9;
+ }
+
+ .flex-item:nth-child(4) {
+ flex-grow: 3;
+ padding-top: 10px;
+ }
+
+ .flex-item:nth-child(5) {
+ flex-grow: 1;
+
+ }
+
+ }
+
+}
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/composition/composition-view-model.ts b/catalog-ui/src/app/view-models/workspace/tabs/composition/composition-view-model.ts
new file mode 100644
index 0000000000..e2d95280c8
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/composition/composition-view-model.ts
@@ -0,0 +1,242 @@
+/*-
+ * ============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=========================================================
+ */
+'use strict';
+import {Component, Product, ComponentInstance, IAppMenu} from "app/models";
+import {SharingService, CacheService, EventListenerService, LeftPaletteLoaderService} from "app/services";
+import {ModalsHandler, GRAPH_EVENTS, ComponentFactory, ChangeLifecycleStateHandler, MenuHandler} from "app/utils";
+import {IWorkspaceViewModelScope} from "../../workspace-view-model";
+import {ComponentServiceNg2} from "app/ng2/services/component-services/component.service";
+import {ComponentGenericResponse} from "app/ng2/services/responses/component-generic-response";
+
+export interface ICompositionViewModelScope extends IWorkspaceViewModelScope {
+
+ currentComponent:Component;
+ selectedComponent:Component;
+ isLoading:boolean;
+ graphApi:any;
+ sharingService:SharingService;
+ sdcMenu:IAppMenu;
+ version:string;
+ isViewOnly:boolean;
+ isLoadingRightPanel:boolean;
+ onComponentInstanceVersionChange(component:Component);
+ isComponentInstanceSelected():boolean;
+ updateSelectedComponent():void
+ openUpdateModal();
+ deleteSelectedComponentInstance():void;
+ onBackgroundClick():void;
+ setSelectedInstance(componentInstance:ComponentInstance):void;
+ printScreen():void;
+
+ cacheComponentsInstancesFullData:Component;
+}
+
+export class CompositionViewModel {
+
+ static '$inject' = [
+ '$scope',
+ '$log',
+ 'sdcMenu',
+ 'MenuHandler',
+ '$uibModal',
+ '$state',
+ 'Sdc.Services.SharingService',
+ '$filter',
+ 'Sdc.Services.CacheService',
+ 'ComponentFactory',
+ 'ChangeLifecycleStateHandler',
+ 'LeftPaletteLoaderService',
+ 'ModalsHandler',
+ 'EventListenerService',
+ 'ComponentServiceNg2'
+ ];
+
+ constructor(private $scope:ICompositionViewModelScope,
+ private $log:ng.ILogService,
+ private sdcMenu:IAppMenu,
+ private MenuHandler:MenuHandler,
+ private $uibModal:ng.ui.bootstrap.IModalService,
+ private $state:ng.ui.IStateService,
+ private sharingService:SharingService,
+ private $filter:ng.IFilterService,
+ private cacheService:CacheService,
+ private ComponentFactory:ComponentFactory,
+ private ChangeLifecycleStateHandler:ChangeLifecycleStateHandler,
+ private LeftPaletteLoaderService:LeftPaletteLoaderService,
+ private ModalsHandler:ModalsHandler,
+ private eventListenerService:EventListenerService,
+ private ComponentServiceNg2: ComponentServiceNg2) {
+
+ this.$scope.setValidState(true);
+ this.initScope();
+ this.initGraphData();
+ this.$scope.updateSelectedMenuItem();
+ this.registerGraphEvents(this.$scope);
+ }
+
+
+ private initGraphData = ():void => {
+ if(!this.$scope.component.componentInstances || !this.$scope.component.componentInstancesRelations ) {
+ this.$scope.isLoading = true;
+ this.ComponentServiceNg2.getComponentInstancesAndRelation(this.$scope.component).subscribe((response:ComponentGenericResponse) => {
+ this.$scope.component.componentInstances = response.componentInstances;
+ this.$scope.component.componentInstancesRelations = response.componentInstancesRelations;
+ this.$scope.isLoading = false;
+ this.initComponent();
+ this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_COMPOSITION_GRAPH_DATA_LOADED);
+ });
+ } else {
+ this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_COMPOSITION_GRAPH_DATA_LOADED);
+ }
+ this.eventListenerService.unRegisterObserver(GRAPH_EVENTS.ON_COMPOSITION_GRAPH_DATA_LOADED);
+ };
+
+
+ private cacheComponentsInstancesFullData:Array<Component>;
+
+ private initComponent = ():void => {
+
+ this.$scope.currentComponent = this.$scope.component;
+ this.$scope.selectedComponent = this.$scope.currentComponent;
+ this.updateUuidMap();
+ this.$scope.isViewOnly = this.$scope.isViewMode();
+ };
+ private registerGraphEvents = (scope:ICompositionViewModelScope):void => {
+
+ this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_NODE_SELECTED, scope.setSelectedInstance);
+ this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_GRAPH_BACKGROUND_CLICKED, scope.onBackgroundClick);
+
+ };
+
+ private openUpdateComponentInstanceNameModal = ():void => {
+ this.ModalsHandler.openUpdateComponentInstanceNameModal(this.$scope.currentComponent).then(()=> {
+ this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_COMPONENT_INSTANCE_NAME_CHANGED, this.$scope.currentComponent.selectedInstance);
+
+ });
+ };
+
+ private removeSelectedComponentInstance = ():void => {
+ this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_DELETE_MULTIPLE_COMPONENTS);
+ };
+
+ private updateUuidMap = ():void => {
+ /**
+ * In case user press F5, the page is refreshed and this.sharingService.currentEntity will be undefined,
+ * but after loadService or loadResource this.sharingService.currentEntity will be defined.
+ * Need to update the uuidMap with the new resource or service.
+ */
+ this.sharingService.addUuidValue(this.$scope.currentComponent.uniqueId, this.$scope.currentComponent.uuid);
+ };
+
+ private initScope = ():void => {
+
+ this.$scope.sharingService = this.sharingService;
+ this.$scope.sdcMenu = this.sdcMenu;
+ this.$scope.isLoading = false;
+ this.$scope.isLoadingRightPanel = false;
+ this.$scope.graphApi = {};
+ this.$scope.version = this.cacheService.get('version');
+ this.initComponent();
+
+ this.cacheComponentsInstancesFullData = new Array<Component>();
+
+ this.$scope.isComponentInstanceSelected = ():boolean => {
+ return this.$scope.currentComponent && this.$scope.currentComponent.selectedInstance != undefined && this.$scope.currentComponent.selectedInstance != null;
+ };
+
+ this.$scope.updateSelectedComponent = ():void => {
+ if (this.$scope.currentComponent.selectedInstance) {
+
+ let componentParent = _.find(this.cacheComponentsInstancesFullData, (component) => {
+ return component.uniqueId === this.$scope.currentComponent.selectedInstance.componentUid;
+ });
+ if (componentParent) {
+ this.$scope.selectedComponent = componentParent;
+ }
+ else {
+ try {
+ let onSuccess = (component:Component) => {
+ this.$scope.isLoadingRightPanel = false;
+ this.$scope.selectedComponent = component;
+ this.cacheComponentsInstancesFullData.push(component);
+ };
+ let onError = (component:Component) => {
+ console.log("Error updating selected component");
+ this.$scope.isLoadingRightPanel = false;
+ };
+ this.ComponentFactory.getComponentFromServer(this.$scope.currentComponent.selectedInstance.originType, this.$scope.currentComponent.selectedInstance.componentUid).then(onSuccess, onError);
+ } catch (e) {
+ console.log("Error updating selected component", e);
+ this.$scope.isLoadingRightPanel = false;
+ }
+ }
+ }
+ else {
+
+ this.$scope.selectedComponent = this.$scope.currentComponent;
+ }
+ };
+
+ this.$scope.setSelectedInstance = (selectedComponent:ComponentInstance):void => {
+
+ this.$log.debug('composition-view-model::onNodeSelected:: with id: ' + selectedComponent.uniqueId);
+ this.$scope.currentComponent.setSelectedInstance(selectedComponent);
+ this.$scope.updateSelectedComponent();
+
+ if (this.$state.current.name === 'workspace.composition.api') {
+ this.$state.go('workspace.composition.details');
+ }
+ if (this.$state.current.name === 'workspace.composition.relations' && this.$scope.currentComponent.isProduct()) {
+ this.$state.go('workspace.composition.details');
+ }
+ };
+
+ this.$scope.onBackgroundClick = ():void => {
+ this.$scope.currentComponent.selectedInstance = null;
+ this.$scope.selectedComponent = this.$scope.currentComponent;
+
+ if (this.$state.current.name === 'workspace.composition.api') {
+ this.$state.go('workspace.composition.details');
+ }
+ };
+
+ this.$scope.openUpdateModal = ():void => {
+ this.openUpdateComponentInstanceNameModal();
+ };
+
+ this.$scope.deleteSelectedComponentInstance = ():void => {
+ let state = "deleteInstance";
+ let onOk = ():void => {
+ this.removeSelectedComponentInstance();
+ //this.$scope.graphApi.deleteSelectedNodes();
+ };
+ let title:string = this.$scope.sdcMenu.alertMessages[state].title;
+ let message:string = this.$scope.sdcMenu.alertMessages[state].message.format([this.$scope.currentComponent.selectedInstance.name]);
+ this.ModalsHandler.openAlertModal(title, message).then(onOk);
+ };
+
+ this.$scope.onComponentInstanceVersionChange = (component:Product):void => {
+ this.$scope.currentComponent = component;
+ this.$scope.setComponent(this.$scope.currentComponent);
+ this.$scope.updateSelectedComponent();
+ }
+
+ }
+}
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/composition/composition-view.html b/catalog-ui/src/app/view-models/workspace/tabs/composition/composition-view.html
new file mode 100644
index 0000000000..761ae53909
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/composition/composition-view.html
@@ -0,0 +1,95 @@
+<div class="workspace-composition">
+ <loader data-display="isLoading"></loader>
+ <div class="w-sdc-designer-canvas" data-ng-class="{sidebaractive: displayDesignerRightSidebar}">
+ <palette current-component="currentComponent"
+ is-view-only="isViewOnly"
+ is-loading="isLoading"></palette>
+
+ <composition-graph component="currentComponent" data-tests-id="canvas"
+ is-view-only="isViewOnly"></composition-graph>
+ </div>
+
+ <div class="w-sdc-designer-sidebar-toggle" data-ng-class="{'active': displayDesignerRightSidebar}"
+ data-ng-init="displayDesignerRightSidebar = true"
+ data-ng-click="displayDesignerRightSidebar = !displayDesignerRightSidebar">
+ <div class="w-sdc-designer-sidebar-toggle-icon sprite-new pointer menu-open-left"></div>
+ </div>
+
+ <div class="w-sdc-designer-sidebar" data-ng-class="{'view-mode':isViewOnly}">
+
+ <div class="w-sdc-designer-sidebar-head" data-tests-id="w-sdc-designer-sidebar-head">
+ <div class="w-sdc-designer-sidebar-logo-ph">
+ <div class="large {{selectedComponent.iconSprite}} {{selectedComponent.icon}}">
+ <div ng-if="isComponentInstanceSelected()"
+ data-ng-class="{'non-certified':'CERTIFIED' !== selectedComponent.lifecycleState, 'smaller-icon': selectedComponent.icon==='vl' || selectedComponent.icon==='cp'}"
+ tooltips tooltip-side="top" tooltip-content="Not certified"></div>
+ </div>
+ </div>
+
+ <div class="w-sdc-designer-sidebar-logo">
+ <span class="w-sdc-designer-sidebar-logo-title" data-tests-id="selectedCompTitle" tooltips
+ tooltip-class="tooltip-custom break-word-tooltip"
+ tooltip-content="&#8203;{{isComponentInstanceSelected() ? currentComponent.selectedInstance.name : currentComponent.name | resourceName}}"
+ data-ng-bind="isComponentInstanceSelected() ? currentComponent.selectedInstance.name : currentComponent.name | resourceName"></span>
+ </div>
+ <div class="sprite e-sdc-small-icon-pencil w-sdc-designer-update-resource-icon"
+ data-ng-if="!isViewOnly && isComponentInstanceSelected()"
+ data-ng-click="openUpdateModal()" id="editPencil"></div>
+
+ <div class="sprite e-sdc-small-icon-delete w-sdc-designer-delete-resource-icon"
+ data-tests-id="e-sdc-small-icon-delete"
+ data-ng-if="!isViewOnly && isComponentInstanceSelected()"
+ data-ng-click="!isLoading && deleteSelectedComponentInstance()" title="Delete Resource Instance"></div>
+ </div>
+
+ <div class="w-sdc-designer-sidebar-tabs">
+ <button class="i-sdc-designer-sidebar-tab" data-ui-sref-active="active"
+ data-ui-sref="workspace.composition.details"
+ tooltips tooltip-class="tooltip-custom tab-tooltip" tooltip-content="Information"
+ data-tests-id="information-tab">
+ <div class="i-sdc-designer-sidebar-tab-icon sprite-new info"></div>
+ </button>
+ <!--<button class="i-sdc-designer-sidebar-tab" data-ui-sref-active="active"-->
+ <!--ui-sref="workspace.composition.structure"-->
+ <!--tooltips tooltip-class="tooltip-custom tab-tooltip" tooltip-content="Composition">-->
+ <!--<div class="i-sdc-designer-sidebar-tab-icon sprite-new structure"></div>-->
+ <!--</button>-->
+ <button class="i-sdc-designer-sidebar-tab" data-ui-sref-active="active"
+ data-ui-sref="workspace.composition.deployment"
+ tooltips tooltip-class="tooltip-custom tab-tooltip" tooltip-content="Deployment Artifacts"
+ data-tests-id="deployment-artifact-tab">
+ <div class="i-sdc-designer-sidebar-tab-icon sprite-new deployment-artifacts"></div>
+ </button>
+ <button tooltips tooltip-class="tooltip-custom tab-tooltip"
+ tooltip-content="{{selectedComponent.isResource() ? 'Properties and Attributes': 'Inputs'}}"
+ class="i-sdc-designer-sidebar-tab" data-ui-sref-active="active"
+ data-ui-sref="workspace.composition.properties"
+ data-tests-id="properties-and-attributes-tab">
+ <div class="i-sdc-designer-sidebar-tab-icon sprite-new"
+ ng-class="selectedComponent.isResource() ? 'properties': 'inputs'"></div>
+ </button>
+ <button class="i-sdc-designer-sidebar-tab" data-ui-sref-active="active"
+ data-ui-sref="workspace.composition.artifacts"
+ tooltips tooltip-class="tooltip-custom tab-tooltip" tooltip-content="Information Artifacts">
+ <div class="i-sdc-designer-sidebar-tab-icon sprite-new information-artifacts"></div>
+ </button>
+ <button data-ng-show="!selectedComponent.isService()" class="i-sdc-designer-sidebar-tab"
+ data-ui-sref-active="active" ui-sref="workspace.composition.relations"
+ tooltips tooltip-class="tooltip-custom tab-tooltip tooltip-rightside"
+ tooltip-content="Requirements and Capabilities">
+ <div class="i-sdc-designer-sidebar-tab-icon sprite-new relations"></div>
+ </button>
+ <button data-ng-show="selectedComponent.isService()" class="i-sdc-designer-sidebar-tab"
+ data-ui-sref-active="active" ui-sref="workspace.composition.api" data-tests-id="tab-api"
+ tooltips tooltip-class="tooltip-custom tab-tooltip tooltip-rightside" tooltip-content="API">
+ <div class="i-sdc-designer-sidebar-tab-icon sprite-new api"></div>
+ </button>
+
+ </div>
+
+ <div data-ui-view="" class="w-sdc-designer-sidebar-tab-content-view"></div>
+
+ <loader data-display="isLoadingRightPanel" relative="true" size="medium"></loader>
+
+ </div>
+</div>
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/composition/composition.less b/catalog-ui/src/app/view-models/workspace/tabs/composition/composition.less
new file mode 100644
index 0000000000..501805be3f
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/composition/composition.less
@@ -0,0 +1,873 @@
+
+.composition{
+ .sdc-workspace-container{
+ .w-sdc-main-container{
+ .w-sdc-main-right-container{
+ left:0;
+ //overflow-y: scroll;
+ .sdc-workspace-top-bar {
+ padding-left: 295px;
+ .not-latest{
+ left: 270px;
+ }
+ }
+ .w-sdc-main-container-body-content{
+ padding: 0 0 0 247px;
+ }
+
+ > div:first-child{
+ padding: 0;
+ }
+ }
+ }
+ }
+}
+
+.workspace-composition {
+ height:100%;
+ display: block;
+ text-align: left;
+ align-items: left;
+ padding: 0;
+
+
+
+ // ---------------------------------------------------------------------------------------------------
+ // Sidebar
+ // ---------------------------------------------------------------------------------------------------
+
+
+
+ .w-sdc-designer-sidebar-toggle {
+ background-color: @main_color_p;
+ border-left: 1px solid @main_color_o;
+ border-bottom: 1px solid @main_color_o;
+ height: 21px;
+ position: absolute;
+ right: 0;
+ top: 53px;
+ width: 17px;
+ transition: right 0.2s;
+ z-index: 10;
+ .box-shadow(-1px 1px 3px 0 @main_color_n);
+
+ &.active {
+ right: 302px;
+ .w-sdc-designer-sidebar-toggle-icon{
+ transform: rotate(180deg);
+ }
+ }
+
+ }
+
+ .w-sdc-designer-sidebar-toggle-icon {
+ margin-left: 6px;
+ margin-top: 6px;
+ }
+
+ .w-sdc-designer-sidebar {
+ background-color:@main_color_p ;
+ .noselect;
+ bottom: @footer_height;
+ position: fixed;
+ right: -302px;
+ width: 302px;
+ top: 102px;
+ transition: right 0.2s;
+ z-index: 9;
+ .box-shadow(-7px -3px 6px -8px @main_color_n);
+
+ }
+
+ .w-sdc-designer-sidebar-toggle.active + .w-sdc-designer-sidebar {
+ right: 0;
+
+ }
+
+ .w-sdc-designer-sidebar-head {
+ padding: 36px 30px 30px 30px;
+ height: 120px;
+ }
+
+ .w-sdc-designer-sidebar-logo-ph {
+ display: inline-block;
+ vertical-align: middle;
+ line-height: 48px;
+ height: 48px;
+ }
+
+ .w-sdc-designer-sidebar-logo {
+ .g_6;
+ display: inline-block;
+ margin-left: 10px;
+ font-weight: 500;
+ }
+
+ .w-sdc-designer-sidebar-logo-title {
+ .s_16_r;
+ .selectable;
+ vertical-align: middle;
+ text-overflow: ellipsis;
+ max-width: 167px;
+ display: inline-block;
+ white-space: nowrap;
+ overflow: hidden;
+ }
+
+ .w-sdc-designer-update-resource-icon {
+ .hand;
+ position: absolute;
+ right: 20px;
+ top: 10px;
+ }
+
+ .w-sdc-designer-delete-resource-icon {
+ .hand;
+ position: absolute;
+ right: 40px;
+ top: 10px;
+ }
+
+ .w-sdc-designer-sidebar-tabs {
+ .bg_e;
+ }
+
+ .w-sdc-designer-sidebar-tabs::after {
+ clear: both;
+ content: '';
+ display: table;
+ }
+
+ .i-sdc-designer-sidebar-tab {
+ background-color: @main_color_p;
+ border: 1px solid @tlv_color_u;;
+ border-left: none;
+ display: inline-block;
+ float: left;
+ height: 36px;
+ padding-top: 9px;
+ text-align: center;
+ width: 60px;
+ .hand;
+
+ &:focus {
+ outline: none;
+ }
+ &.tab-disabled {
+ /* .disabled; */
+ }
+ &.active, &:hover:enabled {
+ background-color: @tlv_color_u;
+ .i-sdc-designer-sidebar-tab-icon {
+ opacity: 1;
+
+
+ }
+
+ }
+
+ div& {
+ padding-top: 0;
+ }
+ /*for tooltip on disabled buttons*/
+ }
+
+ .i-sdc-designer-sidebar-tab-icon {
+ margin-top: 5px ;
+ // opacity: .4;
+ }
+
+ .w-sdc-designer-sidebar-tab-content {
+ .perfect-scrollbar;
+ height: 100%;
+ }
+
+ .w-sdc-designer-sidebar-tab-content-view {
+ position: absolute;
+ top: 156px;
+ bottom: 0;
+ width: 100%;
+ padding-bottom: 10px;
+
+ }
+
+ .w-sdc-designer-sidebar-section {
+ }
+
+ .w-sdc-designer-sidebar-section-title {
+ .m_14_m;
+ background-color: @tlv_color_u;
+ .hand;
+ clear: both;
+ height: 32px;
+ line-height: 32px;
+ margin-top: 1px;
+ padding: 0 40px 0 20px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ position: relative;
+ width: 100%;
+ display: block;
+
+ &.expanded {
+ .w-sdc-designer-sidebar-section-title-icon {
+ transform: rotate(180deg);
+ }
+ }
+ }
+
+ .w-sdc-designer-sidebar-section-title-text {
+ max-width: 240px;
+ display: inline-block;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ position: relative;
+ }
+
+ .w-sdc-designer-sidebar-section-title-icon {
+ .hand;
+ .sprite-new;
+ .arrow-up;
+ right: 16px;
+ top: 10px;
+ transition: .3s all;
+ position: absolute;
+ }
+
+ .w-sdc-designer-sidebar-section-content {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ .w-sdc-designer-sidebar-section-title + .w-sdc-designer-sidebar-section-content {
+ margin: 0 auto;
+ }
+
+ .w-sdc-designer-sidebar-section-title.expanded + .w-sdc-designer-sidebar-section-content {
+ margin: 0 auto 1px;
+
+ }
+
+ .i-sdc-designer-sidebar-section-content-item {
+ .b_7;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ //max-width: 250px;
+
+ &.description {
+ margin-top: 28px;
+ white-space: normal;
+ word-wrap: break-word;
+ }
+ }
+
+ .i-sdc-designer-sidebar-section-content-item-tag {
+ .g_7;
+ .bg_c;
+ border-radius: 4px;
+ display: inline-block;
+ line-height: 25px;
+ margin: 0 4px 6px 0;
+ min-width: 50px;
+ padding: 0 9px;
+ text-align: center;
+ max-width: 280px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ .w-sdc-designer-sidebar-section-footer {
+ margin-top: 10px;
+ text-align: center;
+ width: 100%;
+ }
+
+
+
+ .w-sdc-designer-sidebar-section-footer-action {
+ width: 180px;
+ margin-top: 10px;
+ }
+
+ //////////////////////Relationship
+ .w-sdc-designer-sidebar-section-requirements {
+ border-bottom: 1px solid @color_e;
+ margin: 0 13px 20px 13px;
+ padding: 15px 0 0;
+ }
+
+ .w-sdc-designer-sidebar-section-requirements-item {
+ margin-bottom: 20px;
+ }
+
+ .w-sdc-designer-sidebar-section-requirements-label {
+ display: inline-block;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ vertical-align: middle;
+ white-space: nowrap;
+ width: 102px;
+ }
+
+ .w-sdc-designer-sidebar-section-requirements-select {
+ border: 1px solid @color_e;
+ min-height: 30px;
+ padding: 4px 13px;
+ width: 168px;
+ }
+
+ //////////////////////Properties
+ .i-sdc-designer-sidebar-section-content-item-property-and-attribute {
+ .b_7;
+ border-bottom: 1px solid @color_e;
+ min-height: 72px;
+ padding: 15px 10px 10px 18px;
+ position: relative;
+
+ &:first-child {
+ //margin-top: -18px;
+ }
+
+ &:hover {
+ // .bg_c_hover;
+ .bg_c;
+ transition: all .3s;
+
+ .i-sdc-designer-sidebar-section-content-item-button {
+ display: block;
+ }
+ }
+ }
+
+ .i-sdc-designer-sidebar-section-content-item-property-and-attribute-label {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 200px;
+ white-space: nowrap;
+ display: inline-block;
+ &:hover {
+ .a_7;
+ }
+ }
+
+ .i-sdc-designer-sidebar-section-content-item-property-value {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 200px;
+ display: inline-block;
+ white-space: nowrap;
+
+ }
+
+ .i-sdc-designer-sidebar-section-content-item-property-label-value {
+ }
+
+ .i-sdc-designer-sidebar-section-content-item-button {
+ display: none;
+ position: absolute;
+ top: 25px;
+
+ &.update {
+ background-color: transparent;
+ border: 0;
+ right: 60px;
+ }
+
+ &.delete {
+ background-color: transparent;
+ border: 0;
+ right: 13px;
+ }
+
+ &.download {
+ background-color: transparent;
+ border: 0;
+ right: 35px;
+ }
+
+ &.download-env {
+ background-color: transparent;
+ border: 0;
+ right: 35px;
+ margin-top: 65px;
+ }
+
+ &.update-env {
+ background-color: transparent;
+ border: 0;
+ right: 15px;
+ margin-top: 65px;
+ }
+
+ &.attach {
+ background-color: transparent;
+ border: 0;
+ right: 15px;
+ }
+ }
+
+ // ---------------------------------------------------------------------------------------------------
+ // Canvas
+ // ---------------------------------------------------------------------------------------------------
+ .w-sdc-designer-canvas {
+ height:100%;
+ .noselect;
+ .bg_c;
+ bottom: 0;
+ // position: fixed;
+ //right: 0;
+ //left: 240px;
+ //top: 94px;
+ .view-mode{
+ background-color: #f8f8f8;
+ border:0;
+ }
+ }
+
+ .w-sdc-designer-canvas.sidebaractive {
+ //right: 300px;
+ }
+
+ .w-sdc-designer-element {
+ .hand;
+ width: 200px;
+ height: 100px;
+ position: absolute;
+ text-align: center;
+ top: 50%;
+ margin-top: -200px;
+ left: 50%;
+ margin-left: -50px;
+ }
+
+ .w-sdc-designer-resource-label {
+ .q_7;
+ }
+
+ .w-sdc-designer-resource-label-indicator {
+ .bg_q;
+ border-radius: 50%;
+ display: inline-block;
+ height: 10px;
+ margin-right: 6px;
+ vertical-align: middle;
+ width: 10px;
+
+ &.valid {
+ .bg_l;
+ }
+
+ &.invalid {
+ .bg_h;
+ }
+ }
+
+ // ---------------------------------------------------------------------------------------------------
+ // Leftbar
+ // ---------------------------------------------------------------------------------------------------
+ .w-sdc-designer-leftbar {
+ background-color: @main_color_p;
+ bottom: 0;
+ left: 0;
+ overflow-y: scroll;
+ overflow-x: hidden;
+ position: absolute;
+ top: 0;
+ width: 244px;
+ .box-shadow(7px -3px 6px -8px @main_color_n);
+
+ }
+
+ .w-sdc-designer-leftbar-title {
+
+ .p_16_m;
+ background-color: @main_color_n;
+ line-height: 40px;
+ padding: 0 17px;
+ }
+
+ .w-sdc-designer-leftbar-title-count {
+ float: right;
+ }
+
+ .w-scd-diagram-container {
+ // left: 240px;
+ //right: 300px;
+ }
+
+ .w-sdc-designer-leftbar-search {
+ background-color: @tlv_color_u;
+ padding: 10px;
+ white-space: nowrap;
+ position: relative;
+ }
+
+ .w-sdc-designer-leftbar-search-input {
+ border: 1px solid @color_e;
+ .border-radius(4px);
+ height: 30px;
+ margin: 0;
+ padding: 0px 28px 3px 10px;
+ vertical-align: 4px;
+ width: 100%;
+ outline: none;
+ font-style: italic;
+ }
+
+ .w-sdc-designer-leftbar-search-filter {
+
+ }
+
+ .i-sdc-designer-leftbar-section {
+ .hand;
+ }
+
+ .i-sdc-designer-leftbar-section-title {
+ .m_14_m;
+ background-color: @tlv_color_u;
+ .hand;
+ clear: both;
+ height: 40px;
+ line-height: 40px;
+ margin-top: 1px;
+ padding: 0 10px;
+ position: relative;
+ text-transform: uppercase;
+ font-weight: bold;
+ }
+
+ .i-sdc-designer-leftbar-section-title-icon {
+ .hand;
+ .sprite-new;
+ .arrow-up;
+ width: 15px;
+ height: 9px;
+ position: absolute;
+ right: 13px;
+ top: 18px;
+ transition: .3s all;
+ }
+
+ .i-sdc-designer-leftbar-section.expanded .i-sdc-designer-leftbar-section-title-icon {
+ transform: rotate(180deg);
+ margin-right: 2px;
+ }
+
+ .i-sdc-designer-leftbar-section-content {
+ background-color: @main_color_o;
+ }
+
+ .i-sdc-designer-leftbar-section-content-item {
+ background-color: @main_color_p;
+ overflow: hidden;
+
+ &:hover {
+ background-color: @main_color_p;
+ }
+
+ .cp{
+ margin: 6px;
+ }
+
+ .vl{
+ margin: 6px;
+ }
+ }
+
+ .i-sdc-designer-leftbar-section-content-subcat {
+ .m_14_m;
+ background-color: @tlv_color_t;
+ line-height: 35px;
+ padding: 0 10px;
+ cursor: default;
+
+
+ &:hover {
+ background-color: @func_color_r;
+ }
+
+
+ }
+
+ .i-sdc-designer-leftbar-section .i-sdc-designer-leftbar-section-content .i-sdc-designer-leftbar-section-content-item {
+ max-height: 0px;
+ margin: 0 auto;
+ transition: all .3s;
+ }
+
+ .i-sdc-designer-leftbar-section.expanded .i-sdc-designer-leftbar-section-content .i-sdc-designer-leftbar-section-content-item {
+ max-height: 64px;
+ margin: 0 auto 1px auto;
+ // padding: 4px 13px;
+ }
+
+ .i-sdc-designer-leftbar-section.expanded .i-sdc-designer-leftbar-section-content .i-sdc-designer-leftbar-section-content-subcat {
+ margin: 0;
+ }
+
+ .i-sdc-designer-leftbar-section-content-item-icon-ph {
+ display: inline-block;
+ margin: 12px 0 12px 10px;
+ pointer-events: auto;
+
+ .non-certified {
+ position: relative;
+ left: 27px;
+ bottom: 6px;
+ .sprite;
+ .s-sdc-state-non-certified;
+ display: block;
+
+ &.smaller-icon {
+ bottom: 6px;
+ left: 13px;
+ }
+ }
+
+
+
+ }
+
+ .non-certified {
+ position: relative;
+ left: 43px;
+ bottom: 3px;
+ .sprite;
+ .s-sdc-state-non-certified;
+ display: block;
+
+ &.smaller-icon {
+ left: 35px;
+ bottom: -14px;
+ }
+ }
+ /*
+ .i-sdc-composition-leftbar-section-content-item-icon {
+ background-image: url('../../../styles/images/resource-icons/default.png');
+ // position: absolute;
+ right: 20px;
+ top: 10px;
+ height: 40px;
+ width: 40px;
+ background-size: 40px;
+ }
+ */
+
+ .i-sdc-designer-leftbar-section-content-item-info {
+ display: inline-block;
+ // margin-left: 10px;
+ //overflow: hidden;
+ // vertical-align: middle;
+ width: 160px;
+ padding: 0 0 0 10px;
+ }
+
+ .i-sdc-designer-leftbar-section-content-item-info-title {
+ .m_14_m;
+ line-height: 14px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ text-transform: uppercase;
+ max-width: 120px;
+ display: inline-block;
+ white-space: nowrap;
+ vertical-align: bottom;
+ }
+
+ .i-sdc-designer-leftbar-section-content-item-info-text {
+ .p_3;
+ line-height: 15px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ //margin: -1px 0 2px 0;
+ }
+
+ .i-sdc-designer-leftbar-section-content-item-info-text-link {
+ color: @color_s;
+ text-decoration: underline;
+ float: right;
+ position: absolute;
+ right: 17px;
+ // bottom: 5px;
+ }
+
+ // ---------------------------------------------------------------------------------------------------
+ // Form actions
+ // ---------------------------------------------------------------------------------------------------
+ .w-sdc-form-actions-container.add-property {
+ text-align: center;
+ width: 100%;
+ margin-top: 2px;
+ margin-bottom: 12px;
+
+ .w-sdc-form-action {
+ width: 245px;
+ }
+ .w-sdc-form-action.add-property-add-another {
+ .bg_a;
+ margin-left: 35px;
+ }
+ .w-sdc-form-action.add-property-done {
+ margin-left: 312px;
+ }
+ .w-sdc-form-action.save {
+ margin-left: 327px;
+ margin-bottom: 30px;
+ }
+
+ }
+
+ // ---------------------------------------------------------------------------------------------------
+ // Top menu
+ // ---------------------------------------------------------------------------------------------------
+ .w-sdc-header-menu {
+ padding: 25px 0;
+ text-align: center;
+ white-space: nowrap;
+ }
+
+ .i-sdc-header-menu-item {
+ cursor: pointer;
+ display: inline-block;
+ height: 43px;
+ min-width: 93px;
+ padding: 0 38px;
+ position: relative;
+ vertical-align: middle;
+
+ &::after {
+ border-right: 1px solid @color_m;
+ content: '';
+ display: block;
+ height: 43px;
+ right: 0;
+ position: absolute;
+ top: 0;
+ width: 2px;
+ }
+
+ &:first-child {
+ &::before {
+ border-right: 1px solid @color_m;
+ content: '';
+ display: block;
+ height: 43px;
+ left: 0;
+ position: absolute;
+ top: 0;
+ width: 2px;
+ }
+ }
+ }
+
+ .i-sdc-header-menu-item-icon {
+ display: inline-block;
+ height: 20px;
+ width: 28px;
+ }
+
+ .i-sdc-header-menu-item-label {
+ .g_1;
+ line-height: 18px;
+ }
+
+ // ---------------------------------------------------------------------------------------------------
+ // Canvas inline menu
+ // ---------------------------------------------------------------------------------------------------
+ .w-sdc-canvas-menu {
+ position: fixed;
+ z-index: 100;
+
+ border-style: solid;
+ border-width: 1px;
+ border-color: #d8d8d8;
+ box-sizing: border-box;
+ background-color: #ffffff;
+ box-shadow: 0px 2px 2px 0px rgba(24, 24, 25, 0.1);
+ width: 91px;
+
+ /* &.vl-type-select{
+ width: 173px;
+ }
+ */
+
+ h3 {
+ color: @func_color_s;
+ font-size: 14px;
+ font-weight: bold;
+ margin: 0;
+ padding: 7px 11px;
+ border-bottom: 1px solid #e5e5e5;
+ }
+
+ .w-sdc-canvas-menu-content {
+ padding: 5px 5px;
+
+ &.vl-select{
+ border-bottom: #d8d8d8 solid 1px;
+ line-height: 15px;
+
+ .tlv-radio {
+ padding: 3px 0px;
+
+ .tlv-radio-label {
+ padding: 3px 0px;
+
+ &::before {
+ margin-right: 10px;
+ }
+ }
+ }
+ }
+
+ .w-sdc-canvas-menu-content-update-button {
+ .sprite;
+ .sprite.e-sdc-small-icon-delete;
+ .hand;
+ position: absolute;
+ top: 15px;
+ right: 10px;
+ }
+ .w-sdc-canvas-menu-content-delete-button {
+ .sprite;
+ .sprite.e-sdc-small-icon-delete;
+ .hand;
+ margin: 0 8px 0 6px;
+ }
+ }
+
+ .w-sdc-canvas-menu-arrow {
+ //TODO: Missing image for small blue triangle.
+ background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAFCAYAAAB4ka1VAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDE0IDc5LjE1Njc5NywgMjAxNC8wOC8yMC0wOTo1MzowMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTQgKFdpbmRvd3MpIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOkE1OTIzNDI1MENFQjExRTU4ODRERTI1MDM2REZCOUYzIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOkE1OTIzNDI2MENFQjExRTU4ODRERTI1MDM2REZCOUYzIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6QTU5MjM0MjMwQ0VCMTFFNTg4NERFMjUwMzZERkI5RjMiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6QTU5MjM0MjQwQ0VCMTFFNTg4NERFMjUwMzZERkI5RjMiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz4gBXTlAAAAOElEQVR42mK0rp7NgASMgZgFiE/CBJjQJPcA8U4gNkdXAJMUAGJ+ZEVMaJIwAFfEhEUSRRFAgAEAVtgJyiLAPWAAAAAASUVORK5CYII=');
+ content: '';
+ display: block;
+ height: 21px;
+ position: absolute;
+ right: 12px;
+ top: -24px;
+ width: 184px;
+ background-repeat: no-repeat;
+ background-position: 175px 16px;
+ }
+
+ }
+}
+/*.right-tab-loader {
+ border: 16px solid #f3f3f3; !* Light grey *!
+ border-top: 16px solid #3498db; !* Blue *!
+ border-radius: 50%;
+ width: 120px;
+ height: 120px;
+ animation: spin 2s linear infinite;
+}*/
+
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/artifacts/artifacts-view-model.ts b/catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/artifacts/artifacts-view-model.ts
new file mode 100644
index 0000000000..0ac5fd0799
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/artifacts/artifacts-view-model.ts
@@ -0,0 +1,301 @@
+'use strict';
+import {
+ ArtifactModel,
+ Service,
+ IAppConfigurtaion,
+ Resource,
+ Component,
+ ComponentInstance,
+ ArtifactGroupModel,
+ IFileDownload
+} from "app/models";
+import {ICompositionViewModelScope} from "../../composition-view-model";
+import {ArtifactsUtils, ModalsHandler, ArtifactGroupType} from "app/utils";
+import {GRAPH_EVENTS} from "app/utils/constants";
+import {EventListenerService} from "app/services/event-listener-service";
+
+export interface IArtifactsViewModelScope extends ICompositionViewModelScope {
+ artifacts:Array<ArtifactModel>;
+ artifactType:string;
+ downloadFile:IFileDownload;
+ isLoading:boolean;
+
+ getTitle():string;
+ addOrUpdate(artifact:ArtifactModel):void;
+ delete(artifact:ArtifactModel):void;
+ download(artifact:ArtifactModel):void;
+ openEditEnvParametersModal(artifact:ArtifactModel):void;
+ getEnvArtifact(heatArtifact:ArtifactModel):any;
+ getEnvArtifactName(artifact:ArtifactModel):string;
+ isLicenseArtifact(artifact:ArtifactModel):boolean;
+ isVFiArtifact(artifact:ArtifactModel):boolean;
+}
+
+export class ResourceArtifactsViewModel {
+
+ static '$inject' = [
+ '$scope',
+ '$filter',
+ '$state',
+ 'sdcConfig',
+ 'ArtifactsUtils',
+ 'ModalsHandler',
+ '$q',
+ 'EventListenerService'
+ ];
+
+ constructor(private $scope:IArtifactsViewModelScope,
+ private $filter:ng.IFilterService,
+ private $state:any,
+ private sdcConfig:IAppConfigurtaion,
+ private artifactsUtils:ArtifactsUtils,
+ private ModalsHandler:ModalsHandler,
+ private $q:ng.IQService,
+ private eventListenerService: EventListenerService) {
+
+ this.initScope();
+ }
+
+
+ private initArtifactArr = (artifactType:string):void => {
+ let artifacts:Array<ArtifactModel> = [];
+
+ if (this.$scope.selectedComponent) {
+ if ('interface' == artifactType) {
+ let interfaces = this.$scope.currentComponent.interfaces;
+ if (interfaces && interfaces.standard && interfaces.standard.operations) {
+
+ angular.forEach(interfaces.standard.operations, (operation:any, interfaceName:string):void => {
+ let item:ArtifactModel = <ArtifactModel>{};
+ if (operation.implementation) {
+ item = <ArtifactModel> operation.implementation;
+ }
+ item.artifactDisplayName = interfaceName;
+ item.artifactLabel = interfaceName;
+ item.mandatory = false;
+ artifacts.push(item);
+ });
+ }
+ } else {
+ //init normal artifacts, deployment or api artifacts
+ let artifactsObj:ArtifactGroupModel;
+ switch (artifactType) {
+ case "api":
+ artifactsObj = (<Service>this.$scope.currentComponent).serviceApiArtifacts;
+ break;
+ case "deployment":
+ if (!this.$scope.isComponentInstanceSelected()) {
+ artifactsObj = this.$scope.currentComponent.deploymentArtifacts;
+ } else {
+ artifactsObj = this.$scope.currentComponent.selectedInstance.deploymentArtifacts;
+ }
+ break;
+ default:
+ //artifactsObj = this.$scope.selectedComponent.artifacts;
+ if (!this.$scope.isComponentInstanceSelected()) {
+ artifactsObj = this.$scope.currentComponent.artifacts;
+ } else {
+ artifactsObj = this.$scope.currentComponent.selectedInstance.artifacts;
+ }
+ break;
+ }
+ _.forEach(artifactsObj, (artifact:ArtifactModel, key) => {
+ artifacts.push(artifact);
+ });
+ }
+ }
+ this.$scope.artifacts = artifacts;
+ };
+
+
+ private convertToArtifactUrl = (artifactType:string):string => {
+
+ switch (artifactType) {
+ case 'deployment':
+ return 'DEPLOYMENT';
+ case 'api':
+ return 'SERVICE_API';
+ default:
+ return 'INFORMATIONAL';
+ }
+
+ }
+
+ private loadComponentArtifactIfNeeded = (forceLoad?: boolean) => {
+
+ let onGetComponentArtifactsSuccess = (artifacts:ArtifactGroupModel)=> {
+ switch (this.$scope.artifactType) {
+ case 'deployment':
+ this.$scope.currentComponent.deploymentArtifacts = artifacts;
+ break;
+ case 'api':
+ (<Service>this.$scope.currentComponent).serviceApiArtifacts = artifacts;
+ break;
+ default:
+ this.$scope.currentComponent.artifacts = artifacts;
+ break;
+ }
+ this.$scope.isLoading = false;
+ this.initArtifactArr(this.$scope.artifactType);
+ }
+
+ let onError = ()=> {
+ this.$scope.isLoading = false;
+ };
+
+ switch (this.$scope.artifactType) {
+ case 'deployment':
+ if(forceLoad || !this.$scope.currentComponent.deploymentArtifacts) {
+ this.$scope.component.getArtifactByGroupType(this.convertToArtifactUrl(this.$scope.artifactType)).then(onGetComponentArtifactsSuccess, onError);
+ } else {
+ this.initArtifactArr(this.$scope.artifactType);
+ }
+
+ break;
+ case 'api':
+ if(!(<Service>this.$scope.currentComponent).serviceApiArtifacts) {
+ this.$scope.component.getArtifactByGroupType(this.convertToArtifactUrl(this.$scope.artifactType)).then(onGetComponentArtifactsSuccess, onError);
+ } else {
+ this.initArtifactArr(this.$scope.artifactType);
+ }
+ break;
+ default:
+ if(!this.$scope.currentComponent.artifacts) {
+ this.$scope.component.getArtifactByGroupType(this.convertToArtifactUrl(this.$scope.artifactType)).then(onGetComponentArtifactsSuccess, onError);
+ } else {
+ this.initArtifactArr(this.$scope.artifactType);
+ }
+ break;
+ }
+ }
+ private loadArtifacts = (forceLoad?: boolean):void => {
+
+ let onGetInstanceArtifactsSuccess = (artifacts:ArtifactGroupModel)=> {
+ switch (this.$scope.artifactType) {
+ case 'deployment':
+ this.$scope.currentComponent.selectedInstance.deploymentArtifacts = artifacts;
+ break;
+ default:
+ this.$scope.currentComponent.selectedInstance.artifacts = artifacts;
+ break;
+ }
+ this.loadComponentArtifactIfNeeded();
+ };
+
+ let onError = ()=> {
+ this.$scope.isLoading = false;
+ };
+
+ this.$scope.isLoading = true;
+ if (this.$scope.isComponentInstanceSelected()) {
+ this.$scope.component.getComponentInstanceArtifactsByGroupType(this.$scope.component.selectedInstance.uniqueId, this.convertToArtifactUrl(this.$scope.artifactType)).then(onGetInstanceArtifactsSuccess, onError);
+ } else {
+ this.loadComponentArtifactIfNeeded(forceLoad);
+ }
+ }
+
+ private updateArtifactsIfNeeded = ():void => {
+ if (this.$scope.artifactType === "deployment") {
+ this.loadArtifacts(true);
+ } else {
+ this.initArtifactArr(this.$scope.artifactType);
+ }
+ };
+
+ private openEditArtifactModal = (artifact:ArtifactModel):void => {
+ this.ModalsHandler.openArtifactModal(artifact, this.$scope.currentComponent).then(():void => {
+ this.updateArtifactsIfNeeded();
+ });
+ };
+
+ private initScope = ():void => {
+
+ this.$scope.isLoading = false;
+ this.$scope.artifactType = this.artifactsUtils.getArtifactTypeByState(this.$state.current.name);
+ this.loadArtifacts();
+ this.$scope.getTitle = ():string => {
+ return this.artifactsUtils.getTitle(this.$scope.artifactType, this.$scope.currentComponent);
+ };
+
+ this.$scope.isVFiArtifact = (artifact:ArtifactModel):boolean=> {
+ if (artifact.artifactGroupType === ArtifactGroupType.INFORMATION) {//fix DE256847
+ return this.$scope.currentComponent.artifacts && (!this.$scope.currentComponent.artifacts[artifact.artifactLabel] || !this.$scope.currentComponent.artifacts[artifact.artifactLabel].artifactName);
+ }
+ return this.$scope.currentComponent.deploymentArtifacts && (!this.$scope.currentComponent.deploymentArtifacts[artifact.artifactLabel]);//fix DE251314
+ };
+
+ this.$scope.addOrUpdate = (artifact:ArtifactModel):void => {
+ this.artifactsUtils.setArtifactType(artifact, this.$scope.artifactType);
+ let artifactCopy = new ArtifactModel(artifact);
+ this.openEditArtifactModal(artifactCopy);
+ };
+
+
+ this.$scope.delete = (artifact:ArtifactModel):void => {
+
+ let onOk = ():void => {
+ this.$scope.isLoading = true;
+ this.artifactsUtils.removeArtifact(artifact, this.$scope.artifacts);
+
+ let success = (responseArtifact:ArtifactModel):void => {
+ this.initArtifactArr(this.$scope.artifactType);
+ this.$scope.isLoading = false;
+ };
+
+ let error = (error:any):void => {
+ console.log('Delete artifact returned error:', error);
+ this.initArtifactArr(this.$scope.artifactType);
+ this.$scope.isLoading = false;
+ };
+ if (this.$scope.isComponentInstanceSelected()) {
+ this.$scope.currentComponent.deleteInstanceArtifact(artifact.uniqueId, artifact.artifactLabel).then(success, error);
+ } else {
+ this.$scope.currentComponent.deleteArtifact(artifact.uniqueId, artifact.artifactLabel).then(success, error);//TODO simulate error (make sure error returns)
+ }
+ };
+ let title:string = this.$filter('translate')("ARTIFACT_VIEW_DELETE_MODAL_TITLE");
+ let message:string = this.$filter('translate')("ARTIFACT_VIEW_DELETE_MODAL_TEXT", "{'name': '" + artifact.artifactDisplayName + "'}");
+ this.ModalsHandler.openConfirmationModal(title, message, false).then(onOk);
+ };
+
+
+ this.$scope.getEnvArtifact = (heatArtifact:ArtifactModel):any=> {
+ return _.find(this.$scope.artifacts, (item:ArtifactModel)=> {
+ return item.generatedFromId === heatArtifact.uniqueId;
+ });
+ };
+
+ this.$scope.getEnvArtifactName = (artifact:ArtifactModel):string => {
+ let envArtifact = this.$scope.getEnvArtifact(artifact);
+ if (envArtifact) {
+ return envArtifact.artifactDisplayName;
+ }
+ };
+
+ this.$scope.isLicenseArtifact = (artifact:ArtifactModel):boolean => {
+ let isLicense:boolean = false;
+ if (this.$scope.component.isResource() && (<Resource>this.$scope.component).isCsarComponent()) {
+ isLicense = this.artifactsUtils.isLicenseType(artifact.artifactType);
+ }
+
+ return isLicense;
+ };
+
+ this.$scope.openEditEnvParametersModal = (artifact:ArtifactModel):void => {
+ this.ModalsHandler.openEditEnvParametersModal(artifact, this.$scope.currentComponent).then(()=> {
+ this.updateArtifactsIfNeeded();
+ }, ()=> {
+ // ERROR
+ });
+ };
+
+ this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_NODE_SELECTED, this.loadArtifacts);
+ this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_GRAPH_BACKGROUND_CLICKED, this.loadArtifacts);
+
+ this.$scope.$on('$destroy', () => {
+
+ this.eventListenerService.unRegisterObserver(GRAPH_EVENTS.ON_NODE_SELECTED, this.loadArtifacts);
+ this.eventListenerService.unRegisterObserver(GRAPH_EVENTS.ON_GRAPH_BACKGROUND_CLICKED, this.loadArtifacts);
+ });
+ }
+}
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/artifacts/artifacts-view.html b/catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/artifacts/artifacts-view.html
new file mode 100644
index 0000000000..b0d81b3437
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/artifacts/artifacts-view.html
@@ -0,0 +1,67 @@
+<perfect-scrollbar class="w-sdc-designer-sidebar-tab-content artifacts">
+ <div class="w-sdc-designer-sidebar-section">
+ <expand-collapse
+ expanded-selector=".w-sdc-designer-sidebar-section-content" class="w-sdc-designer-sidebar-section-title">
+ <span class="w-sdc-designer-sidebar-section-title-text" data-ng-bind="getTitle()" tooltips tooltip-content="{{getTitle()}}"></span>
+ <div class="w-sdc-designer-sidebar-section-title-icon"></div>
+ </expand-collapse>
+
+ <div class="w-sdc-designer-sidebar-section-content">
+ <div class="i-sdc-designer-sidebar-section-content-item">
+ <div class="i-sdc-designer-sidebar-section-content-item-artifact"
+ data-ng-repeat="artifact in artifacts | orderBy: ['-mandatory', 'artifactDisplayName'] track by $index"
+ data-ng-if="(!isComponentInstanceSelected() || isVFiArtifact(artifact)|| artifact.esId) && 'HEAT_ENV' !== artifact.artifactType"
+ data-tests-id="artifact-item-{{artifact.artifactDisplayName}}">
+ <span data-ng-if="artifact.heatParameters.length" class="i-sdc-designer-sidebar-section-content-item-file-link"></span>
+ <div class="i-sdc-designer-sidebar-section-content-item-artifact-details" data-ng-class="{'heat':artifact.isHEAT() && artifact.heatParameters.length}">
+ <div class="i-sdc-designer-sidebar-section-content-item-artifact-filename" data-tests-id="artifactName-{{artifact.artifactDisplayName}}"
+ data-ng-bind="artifact.artifactName" tooltips tooltip-content="{{artifact.artifactName}}"
+ data-ng-if="artifact.artifactName"></div>
+ <div>
+ <span class="i-sdc-designer-sidebar-section-content-item-artifact-details-name" data-tests-id="artifact_Display_Name-{{artifact.artifactDisplayName}}"
+ data-ng-class="{'hand enabled': (isVFiArtifact(artifact)) && !isViewMode() && !artifact.isHEAT() && !artifact.isThirdParty() && !isLicenseArtifact(artifact)}"
+ data-ng-bind="artifact.artifactDisplayName" data-ng-click="!isViewMode() && !isLoading && (!isComponentInstanceSelected()||isVFiArtifact(artifact)) && !artifact.isHEAT() && !artifact.isThirdParty() && !isLicenseArtifact(artifact) && addOrUpdate(artifact)"
+ tooltips tooltip-content="{{artifact.artifactDisplayName}}"></span>
+ <div class="i-sdc-designer-sidebar-section-content-item-artifact-heat-env" ng-if="artifact.heatParameters.length">
+ <span data-ng-bind="getEnvArtifactName(artifact)"data-tests-id="heat_env_{{artifact.artifactDisplayName}}"></span>
+ <button class="i-sdc-designer-sidebar-section-content-item-button update-env sprite e-sdc-small-icon-pencil" data-tests-id="edit_{{artifact.artifactDisplayName}}"
+ data-ng-if="!isViewMode()" data-ng-click="addOrUpdate(getEnvArtifact(artifact))"></button>
+ <download-artifact class="i-sdc-designer-sidebar-section-content-item-button download-env sprite e-sdc-small-download hand" artifact="getEnvArtifact(artifact)"
+ component="currentComponent" instance="isComponentInstanceSelected()"
+ data-tests-id="download_env_{{artifact.artifactDisplayName}}"></download-artifact>
+ </div>
+ </div>
+
+ <div class="i-sdc-designer-sidebar-section-content-item-artifact-details-desc">
+ <span class="i-sdc-designer-sidebar-section-content-item-artifact-details-desc-label" data-ng-show="artifact.description">Description:</span>{{artifact.description}}
+ </div>
+ </div>
+ <button ng-if="!isViewMode() && artifact.esId && (!isComponentInstanceSelected()||isVFiArtifact(artifact)) && !artifact.isHEAT() && !artifact.isThirdParty() && !isLicenseArtifact(artifact)" class="i-sdc-designer-sidebar-section-content-item-button delete sprite e-sdc-small-icon-delete"
+ data-tests-id="delete_{{artifact.artifactDisplayName}}" data-ng-click="delete(artifact)" type="button"></button>
+ <button ng-if="!isViewMode() && artifact.isHEAT() && isComponentInstanceSelected() && artifact.heatParameters.length"
+ class="i-sdc-designer-sidebar-section-content-item-button attach sprite e-sdc-small-icon-pad"
+ data-ng-click="openEditEnvParametersModal(getEnvArtifact(artifact))" type="button"
+ data-tests-id="edit-parameters-of-{{artifact.artifactDisplayName}}"></button>
+ <!--need to remove this button -->
+ <button ng-if="!isViewMode() && artifact.isHEAT() && !isComponentInstanceSelected() && artifact.heatParameters.length"
+ class="i-sdc-designer-sidebar-section-content-item-button attach sprite e-sdc-small-icon-pad"
+ data-ng-click="openEditEnvParametersModal(artifact)" type="button"
+ data-tests-id="edit-parameters-of-{{artifact.artifactDisplayName}}"></button>
+
+ <download-artifact ng-if="artifact.esId && 'deployment' != artifactType" class="i-sdc-designer-sidebar-section-content-item-button download sprite e-sdc-small-download hand"
+ artifact="artifact" component="currentComponent" data-tests-id="download-{{artifact.artifactDisplayName}}" instance="isComponentInstanceSelected()"></download-artifact>
+ <download-artifact ng-if="artifact.esId && 'deployment' == artifactType" class="i-sdc-designer-sidebar-section-content-item-button download sprite e-sdc-small-download hand"
+ artifact="artifact" component="currentComponent" instance="isComponentInstanceSelected()" data-tests-id="download_{{artifact.artifactDisplayName}}"
+ show-loader="artifact.isHEAT()"
+ download-icon-class="i-sdc-designer-sidebar-section-content-item-button download sprite e-sdc-small-download hand"></download-artifact>
+ <button ng-if="!isViewMode() && !artifact.esId && artifactType==='deployment' && !isComponentInstanceSelected() && !artifact.isThirdParty()" class="i-sdc-designer-sidebar-section-content-item-button attach sprite e-sdc-small-icon-upload"
+ data-ng-click="addOrUpdate(artifact)" type="button" data-tests-id="add_Artifact"></button>
+ </div>
+ </div>
+
+ </div>
+ <div class="w-sdc-designer-sidebar-section-footer" data-ng-if="!isViewMode() && artifactType!=='api' && (!isComponentInstanceSelected()||selectedComponent.resourceType=='VF') && !currentComponent.isProduct() && ('deployment' != artifactType || selectedComponent.isComplex())">
+ <button class="w-sdc-designer-sidebar-section-footer-action tlv-btn blue" data-tests-id="add_Artifact_Button" data-ng-click="addOrUpdate({})" type="button">Add Artifact</button>
+ </div>
+ </div>
+</perfect-scrollbar>
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/artifacts/artifacts.less b/catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/artifacts/artifacts.less
new file mode 100644
index 0000000000..7c8b8315d9
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/artifacts/artifacts.less
@@ -0,0 +1,172 @@
+.w-sdc-designer-sidebar-tab-content.artifacts {
+
+ .i-sdc-designer-sidebar-section-content-item-artifact.hand {
+ .hand;
+ }
+
+ .w-sdc-designer-sidebar-section-content {
+ padding: 0;
+ }
+ .w-sdc-designer-sidebar-section-title {
+ &.expanded {
+ margin-bottom: 0;
+ }
+ }
+
+ .i-sdc-designer-sidebar-section-content-item-artifact-details {
+ display: inline-block;
+ margin-left: 5px;
+ vertical-align: middle;
+ width: 180px;
+ &.heat {
+ line-height: 18px;
+ width: 250px;
+ }
+ }
+
+ .i-sdc-designer-sidebar-section-content-item-artifact-details-name {
+ .g_7;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ max-width:220px;
+ display: inline-block;
+ //text-transform: capitalize;
+ &.enabled {
+ &:hover {
+ .a_7;
+ }
+ }
+
+ }
+
+ .i-sdc-designer-sidebar-section-content-item-artifact-heat-env {
+ .g_7;
+ margin-top: 6px;
+ line-height: 42px;
+ padding-top: 10px;
+ border-top:1px solid #c8cdd1;
+ .enabled {
+ &:hover {
+ .hand;
+ .a_7;
+ }
+ }
+ }
+
+ .i-sdc-designer-sidebar-section-content-item-artifact-filename {
+ .g_7;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ max-width: 225px;
+ display: inline-block;
+ .bold;
+ &.enabled {
+ &:hover {
+ .a_7;
+ }
+ }
+ }
+
+
+ .i-sdc-designer-sidebar-section-content-item-file-link{
+ border-left: 1px #848586 solid;
+ height: 58px;
+ margin-left: -11px;
+ margin-top: 11px;
+ border-top: 1px #848586 solid;
+ border-bottom: 1px #848586 solid;
+ width: 12px;
+ float: left;
+ }
+
+ .i-sdc-designer-sidebar-section-content-item-artifact-details-desc {
+ display: none;
+ line-height: 16px;
+ word-wrap: break-word;
+ white-space: normal;
+ }
+
+ .i-sdc-designer-sidebar-section-content-item-artifact-details-desc-label {
+ .b_3;
+ }
+
+
+ .i-sdc-designer-sidebar-section-content-item-artifact {
+ border-bottom: 1px solid #c8cdd1;
+ padding: 5px 10px 5px 18px;
+ position: relative;
+ // line-height: 36px;
+ min-height: 61px;
+ //cursor: default;
+ display: flex;
+ align-items: center;
+
+
+ .i-sdc-designer-sidebar-section-content-item-button {
+ top: 20px;
+ line-height: 10px;
+ }
+
+ &:hover {
+ //background-color: @color_c;
+ .bg_c;
+ transition: all .3s;
+
+ .i-sdc-designer-sidebar-section-content-item-button {
+ display: block;
+
+ }
+
+ }
+ }
+
+}
+
+///////////////////Lifecycle Management
+.i-sdc-designer-sidebar-section-content-item-lm {
+ .b_7;
+ border-bottom: 1px solid @color_e;
+ cursor: pointer;
+ height: 65px;
+ padding: 22px 0;
+ position: relative;
+
+ &:hover {
+ .bg_c_hover;
+ margin-left: -10px;
+ margin-right: -10px;
+ padding: 22px 10px;
+
+ .i-sdc-designer-sidebar-section-content-item-lm-icon {
+ right: 16px;
+ }
+ }
+}
+
+.i-sdc-designer-sidebar-section-content-item-lm:first-child {
+ margin-top: -18px;
+}
+
+.i-sdc-designer-sidebar-section-content-item-lm-icon {
+ position: absolute;
+ right: 6px;
+
+ //TODO: Replace the icons.
+ &.icon-view {
+ background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAJCAYAAAACTR1pAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDE0IDc5LjE1Njc5NywgMjAxNC8wOC8yMC0wOTo1MzowMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTQgKFdpbmRvd3MpIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjdGMDNBRUJDMDkxNjExRTVCMjRBOEI5QzMxQTlBQjY4IiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjdGMDNBRUJEMDkxNjExRTVCMjRBOEI5QzMxQTlBQjY4Ij4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6N0YwM0FFQkEwOTE2MTFFNUIyNEE4QjlDMzFBOUFCNjgiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6N0YwM0FFQkIwOTE2MTFFNUIyNEE4QjlDMzFBOUFCNjgiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz4U2decAAABRUlEQVR42pyRPUvDUBSG39t2UyyaBgvWQZMqKoFq4lBH0U0UqXSxKPaPieCoWPAPKIgdjG1ohaghOrSR1iaLKMXpmnMhQVfPcLmc8zxwPhjnHFE4rx3eMO/w4rr4+vwQuZHRMcwqClaMVeRnplnEskg8O7/g96YJSZLgBz5+R0bKIAgC6IaBvdKukBP0HJ+cctu2cVCtQsrIUFUVlcp+LE6EItWIIVaIV9c3/PnRRqlcxkJeYV2vi+/hEO/9QSx6b56oEUMsOal6/TacQcXinCpaGE+n0QnljufFIuUoiAlZTk6iWFyD6zpoWi3RwvrmBlLJZCzRn3IUxBBLjlgO9e06T9ja3sGyrqPX66NtWQLWCgVks5Owmg1c1mpQ8vM4OqywP1s1w1PkpnJY0jTIsizy/sDHQ7sFmt0ITxJtlf33jj8CDADhB52tEX6ifAAAAABJRU5ErkJggg==');
+ height: 9px;
+ top: 29px;
+ width: 14px;
+ }
+
+ //TODO: Replace the icons.
+ &.icon-alert {
+ background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAANCAYAAAB2HjRBAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDE0IDc5LjE1Njc5NywgMjAxNC8wOC8yMC0wOTo1MzowMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTQgKFdpbmRvd3MpIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjhBM0YxQTBCMDkyMDExRTVBNzlCQUYxNEYwMDUwOTQ5IiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjhBM0YxQTBDMDkyMDExRTVBNzlCQUYxNEYwMDUwOTQ5Ij4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6OEEzRjFBMDkwOTIwMTFFNUE3OUJBRjE0RjAwNTA5NDkiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6OEEzRjFBMEEwOTIwMTFFNUE3OUJBRjE0RjAwNTA5NDkiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz7exgceAAAB5klEQVR42oySy2tTQRTGfzN3cpvbpFTaUlzUjeCuVFyJf4DuCl34wL/CrbpUunLnwpUuFFy48IFiwYWhpYuW+gCLrbWKBQumaRIba5ub3MccJ5WECCV44MDMme873/lmBhGhnZulcn/3/sfrO7J/45R017pT0xUS2bC93vy2LAOLt5DfCeUn14RD4h/ysbHRDii/8pyo7DmFiP7VZ4Rbu9KT3I7i+qJklp7ibQt2J6HeNOhXV/gv8tDcbaLtXYLJKeTcZfxqSPPzvGu6ID3J1Q8z0ny/hIkM5swF8qenUFUfwixHCtd7Kwez04jOgEmc+hq6tuZ8N9E6xG5U+Pn2kRxKLs7eE7tRc0XlToRM2sD+2sGmDiY+kWsSFG528Kr1Xgfjfn0n5uElvFrrzVqVDHqkiTKjxCsV9LBx08QkgcY/eZH+89Oqo+x9eumkwQaQaoPEKWpsHHN8ArItgLt5UdiG6736mFLxixyQt+bviik8QPuaNFRIkjqQRZ2YJDo6QWydjTRtOcGPPdJKgnlxFfN9fVmGNhecR4U3mLrL6nPTRTQHXZO5+4jyyI4Iqs99GJvgBiKjcySlj3897y6/kbreI4w0QS7ASxrsa4/cXkxkIzK5PKmNqRMw7NfdeczA+Fn1R4ABAPnMAeCjkgf5AAAAAElFTkSuQmCC');
+ height: 13px;
+ top: 27px;
+ width: 15px;
+ }
+
+}
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/details/details-view-model.ts b/catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/details/details-view-model.ts
new file mode 100644
index 0000000000..a81bb9176e
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/details/details-view-model.ts
@@ -0,0 +1,132 @@
+/*-
+ * ============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=========================================================
+ */
+
+'use strict';
+import {Component} from "app/models";
+import {GRAPH_EVENTS} from "app/utils";
+import {LeftPaletteLoaderService, EventListenerService} from "app/services";
+import {ICompositionViewModelScope} from "../../composition-view-model";
+import {LeftPaletteComponent} from "../../../../../../models/components/displayComponent";
+
+export interface IEditResourceVersion {
+ allVersions:any;
+ changeVersion:string;
+}
+
+interface IDetailsViewModelScope extends ICompositionViewModelScope {
+ isLoading:boolean;
+ $parent:ICompositionViewModelScope;
+ expandedSection:Array<string>;
+ editForm:ng.IFormController;
+ editResourceVersion:IEditResourceVersion;
+
+ changeResourceVersion():void;
+}
+
+export class DetailsViewModel {
+
+ static '$inject' = [
+ '$scope',
+ 'LeftPaletteLoaderService',
+ 'EventListenerService'
+
+ ];
+
+ constructor(private $scope:IDetailsViewModelScope,
+ private LeftPaletteLoaderService:LeftPaletteLoaderService,
+ private eventListenerService:EventListenerService) {
+ this.initScope();
+ }
+
+ private clearSelectedVersion = ():void => {
+ this.$scope.editResourceVersion = {
+ allVersions: {},
+ changeVersion: null
+ };
+ };
+
+ private versioning:Function = (versionNumber:string):string => {
+ let version:Array<string> = versionNumber.split('.');
+ return '00000000'.slice(version[0].length) + version[0] + '.' + '00000000'.slice(version[1].length) + version[1];
+ };
+
+ private initEditResourceVersion = ():void => {
+ this.clearSelectedVersion();
+ this.$scope.editResourceVersion.allVersions[this.$scope.currentComponent.selectedInstance.componentVersion] = this.$scope.currentComponent.selectedInstance.componentUid;
+ _.merge(this.$scope.editResourceVersion.allVersions, angular.copy(this.$scope.selectedComponent.allVersions));
+ let sorted:any = _.sortBy(_.toPairs(this.$scope.editResourceVersion.allVersions), (item)=> {
+ return this.versioning(item[0]);
+ });
+ this.clearSelectedVersion();
+ _.forEach(sorted, (item)=> {
+ this.$scope.editResourceVersion.allVersions[item[0]] = item[1];
+ });
+
+ let highestVersion = _.last(Object.keys(this.$scope.selectedComponent.allVersions));
+
+ if (parseFloat(highestVersion) % 1) { //if highest is minor, make sure it is the latest checked in -
+ let latestVersionComponent:LeftPaletteComponent = _.find(this.LeftPaletteLoaderService.getLeftPanelComponentsForDisplay(this.$scope.currentComponent.componentType), (component:LeftPaletteComponent) => { //latest checked in
+ return (component.systemName === this.$scope.selectedComponent.systemName
+ || component.uuid === this.$scope.selectedComponent.uuid);
+ });
+ let latestVersion:string = latestVersionComponent ? latestVersionComponent.version : highestVersion;
+
+ if (highestVersion != latestVersion) { //highest is checked out - remove from options
+ this.$scope.editResourceVersion.allVersions = _.omit(this.$scope.editResourceVersion.allVersions, highestVersion);
+ }
+ }
+ this.$scope.editResourceVersion.changeVersion = this.$scope.currentComponent.selectedInstance.componentVersion;
+ };
+
+ private initScope = ():void => {
+ this.$scope.isLoading = false;
+ this.$scope.$parent.isLoading = false;
+ this.$scope.expandedSection = ['general', 'tags'];
+ //this.clearSelectedVersion();
+
+ this.$scope.$watch('selectedComponent', (component:Component) => {
+ if (this.$scope.isComponentInstanceSelected()) {
+ this.initEditResourceVersion();
+ }
+ });
+
+ this.$scope.changeResourceVersion = ():void => {
+ this.$scope.isLoading = true;
+ this.$scope.$parent.isLoading = true;
+
+ let onSuccess = (component:Component)=> {
+ this.$scope.isLoading = false;
+ this.$scope.$parent.isLoading = false;
+ this.$scope.onComponentInstanceVersionChange(component);
+
+ this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_VERSION_CHANGED, this.$scope.currentComponent);
+ };
+
+ let onFailed = (error:any)=> {
+ this.$scope.isLoading = false;
+ this.$scope.$parent.isLoading = false;
+ console.log(error);
+ };
+
+ let componentUid:string = this.$scope.editResourceVersion.allVersions[this.$scope.editResourceVersion.changeVersion];
+ this.$scope.currentComponent.changeComponentInstanceVersion(componentUid).then(onSuccess, onFailed);
+ };
+ }
+}
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/details/details-view.html b/catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/details/details-view.html
new file mode 100644
index 0000000000..70dc58075a
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/details/details-view.html
@@ -0,0 +1,136 @@
+<perfect-scrollbar include-padding="true" class="w-sdc-designer-sidebar-tab-content details">
+
+ <div class="w-sdc-designer-sidebar-section">
+ <loader data-display="isLoading"></loader>
+ <expand-collapse expanded-selector=".w-sdc-designer-sidebar-section-content.general" class="w-sdc-designer-sidebar-section-title">
+
+ General Info
+ <div class="w-sdc-designer-sidebar-section-title-icon"></div>
+ </expand-collapse>
+
+ <div class="w-sdc-designer-sidebar-section-content general">
+ <div class="i-sdc-designer-sidebar-section-content-item">
+ <span class="i-sdc-designer-sidebar-section-content-item-label">Type:</span>
+ <span class="i-sdc-designer-sidebar-section-content-item-value" data-tests-id="rightTab_componentType" data-ng-bind="selectedComponent.componentType"></span>
+ </div>
+ <div class="i-sdc-designer-sidebar-section-content-item" data-ng-if="selectedComponent.isResource()">
+ <span class="i-sdc-designer-sidebar-section-content-item-label">Resource Type:</span>
+ <span class="i-sdc-designer-sidebar-section-content-item-value" data-ng-if="selectedComponent.isResource()" data-ng-bind="selectedComponent.resourceType"
+ tooltips tooltip-content="&#8203;{{selectedComponent.resourceType | resourceTypeName}}"
+ data-tests-id="rightTab_resourceType"></span>
+ </div>
+ <div class="i-sdc-designer-sidebar-section-content-item">
+
+ <span class="i-sdc-designer-sidebar-section-content-item-label">Version:</span>
+ <span class="i-sdc-designer-sidebar-section-content-item-value"
+ data-ng-if="!isComponentInstanceSelected() || selectedComponent.isVl()" data-tests-id="rightTab_version" data-ng-bind="selectedComponent.version"></span>
+
+ <ng-form name="editForm" data-ng-if="isComponentInstanceSelected() && !selectedComponent.isVl()">
+ <select data-ng-model="editResourceVersion.changeVersion" name="changeVersion" data-tests-id="changeVersion" data-ng-disabled="$parent.isViewOnly"
+ class="i-sdc-designer-sidebar-section-content-item-value i-sdc-form-select"
+ data-ng-class="{'minor': (editResourceVersion.changeVersion)%1}"
+ data-ng-change="changeResourceVersion()">
+ <option class="select-instance-version" data-ng-class="{'minor': key%1}"
+ ng-repeat="(key, value) in editResourceVersion.allVersions">{{key}}</option>
+ </select></ng-form>
+ </div>
+ <div class="i-sdc-designer-sidebar-section-content-item" data-ng-if="selectedComponent.categories && selectedComponent.categories[0]">
+ <span class="i-sdc-designer-sidebar-section-content-item-label">Category:</span>
+ <span class="i-sdc-designer-sidebar-section-content-item-value" data-ng-bind="selectedComponent.categories[0].name"
+ tooltips tooltip-content="&#8203;{{selectedComponent.categories[0].name}}"
+ data-tests-id="rightTab_category"></span>
+ </div>
+ <div class="i-sdc-designer-sidebar-section-content-item" data-ng-if="selectedComponent.categories && selectedComponent.categories[0] && selectedComponent.categories[0].subcategories">
+ <span class="i-sdc-designer-sidebar-section-content-item-label">Sub Category:</span>
+ <span class="i-sdc-designer-sidebar-section-content-item-value" data-ng-bind="selectedComponent.categories[0].subcategories[0].name"
+ tooltips tooltip-content="&#8203;{{selectedComponent.categories[0].subcategories[0].name}}"
+ data-tests-id="rightTab_subCategory"></span>
+ </div>
+ <div class="i-sdc-designer-sidebar-section-content-item">
+ <span class="i-sdc-designer-sidebar-section-content-item-label">Creation Date:</span>
+ <span class="i-sdc-designer-sidebar-section-content-item-value" data-ng-bind="selectedComponent.creationDate | date: 'MM/dd/yyyy'"
+ data-tests-id="rightTab_creationDate"></span>
+ </div>
+ <div class="i-sdc-designer-sidebar-section-content-item">
+ <span class="i-sdc-designer-sidebar-section-content-item-label">Author:</span>
+ <span class="i-sdc-designer-sidebar-section-content-item-value" data-ng-bind="selectedComponent.creatorFullName"
+ tooltips tooltip-content="&#8203;{{selectedComponent.creatorFullName}}"
+ data-tests-id="rightTab_author">
+ </span>
+ </div>
+ <div class="i-sdc-designer-sidebar-section-content-item" data-ng-if="selectedComponent.isService()">
+ <span class="i-sdc-designer-sidebar-section-content-item-label" translate="GENERAL_LABEL_PROJECT_CODE"></span>
+ <span class="i-sdc-designer-sidebar-section-content-item-value"
+ data-tests-id="rightTab_projectCode" data-ng-bind="selectedComponent.projectCode"></span>
+ </div>
+ <div class="i-sdc-designer-sidebar-section-content-item" data-ng-if="selectedComponent.isResource()">
+ <span class="i-sdc-designer-sidebar-section-content-item-label">Vendor Name:</span>
+ <span class="i-sdc-designer-sidebar-section-content-item-value" data-ng-bind="selectedComponent.vendorName"
+ tooltips tooltip-content="&#8203;{{selectedComponent.vendorName}}"
+ data-tests-id="rightTab_vendorName">
+ </span>
+ </div>
+ <div class="i-sdc-designer-sidebar-section-content-item" data-ng-if="selectedComponent.isResource()">
+ <span class="i-sdc-designer-sidebar-section-content-item-label">Vendor Release:</span>
+ <span class="i-sdc-designer-sidebar-section-content-item-value" data-ng-bind="selectedComponent.vendorRelease"
+ tooltips tooltip-class="tooltip-custom break-word-tooltip" tooltip-content="&#8203;{{selectedComponent.vendorRelease}}"
+ data-tests-id="rightTab_vendorRelease">
+ </span>
+ </div>
+ <div class="i-sdc-designer-sidebar-section-content-item">
+ <span class="i-sdc-designer-sidebar-section-content-item-label" translate="GENERAL_LABEL_CONTACT_ID"></span>
+ <span class="i-sdc-designer-sidebar-section-content-item-value" data-ng-bind="selectedComponent.contactId"
+ data-tests-id="rightTab_contactId"></span>
+ </div>
+ <div class="i-sdc-designer-sidebar-section-content-item" data-ng-if="isViewMode() && currentComponent.isService() && selectedComponent.isResource()">
+ <span class="i-sdc-designer-sidebar-section-content-item-label">Resource Customization UUID:</span><br>
+ <span class="customization-uuid selectable" data-ng-bind="currentComponent.selectedInstance.customizationUUID"
+ data-tests-id="rightTab_customizationModuleUUID"></span><br>
+ </div>
+ <div class="i-sdc-designer-sidebar-section-content-item description">
+ <span class="i-sdc-designer-sidebar-section-content-item-label">Description:
+
+ <span class="i-sdc-designer-sidebar-section-content-description-item-value" ellipsis="selectedComponent.description" max-chars="55"
+ data-tests-id="rightTab_description"></span>
+ </span>
+ </div>
+
+ </div>
+ </div>
+
+ <div class="w-sdc-designer-sidebar-section additionalInformation">
+ <expand-collapse expanded-selector=".w-sdc-designer-sidebar-section-content.additionalInformation" class="w-sdc-designer-sidebar-section-title">
+ Additional Information
+ <div class="w-sdc-designer-sidebar-section-title-icon"></div>
+ </expand-collapse>
+
+ <div class="w-sdc-designer-sidebar-section-content additionalInformation">
+ <div class="i-sdc-designer-sidebar-section-content-item" data-ng-repeat="additionalInformation in selectedComponent.getAdditionalInformation() track by $index">
+ <span class="i-sdc-designer-sidebar-section-content-item-label additional-information" data-ng-bind="additionalInformation.key" tooltips tooltip-content="{{additionalInformation.key}}"></span>
+ <span class="i-sdc-designer-sidebar-section-content-item-label">:</span>
+ <span class="i-sdc-designer-sidebar-section-content-item-value additional-information" data-ng-bind="additionalInformation.value"
+ tooltips tooltip-class="tooltip-custom break-word-tooltip" tooltip-content="{{additionalInformation.value}}"></span>
+ </div>
+ </div>
+ </div>
+
+
+ <div class="w-sdc-designer-sidebar-section tags">
+ <expand-collapse expanded-selector=".w-sdc-designer-sidebar-section-content.tags" class="w-sdc-designer-sidebar-section-title">
+ Tags
+ <div class="w-sdc-designer-sidebar-section-title-icon"></div>
+ </expand-collapse>
+
+ <div class="w-sdc-designer-sidebar-section-content tags">
+ <div class="i-sdc-designer-sidebar-section-content-item">
+ <span class="i-sdc-designer-sidebar-section-content-item-tag" data-ng-if="selectedComponent.tags.indexOf(selectedComponent.name)===-1" data-ng-bind="selectedComponent.name"
+ data-tests-id="rightTab_tag" tooltips tooltip-content="{{selectedComponent.name}}"></span>
+ <span class="i-sdc-designer-sidebar-section-content-item-tag" data-ng-repeat="tag in selectedComponent.tags track by $index" data-ng-bind="tag"
+ data-tests-id="rightTab_tag" tooltips tooltip-content="{{tag}}"></span>
+ </div>
+ </div>
+ </div>
+ </div>
+
+</perfect-scrollbar>
+
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/details/details.less b/catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/details/details.less
new file mode 100644
index 0000000000..841ab3aa49
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/details/details.less
@@ -0,0 +1,72 @@
+.w-sdc-designer-sidebar-tab-content.details {
+
+ .w-sdc-designer-sidebar-section-title + .w-sdc-designer-sidebar-section-content {
+ padding: 0 10px 0 18px;
+ }
+
+ .w-sdc-designer-sidebar-section-title.expanded + .w-sdc-designer-sidebar-section-content {
+ padding: 10px 10px 10px 18px;
+ }
+
+ .i-sdc-designer-sidebar-section-content-item-label {
+ font-weight: bold;
+ &.additional-information{
+ max-width:100px;
+ display: inline-block;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ vertical-align: bottom;
+ }
+
+ }
+
+
+
+ .i-sdc-designer-sidebar-section-content-item-value {
+ // .hyphenate;
+ padding-left: 10px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ display: inline-block;
+ max-width: 160px;
+ vertical-align:bottom;
+ font-weight: normal;
+ &.additional-information{
+ max-width:160px;
+ display: inline-block;
+ }
+ &.i-sdc-form-select {
+ .b_1;
+ border: 1px solid @border_color_f;
+ width: 210px;
+ max-width: 210px;
+ padding-left: 4px;
+
+ .select-instance-version {
+ .b_1;
+ &.minor {
+ .h_1;
+ }
+ }
+ }
+ &.minor {
+ .h_1;
+ }
+ }
+ .i-sdc-designer-sidebar-section-content-description-item-value{
+ max-width: none;
+ font-weight: normal;
+ }
+
+ .customization-uuid{
+ .f-type._12_m;
+ }
+
+ .w-sdc-designer-sidebar-section.tags {
+ .i-sdc-designer-sidebar-section-content-item {
+ white-space: normal;
+ }
+ }
+
+}
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/properties-and-attributes/properties-view-model.ts b/catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/properties-and-attributes/properties-view-model.ts
new file mode 100644
index 0000000000..84769d7a62
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/properties-and-attributes/properties-view-model.ts
@@ -0,0 +1,217 @@
+'use strict';
+import {
+ AttributeModel,
+ AttributesGroup,
+ Component,
+ ComponentInstance,
+ PropertyModel,
+ PropertiesGroup
+} from "app/models";
+import {ICompositionViewModelScope} from "../../composition-view-model";
+import {ModalsHandler} from "app/utils";
+import {ComponentServiceNg2} from "app/ng2/services/component-services/component.service";
+import {ComponentGenericResponse} from "app/ng2/services/responses/component-generic-response";
+
+interface IResourcePropertiesAndAttributesViewModelScope extends ICompositionViewModelScope {
+ properties:PropertiesGroup;
+ attributes:AttributesGroup;
+ propertiesMessage:string;
+ groupPropertiesByInstance:boolean;
+ showGroupsOfInstanceProperties:Array<boolean>;
+ addProperty():void;
+ updateProperty(property:PropertyModel):void;
+ deleteProperty(property:PropertyModel):void;
+ viewAttribute(attribute:AttributeModel):void;
+ groupNameByKey(key:string):string;
+ isPropertyOwner():boolean;
+ getComponentInstanceNameFromInstanceByKey(key:string):string;
+}
+
+export class ResourcePropertiesViewModel {
+
+ static '$inject' = [
+ '$scope',
+ '$filter',
+ '$uibModal',
+ 'ModalsHandler',
+ 'ComponentServiceNg2'
+
+ ];
+
+
+ constructor(private $scope:IResourcePropertiesAndAttributesViewModelScope,
+ private $filter:ng.IFilterService,
+ private $uibModal:ng.ui.bootstrap.IModalService,
+ private ModalsHandler:ModalsHandler,
+ private ComponentServiceNg2:ComponentServiceNg2) {
+
+ this.getComponentInstancesPropertiesAndAttributes();
+ }
+
+ private initComponentProperties = ():void => {
+ let result:PropertiesGroup = {};
+
+ if (this.$scope.selectedComponent) {
+ this.$scope.propertiesMessage = undefined;
+ this.$scope.groupPropertiesByInstance = false;
+ if (this.$scope.isComponentInstanceSelected()) {
+ if (this.$scope.currentComponent.selectedInstance.originType === 'VF') {
+ this.$scope.groupPropertiesByInstance = true;
+ }
+ result[this.$scope.currentComponent.selectedInstance.uniqueId] = this.$scope.currentComponent.componentInstancesProperties[this.$scope.currentComponent.selectedInstance.uniqueId];
+ } else if (this.$scope.currentComponent.isService()) {
+ // Temporally fix to hide properties for service (UI stack when there are many properties)
+ result = this.$scope.currentComponent.componentInstancesProperties;
+ this.$scope.propertiesMessage = "Note: properties for service are disabled";
+ } else {
+ let key = this.$scope.selectedComponent.uniqueId;
+ result[key] = Array<PropertyModel>();
+ let derived = Array<PropertyModel>();
+ _.forEach(this.$scope.selectedComponent.properties, (property:PropertyModel) => {
+ if (key == property.parentUniqueId) {
+ result[key].push(property);
+ } else {
+ property.readonly = true;
+ derived.push(property);
+ }
+ });
+ if (derived.length) {
+ result['derived'] = derived;
+ }
+ }
+ this.$scope.properties = result;
+ }
+ };
+
+
+ private initComponentAttributes = ():void => {
+ let result:AttributesGroup = {};
+
+ if (this.$scope.selectedComponent) {
+ if (this.$scope.isComponentInstanceSelected()) {
+ result[this.$scope.currentComponent.selectedInstance.uniqueId] = this.$scope.currentComponent.componentInstancesAttributes[this.$scope.currentComponent.selectedInstance.uniqueId];
+ } else if (this.$scope.currentComponent.isService()) {
+ result = this.$scope.currentComponent.componentInstancesAttributes;
+ }
+ this.$scope.attributes = result;
+ }
+ };
+
+ /**
+ * This function is checking if the component is the value owner of the current property
+ * in order to notify the edit property modal which fields to disable
+ */
+ private isPropertyValueOwner = ():boolean => {
+ return this.$scope.currentComponent.isService() || !!this.$scope.currentComponent.selectedInstance;
+ };
+
+ /**
+ * The function opens the edit property modal.
+ * It checks if the property is from the VF or from one of it's resource instances and sends the needed property list.
+ * For create property reasons an empty array is transferd
+ *
+ * @param property the wanted property to edit/create
+ */
+ private openEditPropertyModal = (property:PropertyModel):void => {
+ this.ModalsHandler.openEditPropertyModal(property,
+ this.$scope.component,
+ (this.$scope.isPropertyOwner() ?
+ this.$scope.properties[property.parentUniqueId] :
+ this.$scope.properties[property.resourceInstanceUniqueId]) || [],
+ this.isPropertyValueOwner()).then(() => {
+ });
+ };
+
+ private openAttributeModal = (atrribute:AttributeModel):void => {
+
+ let modalOptions:ng.ui.bootstrap.IModalSettings = {
+ template: 'app/view-models/forms/attribute-form/attribute-form-view.html',
+ controller: 'Sdc.ViewModels.AttributeFormViewModel',
+ size: 'sdc-md',
+ backdrop: 'static',
+ keyboard: false,
+ resolve: {
+ attribute: ():AttributeModel => {
+ return atrribute;
+ },
+ component: ():Component => {
+ return this.$scope.currentComponent;
+ }
+ }
+ };
+ this.$uibModal.open(modalOptions);
+ };
+
+ private getComponentInstancesPropertiesAndAttributes = () => {
+
+ this.ComponentServiceNg2.getComponentInstanceAttributesAndProperties(this.$scope.currentComponent).subscribe((genericResponse:ComponentGenericResponse) => {
+ this.$scope.currentComponent.componentInstancesAttributes = genericResponse.componentInstancesAttributes;
+ this.$scope.currentComponent.componentInstancesProperties = genericResponse.componentInstancesProperties;
+ this.initScope();
+ });
+ };
+
+ private initScope = ():void => {
+
+
+ this.initComponentProperties();
+ this.initComponentAttributes();
+
+ this.$scope.$watchCollection('currentComponent.properties', (newData:any):void => {
+ this.initComponentProperties();
+ });
+
+ this.$scope.$watch('currentComponent.selectedInstance', (newInstance:ComponentInstance):void => {
+ if (angular.isDefined(newInstance)) {
+ this.initComponentProperties();
+ this.initComponentAttributes();
+
+ }
+ });
+
+ this.$scope.isPropertyOwner = ():boolean => {
+ return this.$scope.currentComponent && this.$scope.currentComponent.isResource() && !this.$scope.isComponentInstanceSelected();
+ };
+
+ this.$scope.updateProperty = (property:PropertyModel):void => {
+ this.openEditPropertyModal(property);
+ };
+
+ this.$scope.deleteProperty = (property:PropertyModel):void => {
+
+ let onOk = ():void => {
+ this.$scope.currentComponent.deleteProperty(property.uniqueId);
+ };
+
+ let title:string = this.$filter('translate')("PROPERTY_VIEW_DELETE_MODAL_TITLE");
+ let message:string = this.$filter('translate')("PROPERTY_VIEW_DELETE_MODAL_TEXT", "{'name': '" + property.name + "'}");
+ this.ModalsHandler.openConfirmationModal(title, message, false).then(onOk);
+ };
+
+ this.$scope.viewAttribute = (attribute:AttributeModel):void => {
+ this.openAttributeModal(attribute);
+ };
+
+ this.$scope.groupNameByKey = (key:string):string => {
+ switch (key) {
+ case 'derived':
+ return "Derived";
+
+ case this.$scope.currentComponent.uniqueId:
+ return this.$filter("resourceName")(this.$scope.currentComponent.name);
+
+ default:
+ return this.$filter("resourceName")((_.find(this.$scope.currentComponent.componentInstances, {uniqueId: key})).name);
+ }
+ };
+
+ this.$scope.getComponentInstanceNameFromInstanceByKey = (key:string):string => {
+ let instanceName:string = "";
+ if (key !== undefined && this.$scope.selectedComponent.uniqueId == this.$scope.currentComponent.selectedInstance.componentUid) {
+ instanceName = this.$filter("resourceName")((_.find(this.$scope.selectedComponent.componentInstances, {uniqueId: key})).name);
+ }
+ return instanceName;
+ };
+
+ }
+}
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/properties-and-attributes/properties-view.html b/catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/properties-and-attributes/properties-view.html
new file mode 100644
index 0000000000..6df8b6a4d6
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/properties-and-attributes/properties-view.html
@@ -0,0 +1,117 @@
+<perfect-scrollbar class="w-sdc-designer-sidebar-tab-content properties" id="main-scroll">
+
+ <div class="w-sdc-designer-sidebar-section">
+
+ <!--expand-collapse data-ng-if="isPropertyOwner() && !currentComponent.properties.length" expanded-selector=".w-sdc-composition-sidebar-section-content.{{currentComponent.name}}"
+ class="w-sdc-composition-sidebar-section-title">
+ <span class="w-sdc-composition-sidebar-section-title-text" tooltips tooltip-content="{{currentComponent.name | resourceName}}&nbsp;Properties"
+ data-ng-bind="(currentComponent.name | resourceName)+ ' Properties'"></span>
+ <div class="w-sdc-composition-sidebar-section-title-icon"></div>
+ </expand-collapse-->
+ <!--properties-->
+ <expand-collapse data-ng-repeat-start="(key, group) in properties"
+ expanded-selector=".w-sdc-designer-sidebar-section-content.properties.{{$index}}">
+ <div class="first-level">
+ <div class="expand-collapse-title-icon"></div>
+ <span class="w-sdc-designer-sidebar-section-title-text" data-ng-bind="groupNameByKey(key) + ' Properties'"
+ tooltips tooltip-content="{{groupNameByKey(key)}}&nbsp;Properties"
+ data-tests-id="vfi-properties"></span>
+ </div>
+ </expand-collapse>
+
+ <div data-ng-repeat-end="" class="w-sdc-designer-sidebar-section-content properties {{$index}}"> <!--data-ng-show="isShowDetailsSection" -->
+ <div class="i-sdc-designer-sidebar-section-content-item" data-ng-if="!groupPropertiesByInstance">
+ <div class="i-sdc-designer-sidebar-section-content-item-property-and-attribute" data-tests-id="propertyRow"
+ data-ng-repeat="property in group | orderBy: 'name' track by $index">
+ <div>
+ <span class="i-sdc-designer-sidebar-section-content-item-property-and-attribute-label"
+ data-ng-class="{'hand enabled': !$parent.isViewOnly}"
+ tooltips tooltip-content="{{property.name}}"
+ data-ng-click="!$parent.isViewOnly && updateProperty(property)"
+ data-tests-id="{{property.name}}">{{property.name}}</span>
+ </div>
+ <div>
+ <span class="i-sdc-designer-sidebar-section-content-item-property-value" data-ng-if="isPropertyOwner()"
+ tooltips tooltip-content="{{property.defaultValue}}">{{property.defaultValue}}</span>
+ <span class="i-sdc-designer-sidebar-section-content-item-property-value" data-ng-if="!isPropertyOwner()"
+ tooltips tooltip-content="{{property.value}}"
+ data-tests-id="value_{{property.name}}">{{property.value}}</span>
+ </div>
+ <button class="i-sdc-designer-sidebar-section-content-item-button delete sprite e-sdc-small-icon-delete"
+ data-ng-if="!$parent.isViewOnly&&(isPropertyOwner() && !property.readonly)"
+ data-ng-click="deleteProperty(property)" type="button"></button>
+ </div>
+ </div>
+ <div class="i-sdc-designer-sidebar-section-content-item" data-ng-if="groupPropertiesByInstance">
+ <div data-ng-repeat-start="(instancesIds , InstanceProperties) in (group | groupBy:'path')"
+ class="vfci-properties-group"
+ data-ng-click="showGroupsOfInstanceProperties[$index]=!showGroupsOfInstanceProperties[$index]"
+ data-ng-class="{'expanded':showGroupsOfInstanceProperties[$index]}">
+ <div class="second-level">
+ <div class="expand-collapse-title-icon"></div>
+ <span class="w-sdc-designer-sidebar-section-title-text" data-ng-bind="getComponentInstanceNameFromInstanceByKey(InstanceProperties[0].path[1]) + ' Properties'"
+ tooltips tooltip-content="{{getComponentInstanceNameFromInstanceByKey(InstanceProperties[0].path[1])}}&nbsp;Properties"
+ data-tests-id="vfci-properties"></span>
+ </div>
+ </div>
+ <div data-ng-repeat-end="" class="w-sdc-designer-sidebar-section-content instance-properties {{$index}}" data-ng-if="showGroupsOfInstanceProperties[$index]">
+ <div class="i-sdc-designer-sidebar-section-content-item">
+ <div class="i-sdc-designer-sidebar-section-content-item-property-and-attribute" data-tests-id="propertyRow"
+ data-ng-repeat="instanceProperty in InstanceProperties | orderBy: 'name'">
+ <div>
+ <span class="i-sdc-designer-sidebar-section-content-item-property-and-attribute-label"
+ data-ng-class="{'hand enabled': !$parent.isViewOnly}"
+ tooltips tooltip-content="{{instanceProperty.name}}"
+ data-tests-id="vfci-property">{{instanceProperty.name}}</span>
+ </div>
+ <div>
+ <span class="i-sdc-designer-sidebar-section-content-item-property-value"
+ tooltips tooltip-content="{{instanceProperty.value === undefined ? instanceProperty.defaultValue : instanceProperty.value}}">
+ {{instanceProperty.value === undefined ? instanceProperty.defaultValue : instanceProperty.value}}</span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <!--<div class="w-sdc-designer-sidebar-section-footer" data-ng-if="(!$parent.isViewOnly && isPropertyOwner()) || showAddPropertyButton">-->
+ <!--<button class="w-sdc-designer-sidebar-section-footer-action tlv-btn blue" data-tests-id="addGrey" data-ng-click="addProperty()" type="button">-->
+ <!--Add Property-->
+ <!--</button>-->
+ <!--</div>-->
+ </div>
+
+
+ <!--attributes-->
+ <expand-collapse data-ng-repeat-start="(key, group) in attributes"
+ expanded-selector=".w-sdc-designer-sidebar-section-content.attributes.{{$index}}">
+ <div class="first-level">
+ <div class="expand-collapse-title-icon"></div>
+ <span class="w-sdc-designer-sidebar-section-title-text" data-ng-bind="groupNameByKey(key) + ' Attributes'"
+ tooltips tooltip-content="{{groupNameByKey(key)}}&nbsp;Attributes"></span>
+ </div>
+ </expand-collapse>
+
+ <div data-ng-repeat-end="" class="w-sdc-designer-sidebar-section-content attributes {{$index}}"> <!--data-ng-show="isShowDetailsSection" -->
+ <div class="i-sdc-designer-sidebar-section-content-item">
+ <div class="i-sdc-designer-sidebar-section-content-item-property-and-attribute"
+ data-ng-repeat="attribute in group | orderBy: 'name' track by $index">
+ <div>
+ <span class="i-sdc-designer-sidebar-section-content-item-property-and-attribute-label"
+ data-ng-class="{'hand enabled': !$parent.isViewOnly}"
+ tooltips tooltip-content="{{attribute.name}}"
+ data-ng-click="!$parent.isViewOnly && viewAttribute(attribute)"
+ data-tests-id="{{attribute.name}}-attr">{{attribute.name}}</span>
+ </div>
+ <div>
+ <span class="i-sdc-designer-sidebar-section-content-item-property-value" data-ng-if="isPropertyOwner()"
+ tooltips tooltip-content="{{attribute.defaultValue}}">{{attribute.defaultValue}}</span>
+ <span class="i-sdc-designer-sidebar-section-content-item-property-value" data-ng-if="!isPropertyOwner()"
+ tooltips tooltip-content="{{attribute.value}}" data-tests-id="value-of-{{attribute.name}}">{{attribute.value}}</span>
+ </div>
+ </div>
+ </div>
+
+ </div>
+
+ </div>
+</perfect-scrollbar>
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/properties-and-attributes/properties.less b/catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/properties-and-attributes/properties.less
new file mode 100644
index 0000000000..41a90bff9d
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/properties-and-attributes/properties.less
@@ -0,0 +1,38 @@
+.w-sdc-designer-sidebar-tab-content.properties {
+ .i-sdc-designer-sidebar-section-content-item-property-and-attribute-label{
+ font-weight: bold;
+ }
+ .i-sdc-designer-sidebar-section-content-item-button.update{
+ right: 17px;
+ }
+ .i-sdc-designer-sidebar-section-content-item-button.delete{
+ right: 35px;
+ }
+
+ .w-sdc-designer-sidebar-properties-disabled {
+ .s_14_m;
+ padding: 20px 20px;
+ }
+
+ .vfci-properties-group{
+ background-color: @func_color_r;
+ }
+
+ .expand-collapse-title-icon{
+ .hand;
+ .sprite-new;
+ .expand-collapse-plus-icon;
+ vertical-align: middle;
+ margin: 0 6px;
+ }
+
+ .expanded {
+ .expand-collapse-title-icon {
+ .expand-collapse-minus-icon;
+ }
+ }
+
+ .w-sdc-designer-sidebar-section-title-text{
+ vertical-align: middle;
+ }
+}
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/relations/relations-view-model.ts b/catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/relations/relations-view-model.ts
new file mode 100644
index 0000000000..325f250ebe
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/relations/relations-view-model.ts
@@ -0,0 +1,156 @@
+'use strict';
+import {ICompositionViewModelScope} from "../../composition-view-model";
+import {CapabilitiesGroup, Requirement, RequirementsGroup} from "app/models";
+import {ComponentServiceNg2} from "app/ng2/services/component-services/component.service";
+import {ComponentGenericResponse} from "app/ng2/services/responses/component-generic-response";
+import {GRAPH_EVENTS} from "app/utils";
+import {EventListenerService} from "app/services";
+import {ComponentInstance, Capability} from "app/models";
+
+interface IRelationsViewModelScope extends ICompositionViewModelScope {
+ isLoading:boolean;
+ $parent:ICompositionViewModelScope;
+ getRelation(requirement:any):any;
+ capabilities:Array<Capability>;
+ requirements:Array<Requirement>;
+
+ //for complex components
+ capabilitiesInstancesMap:InstanceCapabilitiesMap;
+ requirementsInstancesMap:InstanceRequirementsMap;
+}
+export class InstanceCapabilitiesMap {
+ [key:string]:Array<Capability>;
+}
+
+export class InstanceRequirementsMap {
+ [key:string]:Array<Requirement>;
+}
+
+export class RelationsViewModel {
+
+ static '$inject' = [
+ '$scope',
+ '$filter',
+ 'ComponentServiceNg2',
+ 'EventListenerService'
+ ];
+
+ constructor(private $scope:IRelationsViewModelScope,
+ private $filter:ng.IFilterService,
+ private ComponentServiceNg2:ComponentServiceNg2,
+ private eventListenerService:EventListenerService) {
+ this.initScope();
+ }
+
+ private loadComplexComponentData = () => {
+ this.$scope.isLoading = true;
+ this.ComponentServiceNg2.getCapabilitiesAndRequirements(this.$scope.currentComponent.componentType, this.$scope.currentComponent.uniqueId).subscribe((response:ComponentGenericResponse) => {
+ this.$scope.currentComponent.capabilities = response.capabilities;
+ this.$scope.currentComponent.requirements = response.requirements;
+ this.setScopeCapabilitiesRequirements(this.$scope.currentComponent.capabilities, this.$scope.currentComponent.requirements);
+ this.initInstancesMap();
+ this.$scope.isLoading = false;
+ });
+ }
+
+
+ private extractValuesFromMap = (map:CapabilitiesGroup | RequirementsGroup):Array<any> => {
+ let values = [];
+ _.forEach(map, (capabilitiesOrRequirements:Array<Capability> | Array<Requirement>, key) => {
+ values = values.concat(capabilitiesOrRequirements)
+ }
+ );
+ return values;
+ }
+
+ private setScopeCapabilitiesRequirements = (capabilities:CapabilitiesGroup, requirements:RequirementsGroup) => {
+ this.$scope.capabilities = this.extractValuesFromMap(capabilities);
+ this.$scope.requirements = this.extractValuesFromMap(requirements);
+ }
+
+
+ private initInstancesMap = ():void => {
+
+ this.$scope.capabilitiesInstancesMap = new InstanceCapabilitiesMap();
+ _.forEach(this.$scope.capabilities, (capability:Capability) => {
+ if (this.$scope.capabilitiesInstancesMap[capability.ownerName]) {
+ this.$scope.capabilitiesInstancesMap[capability.ownerName] = this.$scope.capabilitiesInstancesMap[capability.ownerName].concat(capability);
+ } else {
+ this.$scope.capabilitiesInstancesMap[capability.ownerName] = new Array<Capability>(capability);
+ }
+ });
+
+ this.$scope.requirementsInstancesMap = new InstanceRequirementsMap();
+ _.forEach(this.$scope.requirements, (requirement:Requirement) => {
+ if (this.$scope.requirementsInstancesMap[requirement.ownerName]) {
+ this.$scope.requirementsInstancesMap[requirement.ownerName] = this.$scope.requirementsInstancesMap[requirement.ownerName].concat(requirement);
+ } else {
+ this.$scope.requirementsInstancesMap[requirement.ownerName] = new Array<Requirement>(requirement);
+ }
+ });
+ }
+
+ private initRequirementsAndCapabilities = (needUpdate?: boolean) => {
+
+ // if instance selected, we take the requirement and capabilities of the instance - always exist because we load them with the graph
+ if (this.$scope.isComponentInstanceSelected()) {
+ this.$scope.isLoading = false;
+ this.setScopeCapabilitiesRequirements(this.$scope.currentComponent.selectedInstance.capabilities, this.$scope.currentComponent.selectedInstance.requirements);
+ if (this.$scope.currentComponent.selectedInstance.originType === 'VF') {
+ this.initInstancesMap();
+ }
+ } else {
+ // if instance not selected, we take the requirement and capabilities of the VF/SERVICE, if not exist we call api
+ if (needUpdate || !this.$scope.currentComponent.capabilities || !this.$scope.currentComponent.requirements) {
+ this.loadComplexComponentData();
+
+ } else {
+ this.$scope.isLoading = false;
+ this.setScopeCapabilitiesRequirements(this.$scope.currentComponent.capabilities, this.$scope.currentComponent.requirements);
+ this.initInstancesMap();
+ }
+ }
+ }
+
+ private updateRequirementCapabilities = () => {
+ if (!this.$scope.isComponentInstanceSelected()) {
+ this.loadComplexComponentData();
+ }
+ }
+
+ private initEvents = ():void => {
+ this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_NODE_SELECTED, this.initRequirementsAndCapabilities);
+ this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_GRAPH_BACKGROUND_CLICKED, this.updateRequirementCapabilities);
+ this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_CREATE_COMPONENT_INSTANCE, this.updateRequirementCapabilities);
+ this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_DELETE_COMPONENT_INSTANCE, this.updateRequirementCapabilities);
+ }
+
+ private initScope = ():void => {
+
+ this.$scope.requirements = [];
+ this.$scope.capabilities = [];
+
+ this.initEvents();
+ this.initRequirementsAndCapabilities();
+
+ this.$scope.isCurrentDisplayComponentIsComplex = ():boolean => {
+ if (this.$scope.isComponentInstanceSelected()) {
+ if (this.$scope.currentComponent.selectedInstance.originType === 'VF') {
+ return true;
+ }
+ return false;
+ } else {
+ return this.$scope.currentComponent.isComplex();
+ }
+ }
+
+ this.$scope.$on('$destroy', () => {
+
+ this.eventListenerService.unRegisterObserver(GRAPH_EVENTS.ON_NODE_SELECTED, this.initRequirementsAndCapabilities);
+ this.eventListenerService.unRegisterObserver(GRAPH_EVENTS.ON_GRAPH_BACKGROUND_CLICKED, this.updateRequirementCapabilities);
+ this.eventListenerService.unRegisterObserver(GRAPH_EVENTS.ON_CREATE_COMPONENT_INSTANCE, this.updateRequirementCapabilities);
+ this.eventListenerService.unRegisterObserver(GRAPH_EVENTS.ON_DELETE_COMPONENT_INSTANCE, this.updateRequirementCapabilities);
+ });
+
+ }
+}
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/relations/relations-view.html b/catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/relations/relations-view.html
new file mode 100644
index 0000000000..5ecb12cd6f
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/relations/relations-view.html
@@ -0,0 +1,61 @@
+<perfect-scrollbar class="w-sdc-designer-sidebar-tab-content sdc-general-tab relations">
+ <div ng-if="!isCurrentDisplayComponentIsComplex()">
+ <div class="w-sdc-designer-sidebar-section w-sdc-designer-sidebar-section-relations">
+ <expand-collapse expanded-selector=".w-sdc-designer-sidebar-section-content.capabilities" class="w-sdc-designer-sidebar-section-title"> Capabilities
+ <div class="w-sdc-designer-sidebar-section-title-icon"></div>
+ </expand-collapse>
+ <div class="w-sdc-designer-sidebar-section-content capabilities">
+ <capabilities-list capabilities="capabilities"></capabilities-list>
+ </div>
+ </div>
+ <div class="w-sdc-designer-sidebar-section w-sdc-designer-sidebar-section-relations">
+ <expand-collapse expanded-selector=".w-sdc-designer-sidebar-section-content.requirements" class="w-sdc-designer-sidebar-section-title"> Requirements
+ <div class="w-sdc-designer-sidebar-section-title-icon"></div>
+ </expand-collapse>
+
+ <div class="w-sdc-designer-sidebar-section-content requirements">
+ <requirements-list component='currentComponent' requirements="requirements"></requirements-list>
+ </div>
+ </div>
+ </div>
+
+ <div ng-if="isCurrentDisplayComponentIsComplex()">
+ <div class="w-sdc-designer-sidebar-section w-sdc-designer-sidebar-section-relations">
+ <expand-collapse expanded-selector=".w-sdc-designer-sidebar-section-content.capabilities" class="w-sdc-designer-sidebar-section-title"> Capabilities
+ <div class="w-sdc-designer-sidebar-section-title-icon"></div>
+ </expand-collapse>
+ </div>
+ <div class="w-sdc-designer-sidebar-section-content capabilities">
+ <expand-collapse expanded-selector=".capabilities-component-instances.{{$index}}" is-close-on-init="true" class="general-tab-expand-collapse"
+ data-ng-repeat-start="(key, instanceCapabilities) in capabilitiesInstancesMap track by $index">
+ <div class="expand-collapse-title second-level">
+ <div class="expand-collapse-title-icon"></div>
+ <span class="expand-collapse-title-text" data-ng-bind="key"></span>
+ </div>
+ </expand-collapse>
+
+ <div data-ng-repeat-end="" class="capabilities-component-instances {{$index}}">
+ <capabilities-list capabilities="instanceCapabilities"></capabilities-list>
+ </div>
+ </div>
+
+ <div class="w-sdc-designer-sidebar-section w-sdc-designer-sidebar-section-relations">
+ <expand-collapse expanded-selector=".w-sdc-designer-sidebar-section-content.requirements" class="w-sdc-designer-sidebar-section-title"> Requirements
+ <div class="w-sdc-designer-sidebar-section-title-icon"></div>
+ </expand-collapse>
+ </div>
+ <div class="w-sdc-designer-sidebar-section-content requirements">
+ <expand-collapse expanded-selector=".requirements-component-instances.{{$index}}" is-close-on-init="true" class="general-tab-expand-collapse"
+ data-ng-repeat-start="(key, instanceRequirements) in requirementsInstancesMap track by $index">
+ <div class="expand-collapse-title second-level">
+ <div class="expand-collapse-title-icon"></div>
+ <span class="expand-collapse-title-text" data-ng-bind="key"></span>
+ </div>
+ </expand-collapse>
+
+ <div data-ng-repeat-end="" class="requirements-component-instances {{$index}}">
+ <requirements-list component='currentComponent' requirements="instanceRequirements"></requirements-list>
+ </div>
+ </div>
+ </div>
+</perfect-scrollbar>
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/relations/relations.less b/catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/relations/relations.less
new file mode 100644
index 0000000000..c3b224d5a6
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/relations/relations.less
@@ -0,0 +1,14 @@
+.w-sdc-designer-sidebar-tab-content.relations {
+
+ .w-sdc-designer-sidebar-section-content {
+ padding: 0;
+ }
+
+ .w-sdc-designer-sidebar-section-title {
+ &.expanded {
+ margin-bottom: 0;
+ }
+ }
+}
+
+
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/structure/structure-view.html b/catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/structure/structure-view.html
new file mode 100644
index 0000000000..2070041990
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/structure/structure-view.html
@@ -0,0 +1,13 @@
+<perfect-scrollbar include-padding="true" class="w-sdc-designer-sidebar-tab-content">
+
+ <div class="w-sdc-designer-sidebar-section">
+ <expand-collapse expanded-selector=".w-sdc-designer-sidebar-section-content" class="w-sdc-designer-sidebar-section-title">
+ Composition
+ <div class="w-sdc-designer-sidebar-section-title-icon"></div>
+ </expand-collapse>
+
+ <div class="w-sdc-designer-sidebar-section-content" ng-show="selectedComponent.isComplex()">
+ <structure-tree component="selectedComponent"></structure-tree>
+ </div>
+ </div>
+</perfect-scrollbar>
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/structure/structure-view.ts b/catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/structure/structure-view.ts
new file mode 100644
index 0000000000..1c28a46d37
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/composition/tabs/structure/structure-view.ts
@@ -0,0 +1,14 @@
+'use strict';
+import {ICompositionViewModelScope} from "../../composition-view-model";
+
+interface IStructureViewModel extends ICompositionViewModelScope {
+}
+
+export class StructureViewModel {
+ static '$inject' = [
+ '$scope'
+ ];
+
+ constructor(private $scope:IStructureViewModel) {
+ }
+}
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/deployment-artifacts/deployment-artifacts-description-popover.html b/catalog-ui/src/app/view-models/workspace/tabs/deployment-artifacts/deployment-artifacts-description-popover.html
new file mode 100644
index 0000000000..0d7d5dc8f4
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/deployment-artifacts/deployment-artifacts-description-popover.html
@@ -0,0 +1,23 @@
+<!-- Description Popover -->
+<div >
+ <span data-tests-id='popover-x-button' data-ng-click='closeDescriptionPopover()' class='tlv-sprite tlv-x-btn close-popover-btn'></span>
+ <div class="w-sdc-form-item" ng-form="descriptionForm" data-ng-class="{error:(descriptionForm.$dirty && descriptionForm.$invalid)}">
+ <textarea class="i-sdc-form-textarea {{$index}}" data-ng-class="{'view-mode': isViewMode()}"
+ data-ng-maxlength="256"
+ maxlength="256"
+ data-ng-required="true"
+ name="description"
+ data-ng-model="artifact.description"
+ data-ng-model-options="{ debounce: 200 }"
+ data-ng-pattern="getValidationPattern('string')"
+ ng-readonly="isViewMode()"
+ data-tests-id="description">
+ </textarea>
+
+ <div class="input-error" data-ng-show="descriptionForm.$dirty && descriptionForm.$invalid">
+ <span ng-show="descriptionForm.$error.required" translate="ADD_ARTIFACT_ERROR_DESCRIPTION_REQUIRED"></span>
+ <span ng-show="descriptionForm.$error.maxlength" translate="VALIDATION_ERROR_MAX_LENGTH" translate-values="{'max': '256' }"></span>
+ <span ng-show="descriptionForm.$error.pattern" translate="VALIDATION_ERROR_SPECIAL_CHARS_NOT_ALLOWED"></span>
+ </div>
+ </div>
+</div>
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/deployment-artifacts/deployment-artifacts-view-model.ts b/catalog-ui/src/app/view-models/workspace/tabs/deployment-artifacts/deployment-artifacts-view-model.ts
new file mode 100644
index 0000000000..2816c312f9
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/deployment-artifacts/deployment-artifacts-view-model.ts
@@ -0,0 +1,276 @@
+//@require "./*.html"
+'use strict';
+import {IWorkspaceViewModelScope} from "app/view-models/workspace/workspace-view-model";
+import {ArtifactModel, ArtifactGroupModel, Resource} from "app/models";
+import {ArtifactsUtils, ModalsHandler, ValidationUtils} from "app/utils";
+import {ComponentServiceNg2} from "app/ng2/services/component-services/component.service";
+import {ComponentGenericResponse} from "../../../../ng2/services/responses/component-generic-response";
+
+interface IDeploymentArtifactsViewModelScope extends IWorkspaceViewModelScope {
+ tableHeadersList:Array<any>;
+ reverse:boolean;
+ sortBy:string;
+ artifacts:Array<ArtifactModel>;
+ editForm:ng.IFormController;
+ isLoading:boolean;
+ artifactDescriptions:any;
+ selectedArtifactId:string;
+ popoverTemplate:string;
+
+ addOrUpdate(artifact:ArtifactModel):void;
+ updateSelectedArtifact():void;
+ delete(artifact:ArtifactModel):void;
+ sort(sortBy:string):void;
+ noArtifactsToShow():boolean;
+ getValidationPattern(validationType:string, parameterType?:string):RegExp;
+ validateJson(json:string):boolean;
+ resetValue(parameter:any):void;
+ viewModeOrCsarComponent():boolean;
+ isLicenseArtifact(artifact:ArtifactModel):void;
+ getEnvArtifact(heatArtifact:ArtifactModel):ArtifactModel;
+ getEnvArtifactName(artifact:ArtifactModel):string;
+ openEditEnvParametersModal(artifact:ArtifactModel):void;
+ openDescriptionPopover(artifactId:string):void;
+ closeDescriptionPopover():void;
+}
+
+export class DeploymentArtifactsViewModel {
+
+ static '$inject' = [
+ '$scope',
+ '$templateCache',
+ '$filter',
+ 'ValidationUtils',
+ 'ArtifactsUtils',
+ 'ModalsHandler',
+ 'ComponentServiceNg2'
+ ];
+
+ constructor(private $scope:IDeploymentArtifactsViewModelScope,
+ private $templateCache:ng.ITemplateCacheService,
+ private $filter:ng.IFilterService,
+ private validationUtils:ValidationUtils,
+ private artifactsUtils:ArtifactsUtils,
+ private ModalsHandler:ModalsHandler,
+ private ComponentServiceNg2: ComponentServiceNg2) {
+ this.initScope();
+ this.$scope.updateSelectedMenuItem();
+ }
+
+ private initDescriptions = ():void => {
+ this.$scope.artifactDescriptions = {};
+ _.forEach(this.$scope.component.deploymentArtifacts, (artifact:ArtifactModel):void => {
+ this.$scope.artifactDescriptions[artifact.artifactLabel] = artifact.description;
+ });
+ };
+
+ private setArtifact = (artifact:ArtifactModel):void => {
+ if (!artifact.description || !this.$scope.getValidationPattern('string').test(artifact.description)) {
+ artifact.description = this.$scope.artifactDescriptions[artifact.artifactLabel];
+ }
+ };
+
+ private initScopeArtifacts = ()=> {
+ this.$scope.artifacts = <ArtifactModel[]>_.values(this.$scope.component.deploymentArtifacts);
+ _.forEach(this.$scope.artifacts, (artifact:ArtifactModel):void => {
+ artifact.envArtifact = this.getEnvArtifact(artifact);
+ });
+ };
+
+ private initArtifacts = (loadFromServer:boolean):void => {
+ if (loadFromServer) {
+ this.$scope.isLoading = true;
+ this.ComponentServiceNg2.getComponentDeploymentArtifacts(this.$scope.component).subscribe((response:ComponentGenericResponse) => {
+ this.$scope.component.deploymentArtifacts = response.deploymentArtifacts;
+ this.initScopeArtifacts();
+ this.$scope.isLoading = false;
+ });
+ } else {
+ this.initScopeArtifacts();
+ }
+
+ };
+
+ private getEnvArtifact = (heatArtifact:ArtifactModel):ArtifactModel=> {
+ return _.find(this.$scope.artifacts, (item:ArtifactModel)=> {
+ return item.generatedFromId === heatArtifact.uniqueId;
+ });
+ };
+
+ private getCurrentArtifact = ():ArtifactModel => {
+ if (!this.$scope.selectedArtifactId) {
+ return null;
+ }
+ let artifact:ArtifactModel = this.$scope.artifacts.filter((art) => {
+ return art.uniqueId == this.$scope.selectedArtifactId;
+ })[0];
+ return artifact;
+ }
+
+ private initScope = ():void => {
+ let self = this;
+ this.$scope.isLoading = false;
+ this.$scope.selectedArtifactId = null;
+ this.initDescriptions();
+ if(this.$scope.component.deploymentArtifacts) {
+ this.initArtifacts(false);
+ } else {
+ this.initArtifacts(true);
+ }
+ this.$scope.setValidState(true);
+
+ this.$scope.tableHeadersList = [
+ {title: 'Name', property: 'artifactDisplayName'},
+ {title: 'Type', property: 'artifactType'},
+ {title: 'Deployment timeout', property: 'timeout'},
+ {title: 'Version', property: 'artifactVersion'},
+ {title: 'UUID', property: 'artifactUUID'}
+ ];
+
+ this.$templateCache.put("deployment-artifacts-description-popover.html", require('app/view-models/workspace/tabs/deployment-artifacts/deployment-artifacts-description-popover.html'));
+ this.$scope.popoverTemplate = "deployment-artifacts-description-popover.html";
+
+ this.$scope.isLicenseArtifact = (artifact:ArtifactModel):boolean => {
+ let isLicense:boolean = false;
+ if (this.$scope.component.isResource() && (<Resource>this.$scope.component).isCsarComponent()) {
+
+ isLicense = this.artifactsUtils.isLicenseType(artifact.artifactType);
+ }
+
+ return isLicense;
+ };
+
+ this.$scope.sort = (sortBy:string):void => {
+ this.$scope.reverse = (this.$scope.sortBy === sortBy) ? !this.$scope.reverse : false;
+ this.$scope.sortBy = sortBy;
+ };
+
+ this.$scope.getValidationPattern = (validationType:string, parameterType?:string):RegExp => {
+ return this.validationUtils.getValidationPattern(validationType, parameterType);
+ };
+
+ this.$scope.validateJson = (json:string):boolean => {
+ if (!json) {
+ return true;
+ }
+ return this.validationUtils.validateJson(json);
+ };
+
+ this.$scope.viewModeOrCsarComponent = ():boolean => {
+ return this.$scope.isViewMode() || (this.$scope.component.isResource() && (<Resource>this.$scope.component).isCsarComponent());
+ };
+
+ this.$scope.addOrUpdate = (artifact:ArtifactModel):void => {
+ artifact.artifactGroupType = 'DEPLOYMENT';
+ let artifactCopy = new ArtifactModel(artifact);
+
+ let success = (response:any):void => {
+ this.$scope.artifactDescriptions[artifactCopy.artifactLabel] = artifactCopy.description;
+ this.initArtifacts(true);
+ // this.$scope.artifacts = _.values(this.$scope.component.deploymentArtifacts);
+ };
+
+ let error = (err:any):void => {
+ console.log(err);
+ this.initArtifacts(true);
+ // self.$scope.artifacts = _.values(self.$scope.component.deploymentArtifacts);
+ };
+
+ this.ModalsHandler.openArtifactModal(artifactCopy, this.$scope.component).then(success, error);
+ };
+
+ this.$scope.noArtifactsToShow = ():boolean => {
+ return !_.some(this.$scope.artifacts, 'esId');
+ };
+
+ this.$scope.resetValue = (parameter:any):void => {
+ if (!parameter.currentValue && parameter.defaultValue) {
+ parameter.currentValue = parameter.defaultValue;
+ }
+ else if ('boolean' == parameter.type) {
+ parameter.currentValue = parameter.currentValue.toUpperCase();
+ }
+ };
+
+ this.$scope.$watch('editForm.$valid', ():void => {
+ if (this.$scope.editForm) {
+ // this.$scope.setValidState(this.$scope.editForm.$valid);
+ }
+ });
+
+ this.$scope.updateSelectedArtifact = ():void => {
+ if (!this.$scope.isViewMode() && !this.$scope.isLoading) {
+ let artifact:ArtifactModel = this.getCurrentArtifact();
+ this.setArtifact(artifact); //resets artifact description to original value if invalid.
+ if (artifact && artifact.originalDescription != artifact.description) {
+ this.$scope.isLoading = true;
+ let onSuccess = (responseArtifact:ArtifactModel):void => {
+ this.$scope.artifactDescriptions[responseArtifact.artifactLabel] = responseArtifact.description;
+ // this.$scope.artifacts = _.values(this.$scope.component.deploymentArtifacts);
+ this.initArtifacts(true);
+ this.$scope.isLoading = false;
+ };
+
+ let onFailed = (error:any):void => {
+ console.log('Delete artifact returned error:', error);
+ this.$scope.isLoading = false;
+ };
+
+ this.$scope.component.addOrUpdateArtifact(artifact).then(onSuccess, onFailed);
+ }
+ }
+ };
+
+ this.$scope.delete = (artifact:ArtifactModel):void => {
+ let onOk = ():void => {
+ this.$scope.isLoading = true;
+ let onSuccess = ():void => {
+ this.$scope.isLoading = false;
+ this.initArtifacts(true);
+ //this.$scope.artifacts = _.values(this.$scope.component.deploymentArtifacts);
+ };
+
+ let onFailed = (error:any):void => {
+ this.$scope.isLoading = false;
+ console.log('Delete artifact returned error:', error);
+ };
+
+ this.$scope.component.deleteArtifact(artifact.uniqueId, artifact.artifactLabel).then(onSuccess, onFailed);
+ };
+
+ let title:string = self.$filter('translate')("ARTIFACT_VIEW_DELETE_MODAL_TITLE");
+ let message:string = self.$filter('translate')("ARTIFACT_VIEW_DELETE_MODAL_TEXT", "{'name': '" + artifact.artifactDisplayName + "'}");
+ this.ModalsHandler.openConfirmationModal(title, message, false).then(onOk);
+ };
+
+ this.$scope.getEnvArtifactName = (artifact:ArtifactModel):string => {
+ let envArtifact = this.$scope.getEnvArtifact(artifact);
+ if (envArtifact) {
+ return envArtifact.artifactDisplayName;
+ }
+ };
+
+ this.$scope.openEditEnvParametersModal = (artifact:ArtifactModel):void => {
+ this.ModalsHandler.openEditEnvParametersModal(artifact, this.$scope.component).then(()=> {
+ this.initArtifacts(true);
+ }, ()=> {
+ this.initArtifacts(true);
+ });
+ };
+
+ this.$scope.openDescriptionPopover = (artifactId:string):void => {
+ if (this.$scope.selectedArtifactId && this.$scope.selectedArtifactId != artifactId) {
+ this.$scope.updateSelectedArtifact();
+ }
+ this.$scope.selectedArtifactId = artifactId;
+
+ };
+
+ this.$scope.closeDescriptionPopover = ():void => {
+ if (this.$scope.selectedArtifactId) {
+ this.$scope.updateSelectedArtifact();
+ this.$scope.selectedArtifactId = null;
+ }
+ };
+ };
+}
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/deployment-artifacts/deployment-artifacts-view.html b/catalog-ui/src/app/view-models/workspace/tabs/deployment-artifacts/deployment-artifacts-view.html
new file mode 100644
index 0000000000..cc914b07a8
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/deployment-artifacts/deployment-artifacts-view.html
@@ -0,0 +1,126 @@
+<div class="workspace-deployment-artifact">
+
+ <div data-tests-id="add-deployment-artifact-button" ng-if="!isViewMode()" data-ng-class="{'disabled': isDisableMode()}" data-tests-id="add-property-button" class="add-btn" data-ng-click="addOrUpdate({})">Add</div>
+
+ <div class="table-container-flex">
+
+ <div class="table" data-ng-class="{'view-mode': isViewMode()}">
+ <loader data-display="isLoading"></loader>
+ <div class="head flex-container">
+ <div class="table-header head-row hand flex-item" data-ng-repeat="header in tableHeadersList track by $index" data-ng-click="sort(header.property)">{{header.title}}
+ <span data-ng-if="sortBy === header.property" class="table-header-sort-arrow" data-ng-class="{'down': reverse, 'up':!reverse}"> </span>
+ </div>
+ <div class="table-no-text-header head-row flex-item"></div>
+ </div>
+
+ <form class="body" name="editForm">
+
+ <perfect-scrollbar scroll-y-margin-offset="0" include-padding="true" class="scrollbar-container">
+
+ <!-- Artifact row -->
+ <div ng-if="noArtifactsToShow()" data-ng-class="{'disabled': isDisableMode()}" class="no-row-text" translate="DEPLOYMENT_ARTIFACT_NO_ARTIFACTS_TO_DISPLAY"></div>
+ <div data-ng-repeat-start="artifact in artifacts | orderBy:sortBy:reverse track by $index"
+ class="flex-container data-row"
+ data-ng-class="{'selected': selectedArtifactId == artifact.uniqueId }"
+ data-ng-if="artifact.esId && 'HEAT_ENV' !== artifact.artifactType"
+ data-tests-id="artifact-item-{{artifact.artifactDisplayName}}">
+ <div class="table-col-general flex-item" >
+ <div class="heat-env-connect-container" ng-class="{'heat-env-connect-container-view-mode': isViewMode()}" data-ng-if="artifact.envArtifact">
+ <span class="heat-env-connect"></span>
+ </div>
+ <span data-tests-id="artifactDisplayName_{{artifact.artifactDisplayName}}" sdc-smart-tooltip class="artifact-name">{{artifact.artifactDisplayName}}</span>
+
+ <span class="sprite-new show-desc hand description-popover-icon"
+ uib-popover-template="popoverTemplate"
+ popover-class="parameter-description-popover deployment-artifact-view top"
+ popover-title="Description"
+ popover-placement="top-left"
+ popover-is-open="selectedArtifactId == artifact.uniqueId && !isLoading"
+ popover-trigger="'none'"
+ popover-append-to-body="true"
+ data-ng-click="openDescriptionPopover(artifact.uniqueId)"
+ data-tests-id="descriptionIcon_{{artifact.artifactDisplayName}}"></span>
+ </div>
+
+ <div class="table-col-general flex-item text" data-tests-id="artifactType_{{artifact.artifactDisplayName}}" tooltips tooltip-content="{{artifact.artifactType}}">
+ {{artifact.artifactType}}
+ </div>
+ <div class="table-col-general flex-item" data-tests-id="timeout_{{artifact.artifactDisplayName}}">
+ {{artifact.timeout? artifact.timeout:''}}
+ </div>
+ <div class="table-col-general flex-item" data-tests-id="artifactVersion_{{artifact.artifactDisplayName}}">
+ {{artifact.artifactVersion}}
+ </div>
+ <div class="table-col-general flex-item text" data-tests-id="artifactUUID_{{artifact.artifactDisplayName}}" tooltips tooltip-content="{{artifact.artifactUUID}}">
+ <span>{{artifact.artifactUUID}}</span>
+ </div>
+
+ <div class="table-btn-col flex-item">
+ <button class="table-edit-btn" data-tests-id="edit_{{artifact.artifactDisplayName}}"
+ data-ng-if="!isViewMode() && !artifact.isHEAT() && !artifact.isThirdParty() && !isLicenseArtifact(artifact)" data-ng-click="addOrUpdate(artifact)"></button>
+ <button class="table-delete-btn" data-tests-id="delete_{{artifact.artifactDisplayName}}"
+ data-ng-if="!isViewMode() && !artifact.isHEAT() && !artifact.isThirdParty() && !isLicenseArtifact(artifact)" data-ng-click="delete(artifact)"> </button>
+ <button class="table-download-btn" download-artifact data-tests-id="download_{{artifact.artifactDisplayName}}"
+ data-ng-if="artifact.artifactDisplayName" component="component" artifact="artifact"></button>
+ <button ng-if="!isViewMode() && artifact.isHEAT()"
+ class="sprite e-sdc-small-icon-pad edit-paramtes-button"
+ data-ng-click="openEditEnvParametersModal(artifact)" type="button"
+ data-tests-id="edit-parameters-of-{{artifact.artifactDisplayName}}"></button>
+ </div>
+ </div>
+ <div data-ng-repeat-end="" class="flex-container data-row" data-ng-if="artifact.envArtifact">
+
+ <div class="table-col-general flex-item" zzdata-ng-click="!isViewMode() && addOrUpdate(artifact.envArtifact)">
+ <span>{{artifact.envArtifact.artifactDisplayName}}</span>
+ </div>
+
+ <div class="table-col-general flex-item" data-tests-id="{{artifact.envArtifact.artifactType}}">
+ {{artifact.envArtifact.artifactType}}
+ </div>
+ <div class="table-col-general flex-item" data-tests-id="{{artifact.envArtifact.timeout}}">
+ {{artifact.envArtifact.timeout? artifact.envArtifact.timeout:''}}
+ </div>
+ <div class="table-col-general flex-item" data-tests-id="artifactEnvVersion_{{artifact.artifactDisplayName}}">
+ {{artifact.envArtifact.artifactVersion}}
+ </div>
+ <div class="table-col-general flex-item text" data-tests-id="{{artifact.envArtifact.artifactUUID}}" tooltips tooltip-content="{{artifact.envArtifact.artifactUUID}}">
+ <span>{{artifact.envArtifact.artifactUUID}}</span>
+ </div>
+
+
+ <div class="table-btn-col flex-item" >
+ <button class="table-edit-btn" data-tests-id="edit_{{artifact.artifactLabel}}env"
+ data-ng-if="!isViewMode()" data-ng-click="addOrUpdate(artifact.envArtifact)"></button>
+ <button class="table-download-btn" data-tests-id="download_env_{{artifact.artifactDisplayName}}" download-artifact
+ data-ng-if="artifact.artifactName" component="component" artifact="artifact.envArtifact"></button>
+
+ </div>
+ </div>
+
+ <!--<div class="i-sdc-designer-sidebar-section-content-item-artifact-heat-env" ng-if="artifact.heatParameters.length">-->
+ <!--<span class="enabled" data-ng-bind="getEnvArtifactName(artifact)" data-ng-click="!isViewMode() && addOrUpdate(getEnvArtifact(artifact))"></span>-->
+ <!--<download-artifact class="i-sdc-designer-sidebar-section-content-item-button download-env sprite e-sdc-small-download hand" artifact="getEnvArtifact(artifact)"-->
+ <!--component="currentComponent" instance="true"-->
+ <!--data-tests-id="download"></download-artifact>-->
+ <!--</div>-->
+
+
+
+ <!-- Add artifacts buttons -->
+ <!--<button class="add-button" data-ng-repeat="artifact in artifacts track by $index"-->
+ <!--type="button"-->
+ <!--data-ng-show="!artifact.esId"-->
+ <!--data-ng-if="!viewModeOrCsarComponent()"-->
+ <!--data-ng-class="{'disabled': isDisableMode() || component.isCsarComponent()}"-->
+ <!--data-tests-id="{{artifact.artifactDisplayName}} deployment_artifact"-->
+ <!--translate="DEPLOYMENT_ARTIFACT_BUTTON_ADD_HEAT"-->
+ <!--translate-values="{'name': '{{artifact.artifactDisplayName}}'}"-->
+ <!--data-ng-click="addOrUpdate(artifact)"></button>-->
+
+ <!-- Top add button -->
+ <button class="add-button" type="button" data-ng-if="!isViewMode()" data-ng-class="{'disabled': isDisableMode()}" translate="DEPLOYMENT_ARTIFACT_BUTTON_ADD_OTHER" data-ng-click="addOrUpdate({})"></button>
+ </perfect-scrollbar>
+ </form>
+ </div>
+ </div>
+</div>
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/deployment-artifacts/deployment-artifacts.less b/catalog-ui/src/app/view-models/workspace/tabs/deployment-artifacts/deployment-artifacts.less
new file mode 100644
index 0000000000..dd3b16447c
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/deployment-artifacts/deployment-artifacts.less
@@ -0,0 +1,196 @@
+.workspace-deployment-artifact {
+ width: 93%;
+ display: inline-block;
+ .table-container-flex .table .body .data-row + div.item-opened {
+ align-items: center;
+ padding: 10px 40px 10px 30px;
+ }
+
+ .w-sdc-classic-btn {
+ float: right;
+ margin-bottom: 10px;
+ }
+
+
+ .heat-env-connect-container{
+ background-color: white;
+ position: absolute;
+ height: 70px;
+ width:20px;
+ left: 0;
+ top:0;
+ }
+ .heat-env-connect-container-view-mode{
+ background-color: @tlv_color_t;
+ }
+ .heat-env-connect{
+ border-left: 1px #848586 solid;
+ height: 50px;
+ margin-left: 10px;
+ margin-top: 10px;
+ border-top: 1px #848586 solid;
+ border-bottom: 1px #848586 solid;
+ width: 11px;
+ float: left;
+
+ }
+
+ .artifact-name{
+ width:85%;
+ }
+
+ .table-container-flex .table .body .data-row div .heat-env-connect-container{
+ border-right: none;
+ }
+
+ .i-sdc-designer-sidebar-section-content-item-file-link::before{
+ content:"";
+ background-color: white;
+ width: 12px;
+
+ }
+
+
+
+ .table {
+ height:490px;
+ margin-bottom: 0;
+ }
+
+ .parameter-description {
+ .circle(18px, @color_p);
+ content: '?';
+ line-height: 18px;
+ vertical-align: middle;
+ margin-left: 5px;
+ cursor: default;
+ display: inline-block;
+ position: absolute;
+ top: 16px;
+ }
+
+ .table-container-flex {
+
+ margin-top: 27px;
+
+ .text{
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: inline-block;
+ white-space: nowrap;
+ }
+
+ .flex-item:nth-child(1) {
+ flex-grow: 15;
+ .hand;
+ padding-left: 30px;
+ position: relative;
+ span.table-arrow {
+ margin-right: 7px;
+ }
+ .description-popover-icon{
+ float:right;
+ margin-top:6px;
+ }
+ }
+
+ .flex-item:nth-child(2) {
+ flex-grow: 6;
+ }
+
+ .flex-item:nth-child(3) {
+ flex-grow: 9;
+ }
+
+ .flex-item:nth-child(4) {
+ flex-grow: 3;
+ }
+
+ .flex-item:nth-child(5) {
+ flex-grow: 20;
+ }
+
+ .flex-item:nth-child(6) {
+ flex-grow: 5;
+
+ &.table-btn-col {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ button {
+ flex: 0 1 auto;
+ background-color: transparent;
+ border: 0;
+ margin: 0;
+ }
+ .edit-paramtes-button {
+ order: -1;
+ }
+ }
+ }
+ }
+ .w-sdc-form{
+ text-align: left;
+
+ .w-sdc-env-params{
+ border-top: 1px solid #cdcdcd;
+ margin: 25px 0 10px 0;
+ }
+
+ .i-sdc-form-textarea {
+ border: 1px solid @color_e;
+ min-height: 60px;
+ padding: 10px 13px;
+ width: 100%;
+ resize: none;
+
+ }
+
+ .w-sdc-form-item {
+ &.error {
+ .i-sdc-form-input,
+ .i-sdc-form-select,
+ .i-sdc-form-textarea {
+ border-color: @color_h;
+ outline: none;
+ box-sizing: border-box;
+ }
+ }
+ }
+
+ .i-sdc-env-form-label{
+ font-family: @font-omnes-medium;
+ color: @main_color_m;
+ overflow: hidden;
+ max-width: 450px;
+ text-overflow: ellipsis;
+ display: inline-block;
+ white-space: nowrap;
+ margin-top: 14px;
+
+ &.required::before {
+ color: #f33;
+ content: '*';
+ margin-right: 4px;
+ }
+ }
+ }
+}
+
+.parameter-description-popover.deployment-artifact-view {
+ margin-left: -10px;
+ z-index: 1040;
+ min-width: 300px;
+ .input-error {
+ .q_12_m;
+ }
+ .error textarea{
+ border-color: @main_color_g;
+ color: @color_h;
+ outline: none;
+ }
+ .popover-content textarea {
+ width:100%;
+ }
+}
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/deployment/deployment-view-model.ts b/catalog-ui/src/app/view-models/workspace/tabs/deployment/deployment-view-model.ts
new file mode 100644
index 0000000000..c94342a51e
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/deployment/deployment-view-model.ts
@@ -0,0 +1,127 @@
+'use strict';
+import {IWorkspaceViewModelScope} from "app/view-models/workspace/workspace-view-model";
+import {ComponentFactory, MenuHandler, ChangeLifecycleStateHandler, ModalsHandler} from "app/utils";
+import {LeftPaletteLoaderService, CacheService, SharingService} from "app/services";
+import {Component, IAppMenu, Tab, ComponentInstance} from "app/models";
+import {GRAPH_EVENTS} from "../../../../utils/constants";
+import {ComponentGenericResponse} from "../../../../ng2/services/responses/component-generic-response";
+import {EventListenerService} from "../../../../services/event-listener-service";
+import {ComponentServiceNg2} from "../../../../ng2/services/component-services/component.service";
+
+export interface IDeploymentViewModelScope extends IWorkspaceViewModelScope {
+
+ currentComponent:Component;
+ selectedComponent:Component;
+ isLoading:boolean;
+ sharingService:SharingService;
+ sdcMenu:IAppMenu;
+ version:string;
+ isViewOnly:boolean;
+ tabs:Array<Tab>;
+ selectedTab: Tab;
+ isComponentInstanceSelected():boolean;
+ updateSelectedComponent():void
+ openUpdateModal();
+ deleteSelectedComponentInstance():void;
+ onBackgroundClick():void;
+ setSelectedInstance(componentInstance:ComponentInstance):void;
+ printScreen():void;
+
+}
+
+export class DeploymentViewModel {
+
+ static '$inject' = [
+ '$scope',
+ '$templateCache',
+ 'sdcMenu',
+ 'MenuHandler',
+ '$state',
+ 'Sdc.Services.SharingService',
+ '$filter',
+ 'Sdc.Services.CacheService',
+ 'ComponentFactory',
+ 'ChangeLifecycleStateHandler',
+ 'LeftPaletteLoaderService',
+ 'ModalsHandler',
+ 'EventListenerService',
+ 'ComponentServiceNg2'
+ ];
+
+ constructor(private $scope:IDeploymentViewModelScope,
+ private $templateCache:ng.ITemplateCacheService,
+ private sdcMenu:IAppMenu,
+ private MenuHandler:MenuHandler,
+ private $state:ng.ui.IStateService,
+ private sharingService:SharingService,
+ private $filter:ng.IFilterService,
+ private cacheService:CacheService,
+ private ComponentFactory:ComponentFactory,
+ private ChangeLifecycleStateHandler:ChangeLifecycleStateHandler,
+ private LeftPaletteLoaderService:LeftPaletteLoaderService,
+ private ModalsHandler:ModalsHandler,
+ private eventListenerService: EventListenerService,
+ private ComponentServiceNg2: ComponentServiceNg2) {
+
+ this.$scope.setValidState(true);
+ this.initScope();
+ this.initGraphData();
+ this.$scope.updateSelectedMenuItem();
+ }
+
+
+ private initComponent = ():void => {
+
+ this.$scope.currentComponent = this.$scope.component;
+ this.$scope.selectedComponent = this.$scope.currentComponent;
+ this.updateUuidMap();
+ this.$scope.isViewOnly = this.$scope.isViewMode();
+ };
+
+
+ private updateUuidMap = ():void => {
+ /**
+ * In case user press F5, the page is refreshed and this.sharingService.currentEntity will be undefined,
+ * but after loadService or loadResource this.sharingService.currentEntity will be defined.
+ * Need to update the uuidMap with the new resource or service.
+ */
+ this.sharingService.addUuidValue(this.$scope.currentComponent.uniqueId, this.$scope.currentComponent.uuid);
+ };
+
+ private initRightTabs = ()=> {
+ if (this.$scope.currentComponent.groups) {
+ this.$templateCache.put("hierarchy-view.html", require('app/view-models/tabs/hierarchy/hierarchy-view.html'));
+ let hierarchyTab = new Tab("hierarchy-view.html", 'Sdc.ViewModels.HierarchyViewModel', 'hierarchy', this.$scope.isViewMode(), this.$scope.currentComponent, 'hierarchy');
+ this.$scope.tabs.push(hierarchyTab)
+ }
+ }
+
+ private initGraphData = ():void => {
+ if(!this.$scope.component.componentInstances || !this.$scope.component.componentInstancesRelations || !this.$scope.component.groups) {
+ this.ComponentServiceNg2.getDeploymentGraphData(this.$scope.component).subscribe((response:ComponentGenericResponse) => {
+ this.$scope.component.componentInstances = response.componentInstances;
+ this.$scope.component.componentInstancesRelations = response.componentInstancesRelations;
+ this.$scope.component.groups = response.groups;
+ this.$scope.isLoading = false;
+ this.initComponent();
+ this.initRightTabs();
+ this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_DEPLOYMENT_GRAPH_DATA_LOADED);
+ this.$scope.selectedTab = this.$scope.tabs[0];
+ });
+ } else {
+ this.$scope.isLoading = false;
+ this.initRightTabs();
+ this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_DEPLOYMENT_GRAPH_DATA_LOADED);
+
+ }
+ };
+
+ private initScope = ():void => {
+ this.$scope.isLoading = true;
+ this.$scope.sharingService = this.sharingService;
+ this.$scope.sdcMenu = this.sdcMenu;
+ this.$scope.version = this.cacheService.get('version');
+ this.initComponent();
+ this.$scope.tabs = Array<Tab>();
+ }
+}
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/deployment/deployment-view.html b/catalog-ui/src/app/view-models/workspace/tabs/deployment/deployment-view.html
new file mode 100644
index 0000000000..f8b5f23a25
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/deployment/deployment-view.html
@@ -0,0 +1,10 @@
+<div class="deployment-view">
+ <loader display="isLoading"></loader>
+ <div class="w-sdc-deployment-canvas" data-ng-class="{sidebaractive: displayDesignerRightSidebar}">
+ <deployment-graph component="currentComponent" is-view-only="isViewOnly"></deployment-graph>
+ </div>
+
+ <div class="w-sdc-deployment-right-bar">
+ <sdc-tabs tabs="tabs" is-view-only="isViewOnly" selected-tab="selectedTab"></sdc-tabs>
+ </div>
+</div>
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/deployment/deployment.less b/catalog-ui/src/app/view-models/workspace/tabs/deployment/deployment.less
new file mode 100644
index 0000000000..4c548c7331
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/deployment/deployment.less
@@ -0,0 +1,33 @@
+.deployment-view {
+
+ display: inline-block;
+ text-align: left;
+ align-items: left;
+ padding: 0;
+ width: 100%;
+ height: 100%;
+
+ .w-sdc-deployment-canvas {
+ .noselect;
+ .bg_c;
+ bottom: 0;
+ width: 100%;
+ height: 100%;
+
+ .view-mode{
+ background-color: #f8f8f8;
+ border:0;
+ }
+ }
+
+ .w-sdc-deployment-right-bar {
+
+ .noselect;
+ bottom: 0;
+ position: absolute;
+ right: 0px;
+ transition: right 0.2s;
+ z-index: 10000;
+ top: @action_nav_height;
+ }
+}
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/distribution/disribution-status-modal/disribution-status-modal-view-model.ts b/catalog-ui/src/app/view-models/workspace/tabs/distribution/disribution-status-modal/disribution-status-modal-view-model.ts
new file mode 100644
index 0000000000..ce2e0169bf
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/distribution/disribution-status-modal/disribution-status-modal-view-model.ts
@@ -0,0 +1,83 @@
+'use strict';
+import {Distribution, DistributionComponent, ExportExcel} from "app/models";
+
+interface IDistributionStatusModalViewModelScope {
+ distribution:Distribution;
+ status:string;
+ getStatusCount(distributionComponent:Array<DistributionComponent>):any;
+ getUrlName(url:string):string;
+ modalDitributionStatus:ng.ui.bootstrap.IModalServiceInstance;
+ footerButtons:Array<any>;
+ //exportExcelData:ExportExcel;
+ close():void;
+ initDataForExportExcel():ExportExcel;
+}
+
+export class DistributionStatusModalViewModel {
+
+ static '$inject' = ['$scope', '$uibModalInstance', 'data', '$filter'];
+
+ constructor(private $scope:IDistributionStatusModalViewModelScope,
+ private $uibModalInstance:ng.ui.bootstrap.IModalServiceInstance,
+ private data:any,
+ private $filter:ng.IFilterService) {
+ this.initScope();
+ }
+
+ private generateMetaDataForExportExcel = ():Array<string>=> {
+ let metaData = [];
+ metaData[0] = 'Name:' + this.data.component.name + '| UUID:' + this.data.component.uuid + '| Invariant UUID:' + this.data.component.invariantUUID;
+ metaData[1] = 'Distribution ID:' + this.$scope.distribution.distributionID +
+ '| USER ID:' + this.$scope.distribution.userId +
+ '| Time[UTC]:' + this.$filter('date')(this.$scope.distribution.timestamp, 'MM/dd/yyyy h:mma', 'UTC') +
+ '| Status:' + this.$scope.distribution.deployementStatus;
+ return metaData;
+ };
+
+ private generateDataObjectForExportExcel = ():any=> {
+ let correctFormatDataObj = [];
+ _.each(this.$scope.distribution.distributionComponents, (dComponent:DistributionComponent) => {
+ if (dComponent.status == this.$scope.status) {
+ correctFormatDataObj.push({
+ 'omfComponentID': dComponent.omfComponentID,
+ 'artiFactName': this.$scope.getUrlName(dComponent.url),
+ 'url': dComponent.url,
+ 'timestamp': this.$filter('date')(dComponent.timestamp, 'MM/dd/yyyy h:mma', 'UTC'),
+ 'status': dComponent.status
+ });
+ }
+ });
+ return correctFormatDataObj;
+ };
+
+ private initScope = ():void => {
+ this.$scope.distribution = this.data.distribution;
+ this.$scope.status = this.data.status;
+ this.$scope.modalDitributionStatus = this.$uibModalInstance;
+
+
+ this.$scope.getUrlName = (url:string):string => {
+ let urlName:string = _.last(url.split('/'));
+ return urlName;
+ };
+
+ this.$scope.initDataForExportExcel = ():ExportExcel => {
+ let exportExcelData = new ExportExcel();
+ exportExcelData.fileName = this.$scope.status;
+ exportExcelData.groupByField = "omfComponentID";
+ exportExcelData.tableHeaders = ["Component ID", "Artifact Name", "URL", "Time(UTC)", "Status"];
+ exportExcelData.metaData = this.generateMetaDataForExportExcel();
+ exportExcelData.dataObj = this.generateDataObjectForExportExcel();
+ return exportExcelData;
+ };
+
+ this.$scope.close = ():void => {
+ this.$uibModalInstance.close();
+ };
+
+ this.$scope.footerButtons = [
+ {'name': 'Close', 'css': 'blue', 'callback': this.$scope.close}
+ ];
+
+ };
+}
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/distribution/disribution-status-modal/disribution-status-modal-view.html b/catalog-ui/src/app/view-models/workspace/tabs/distribution/disribution-status-modal/disribution-status-modal-view.html
new file mode 100644
index 0000000000..3367193fc7
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/distribution/disribution-status-modal/disribution-status-modal-view.html
@@ -0,0 +1,130 @@
+<sdc-modal modal="modalDitributionStatus" type="classic" class="w-sdc-classic-top-line-modal" buttons="footerButtons" header="Distribution by Status" show-close-button="true">
+
+ <div class="w-sdc-distribution-view">
+ <div class="w-sdc-distribution-view-header">
+
+ </div>
+
+ <div class="actions-buttons">
+ <json-export-excel init-export-excel-data="initDataForExportExcel()">
+ </json-export-excel>
+ </div>
+
+ <perfect-scrollbar include-padding="true" class="w-sdc-distribution-view-content">
+ <div class="w-sdc-distribution-view-content-section w-sdc-distribute-parent-block">
+ <ul>
+ <li class="w-sdc-distribute-parent-block" >
+ <div class="w-sdc-distribute-row w-sdc-distribute-row-extends extends">
+
+ <div class="w-sdc-distribute-row-content">
+ <div class="w-sdc-distribute-content">
+ <div class="title-section item-1">
+ <div class="title">Distribution ID</div>
+ <div data-ng-bind="distribution.distributionID"></div>
+ </div>
+ <div class="title-section item-2">
+ <div class="title" translate="DISTRIBUTION_VIEW_TITLE_USER_ID"></div>
+ <div data-ng-bind="distribution.userId"></div>
+ </div>
+ <div class="title-section item-3">
+ <div class="title">Time[UTC]:</div>
+ <div
+ data-ng-bind="distribution.timestamp | stringToDateFilter | date: 'MM/dd/yyyy h:mma':'UTC'"></div>
+ </div>
+ <div class="title-section item-4">
+ <span class="sprite-new status-icon" data-ng-class="distribution.deployementStatus"></span>
+ <span class="sprite-new" data-ng-bind="distribution.deployementStatus"></span>
+ </div>
+ </div>
+ <div class="w-sdc-distribute-status-block" data-ng-show="distribution.statusCount">
+ <div class="status-item-1">Status: {{status}} <span data-ng-bind="(distribution.distributionComponents | filter:status:true).length"
+ class="blue-font"></span></div>
+
+ </div>
+ </div>
+ </div>
+
+ <ul class="w-sdc-distribute-components-block disable-hover">
+ <li data-ng-repeat="(omfComponentID,omfComponentList) in distribution.distributionComponents | orderBy: '-timestamp' | filter:status:true | groupBy:'omfComponentID'"
+ class="disable-hover">
+ <div class="w-sdc-distribute-row omf-component-row w-sdc-distribute-row-extends "
+ data-ng-class="{'extends': omfComponentListExtends}">
+ <div class="w-sdc-distribution-arrow-btn" data-ng-click="omfComponentListExtends=!omfComponentListExtends"
+ ng-class="{'extends': omfComponentListExtends}"
+ data-ng-init="omfComponentListExtends=false"
+ ></div>
+ <div class="w-sdc-distribute-status-block">
+ <div class="status-item-1">{{omfComponentID}} <span class="blue-font">{{omfComponentList.length}}</span>
+ </div>
+ </div>
+ </div>
+ <div data-ng-show="omfComponentListExtends"
+ class="w-sdc-distribute-omfComponent-block disable-hover">
+ <div class="w-sdc-distribute-row-extends disable-hover">
+ <div class="disable-hover">
+ <div class="w-sdc-distribute-row omfComponent-table-head">
+ <div class="title item-1">Component ID</div>
+ <div class="title item-2">Artifact Name</div>
+ <div class="title item-3">URL</div>
+ <div class="title item-4">Time(UTC)</div>
+ <div class="title item-5">Status</div>
+ </div>
+
+ <div class="w-sdc-distribute-row omfComponent-table-row"
+ data-ng-repeat-start="(url,urlList) in omfComponentList | orderBy: '-timestamp' | groupBy:'url'"
+ data-ng-class="urlListExtends?'extends row-{{$index}}':'row-{{$index}}'" >
+ <div class="w-sdc-distribute-cell item-1">
+ <div class="w-sdc-distribution-arrow-btn" data-ng-click="urlListExtends=!urlListExtends"
+ data-ng-class="{'extends': urlListExtends}"
+ data-ng-init="urlListListExtends=false"
+ ></div>
+ {{urlList[0].omfComponentID}}
+ </div>
+ <div class="w-sdc-distribute-cell item-2" sdc-smart-tooltip>
+ {{getUrlName(urlList[0].url)}}
+ </div>
+ <div class="w-sdc-distribute-cell item-3 disable-hover">
+ <div sdc-smart-tooltip class="distribution-url">{{urlList[0].url}}</div>
+ <div sdc-smart-tooltip title="Copy url" clipboard text="urlList[0].url"
+ class="sprite-new link-btn copy-link disable-hover"></div>
+ </div>
+ <div class="w-sdc-distribute-cell item-4"><span
+ data-ng-bind="urlList[0].timestamp | date: 'MM/dd/yyyy h:mma':'UTC'"></span>
+ </div>
+ <div class="w-sdc-distribute-cell item-5">{{urlList[0].status}}</div>
+ </div>
+
+
+ <div data-ng-repeat-end data-ng-show="urlListExtends" class="disable-hover">
+ <div class="w-sdc-distribute-row extends disable-hover">
+ <ul data-ng-show="urlListExtends"
+ class="w-sdc-distribute-url-block disable-hover">
+ <li data-ng-repeat="distributionComponent in urlList | orderBy: '-timestamp'"
+ class="disable-hover">
+ <span
+ data-ng-bind="distributionComponent.timestamp | date: 'MM/dd/yyyy h:mma':'UTC'"
+ class="disable-hover"></span>
+ <span
+ class="disable-hover">{{distributionComponent.status}}</span>
+ <span
+ class="disable-hover reason" data-ng-show="distributionComponent.status == 'NOT_NOTIFIED'">Reason: Component has determined artifact is not needed.</span>
+ <span
+ class="disable-hover reason" data-ng-show="distributionComponent.errorReason">Reason: {{distributionComponent.errorReason}}</span>
+ </li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </li>
+ </ul>
+ </li>
+ </ul>
+ </div>
+
+ </perfect-scrollbar>
+ </div>
+
+
+</sdc-modal>
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/distribution/disribution-status-modal/disribution-status-modal.less b/catalog-ui/src/app/view-models/workspace/tabs/distribution/disribution-status-modal/disribution-status-modal.less
new file mode 100644
index 0000000000..d167083a2b
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/distribution/disribution-status-modal/disribution-status-modal.less
@@ -0,0 +1,40 @@
+.w-sdc-classic-top-line-modal {
+
+ .w-sdc-modal-head {
+ // border-bottom: none;
+ }
+ .w-sdc-distribution-view {
+ .actions-buttons {
+ height: 29px;
+ padding: 0 25px 0 0px;
+ span{
+ float: right;
+ }
+ }
+
+ .w-sdc-distribution-view-content {
+ height: 500px;
+ }
+
+ .w-sdc-distribution-view-content-section {
+
+ .w-sdc-distribute-parent-block {
+ .w-sdc-distribute-components-block {
+
+ .omf-component-row {
+ .w-sdc-distribute-status-block {
+ margin-left: 0;
+ }
+
+ }
+ div {
+ padding-left: 0;
+ }
+ }
+
+ }
+
+ }
+ }
+}
+
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/distribution/distribution-view-model.ts b/catalog-ui/src/app/view-models/workspace/tabs/distribution/distribution-view-model.ts
new file mode 100644
index 0000000000..002b16f63f
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/distribution/distribution-view-model.ts
@@ -0,0 +1,111 @@
+'use strict';
+import {Distribution, DistributionComponent, Service} from "app/models";
+import {ModalsHandler, Dictionary} from "app/utils";
+import {IWorkspaceViewModelScope} from "app/view-models/workspace/workspace-view-model";
+
+interface IDistributionViewModel extends IWorkspaceViewModelScope {
+ modalDistribution:ng.ui.bootstrap.IModalServiceInstance;
+ service:Service;
+ distributions:Array<Distribution>;
+ showComponents(distribution:Distribution):void;
+ markAsDeployed(distribution:Distribution):void;
+ getStatusCount(distributionComponent:Array<DistributionComponent>):any;
+ initDistributions():void;
+ getUrlName(url:string):string;
+ close():void;
+ openDisributionStatusModal:Function;
+}
+
+export class DistributionViewModel {
+
+ static '$inject' = [
+ '$scope',
+ 'ModalsHandler'
+
+ ];
+
+ constructor(private $scope:IDistributionViewModel,
+ private ModalsHandler:ModalsHandler) {
+ this.initScope();
+ this.$scope.setValidState(true);
+ this.$scope.updateSelectedMenuItem();
+ }
+
+ private initScope = ():void => {
+ this.$scope.service = <Service>this.$scope.component;
+
+
+ // Open Distribution status modal
+ this.$scope.openDisributionStatusModal = (distribution:Distribution, status:string):void => {
+ this.ModalsHandler.openDistributionStatusModal(distribution, status, this.$scope.component).then(()=> {
+ // OK
+ }, ()=> {
+ // ERROR
+ });
+ };
+
+
+ this.$scope.showComponents = (distribution:Distribution):void => {
+ let onError = (response) => {
+ console.info('onError showComponents', response);
+ };
+ let onSuccess = (distributionComponents:Array<DistributionComponent>) => {
+ distribution.distributionComponents = distributionComponents;
+ distribution.statusCount = this.$scope.getStatusCount(distribution.distributionComponents);
+ // distribution.components = this.aggregateDistributionComponent(distributionComponents);;
+ };
+ this.$scope.service.getDistributionsComponent(distribution.distributionID).then(onSuccess, onError);
+ };
+
+ this.$scope.getStatusCount = (distributionComponent:Array<DistributionComponent>):any => {
+ return _.countBy(distributionComponent, 'status')
+ };
+
+ this.$scope.getUrlName = (url:string):string => {
+ let urlName:string = _.last(url.split('/'));
+ return urlName;
+ };
+
+ this.$scope.markAsDeployed = (distribution:Distribution):void => {
+ let onError = (response) => {
+ console.info('onError markAsDeployed', response);
+ };
+ let onSuccess = (result:any) => {
+ distribution.deployementStatus = 'Deployed';
+ };
+ this.$scope.service.markAsDeployed(distribution.distributionID).then(onSuccess, onError);
+
+ };
+
+ this.$scope.initDistributions = ():void => {
+ let onError = (response) => {
+ console.info('onError initDistributions', response);
+ };
+ let onSuccess = (distributions:Array<Distribution>) => {
+ this.$scope.distributions = distributions;
+ };
+ this.$scope.service.getDistributionsList().then(onSuccess, onError);
+ };
+
+ this.$scope.initDistributions();
+
+ };
+
+
+ private aggregateDistributionComponent = (distributionComponents:Array<DistributionComponent>):any => {
+ let aggregateDistributions:Dictionary<string,Dictionary<string,Array<DistributionComponent>>> = new Dictionary<string,Dictionary<string,Array<DistributionComponent>>>();
+ let tempAggregateDistributions:any = _.groupBy(distributionComponents, 'omfComponentID');
+ let aa = new Dictionary<string,Array<DistributionComponent>>();
+
+ let tempAggregate:any;
+ _.forEach(tempAggregateDistributions, (distributionComponents:Array<DistributionComponent>, omfComponentID:string)=> {
+
+ let urls:any = _.groupBy(distributionComponents, 'url');
+ aggregateDistributions.setValue(omfComponentID, urls);
+ // aggregateDistributions[omfComponentID] = ;
+
+ });
+ console.log(aggregateDistributions);
+ return aggregateDistributions;
+ };
+}
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/distribution/distribution-view.html b/catalog-ui/src/app/view-models/workspace/tabs/distribution/distribution-view.html
new file mode 100644
index 0000000000..710336af15
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/distribution/distribution-view.html
@@ -0,0 +1,181 @@
+<div class="w-sdc-distribution-view">
+ <div class="w-sdc-distribution-view-header">
+ <div class="w-sdc-distribution-view-title">DISTRIBUTION <span data-ng-bind="'[' + distributions.length +']'"
+ class="blue-font"></span></div>
+ <div class="header-spacer"></div>
+ <div class="top-search">
+ <input type="text"
+ class="search-text"
+ data-tests-id="searchTextbox"
+ 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="sprite-new refresh-btn" data-tests-id="refreshButton" data-ng-click="initDistributions()" sdc-smart-tooltip=""
+ title="Refresh"></div>
+ </div>
+
+
+ <perfect-scrollbar include-padding="true" class="w-sdc-distribution-view-content">
+ <div class="w-sdc-distribution-view-content-section" data-tests-id="ditributionTable">
+ <ul>
+ <li data-ng-repeat="item in distributions | orderBy: '-timestamp' | filter:searchBind"
+ data-ng-init="item.dateFormat = ( item.timestamp | stringToDateFilter | date: 'MM/dd/yyyy h:mma':'UTC' )"
+ class="w-sdc-distribute-parent-block" data-tests-id="record_{{$index}}" data-ng-class="{'extends': item.showDetails}">
+ <div class="w-sdc-distribute-row w-sdc-distribute-row-extends"
+ data-ng-class="{'extends': item.showDetails && item.distributionComponents.length}">
+ <div class="w-sdc-distribution-arrow-btn" data-tests-id="ShowRecordButton_{{$index}}" data-ng-click="showComponents(item); item.showDetails=!item.showDetails"
+ data-ng-class="{'extends': item.showDetails}"
+ ></div>
+ <div class="w-sdc-distribute-row-content">
+ <div class="w-sdc-distribute-content">
+ <div class="title-section item-1">
+ <div class="title">Distribution ID</div>
+ <div data-ng-bind="item.distributionID"></div>
+ </div>
+ <div class="title-section item-2">
+ <div class="title" translate="DISTRIBUTION_VIEW_TITLE_USER_ID"></div>
+ <div data-ng-bind="item.userId"></div>
+ </div>
+ <div class="title-section item-3">
+ <div class="title">Time[UTC]:</div>
+ <div
+ data-ng-bind="item.dateFormat"></div>
+ </div>
+ <div class="title-section item-4">
+ <span class="sprite-new status-icon" data-ng-class="item.deployementStatus"></span>
+ <span class="sprite-new" data-ng-bind="item.deployementStatus"></span>
+ </div>
+ <div>
+ <div class="sprite-new distribution-bth item-5"
+ data-ng-class="{'disable':item.deployementStatus==='Deployed'}"
+ data-ng-click="(item.deployementStatus==='Deployed') || markAsDeployed(item)"></div>
+ </div>
+ </div>
+ <div class="w-sdc-distribute-status-block" data-ng-if="item.statusCount">
+ <div class="status-item-1">Total Artifacts:<span data-ng-bind="(item.statusCount.NOT_NOTIFIED || 0) + (item.statusCount.NOTIFIED || 0) "
+ class="blue-font" data-tests-id="totalArtifacts_{{$index}}"></span></div>
+ <div class="status-item-2 " ><sapn class="link" data-ng-click="openDisributionStatusModal(item,'NOTIFIED')">Notified:</sapn><span
+ data-ng-bind="item.statusCount.NOTIFIED || 0" class="blue-font" data-tests-id="notified_{{$index}}"></span></div>
+
+ <div class="status-item-3 link" ><sapn class="link" data-ng-click="openDisributionStatusModal(item,'DOWNLOAD_OK')">Downloaded:</sapn><span
+ data-ng-bind="item.statusCount.DOWNLOAD_OK || 0" class="blue-font" data-tests-id="downloaded_{{$index}}"></span></div>
+
+ <div class="status-item-4 link"><sapn class="link" data-ng-click="openDisributionStatusModal(item,'DEPLOY_OK')">Deployed:</sapn><span
+ data-ng-bind="item.statusCount.DEPLOY_OK || 0" class="blue-font" data-tests-id="deployed_{{$index}}" ></span><span
+ data-ng-class="{'deployed':(item.statusCount.DEPLOY_OK > 0)}"></span></div>
+ <div class="status-item-5 link" ><sapn class="link" data-ng-click="openDisributionStatusModal(item,'NOT_NOTIFIED')">Not Notified:</sapn><span
+ data-ng-bind="item.statusCount.NOT_NOTIFIED || 0" class="blue-font" data-tests-id="NotNotified_{{$index}}"></span></div>
+ <div class="status-item-6"><sapn class="link" data-ng-click="openDisributionStatusModal(item,'DEPLOY_ERROR')" >Deploy Errors:</sapn><span
+ data-ng-bind="item.statusCount.DEPLOY_ERROR || 0" class="red-font "></span><span
+ data-ng-class="{'error':(item.statusCount.DEPLOY_ERROR > 0)}" data-tests-id="errors_{{$index}}"></span></div>
+ <div class="status-item-7"><sapn class="link" data-ng-click="openDisributionStatusModal(item,'DOWNLOAD_ERROR')" >Download Errors:</sapn><span
+ data-ng-bind="item.statusCount.DOWNLOAD_ERROR || 0" class="red-font "></span><span
+ data-ng-class="{'error':(item.statusCount.DOWNLOAD_ERROR > 0)}" data-tests-id="errors_{{$index}}"></span></div>
+ </div>
+ </div>
+ </div>
+
+ <ul data-ng-if="item.showDetails && item.distributionComponents.length"
+ class="w-sdc-distribute-components-block disable-hover">
+
+ <li data-ng-repeat="(omfComponentID,omfComponentList) in ::item.distributionComponents | orderBy: '-timestamp' | filter:searchBind | groupBy:'omfComponentID' "
+ class="disable-hover"
+ data-ng-init="statusCount = getStatusCount(omfComponentList);">
+ <div class="w-sdc-distribute-row omf-component-row w-sdc-distribute-row-extends"
+ data-ng-class="{'extends': omfComponentListExtends}">
+ <div class="w-sdc-distribution-arrow-btn" data-ng-click="omfComponentListExtends=!omfComponentListExtends"
+ ng-class="{'extends': omfComponentListExtends}"
+ data-ng-init="omfComponentListExtends=false"
+ ></div>
+ <div class="w-sdc-distribute-status-block">
+ <div class="status-item-1">{{omfComponentID}} <span class="blue-font">{{(statusCount.NOT_NOTIFIED || 0) + (statusCount.NOTIFIED || 0) }}</span>
+ </div>
+ <div class="status-item-2">Notified:<span data-ng-bind="statusCount.NOTIFIED || 0"
+ class="blue-font"></span></div>
+ <div class="status-item-3">Downloaded:<span
+ data-ng-bind="statusCount.DOWNLOAD_OK || 0" class="blue-font"></span></div>
+ <div class="status-item-4">Deployed:<span data-ng-bind="statusCount.DEPLOY_OK || 0"
+ class="blue-font"></span><span
+ data-ng-class="{'deployed':(statusCount.DEPLOY_OK > 0)}"></span></div>
+ <div class="status-item-5">Not Notified:<span
+ data-ng-bind="statusCount.NOT_NOTIFIED || 0" class="blue-font"></span></div>
+ <div class="status-item-6">Deploy Errors:<span
+ data-ng-bind="statusCount.DEPLOY_ERROR || 0" class="red-font"></span><span
+ data-ng-class="{'error':(statusCount.DEPLOY_ERROR > 0)}"></span></div>
+ <div class="status-item-7">Download Errors:<span
+ data-ng-bind="statusCount.DOWNLOAD_ERROR || 0" class="red-font"></span><span
+ data-ng-class="{'error':(statusCount.DOWNLOAD_ERROR > 0)}"></span></div>
+ </div>
+ </div>
+ <div data-ng-if="omfComponentListExtends"
+ class="w-sdc-distribute-omfComponent-block disable-hover">
+ <div class="w-sdc-distribute-row-extends disable-hover">
+ <div class="disable-hover">
+ <div class="w-sdc-distribute-row omfComponent-table-head">
+ <div class="title item-1">Component ID</div>
+ <div class="title item-2">Artifact Name</div>
+ <div class="title item-3">URL</div>
+ <div class="title item-4">Time(UTC)</div>
+ <div class="title item-5">Status</div>
+ </div>
+
+ <div class="w-sdc-distribute-row omfComponent-table-row"
+ data-ng-repeat-start="(url,urlList) in ::omfComponentList | orderBy: '-timestamp' | groupBy:'url'"
+ data-ng-class="urlListExtends?'extends row-{{$index}}':'row-{{$index}}'">
+ <div class="w-sdc-distribute-cell item-1" sdc-smart-tooltip>
+ <div class="w-sdc-distribution-arrow-btn" data-ng-click="urlListExtends=!urlListExtends"
+ data-ng-class="{'extends': urlListExtends}"
+ data-ng-init="urlListListExtends=false;urlList[0].displayUrl=getUrlName(urlList[0].url)"
+ ></div>
+ {{urlList[0].omfComponentID}}
+ </div>
+ <div class="w-sdc-distribute-cell item-2" sdc-smart-tooltip>
+ {{urlList[0].displayUrl}}
+ </div>
+ <div class="w-sdc-distribute-cell item-3 disable-hover">
+ <div sdc-smart-tooltip class="distribution-url">{{urlList[0].url}}</div>
+ <div sdc-smart-tooltip title="Copy url" clipboard text="urlList[0].url"
+ class="sprite-new link-btn copy-link disable-hover"></div>
+ </div>
+ <div class="w-sdc-distribute-cell item-4"><span
+ data-ng-bind="urlList[0].timestamp | date: 'MM/dd/yyyy h:mma':'UTC'"></span>
+ </div>
+ <div class="w-sdc-distribute-cell item-5" sdc-smart-tooltip>
+ {{urlList[0].status}}
+ </div>
+ </div>
+
+
+ <div data-ng-repeat-end data-ng-if="urlListExtends" class="disable-hover" >
+ <div class="w-sdc-distribute-row extends disable-hover">
+ <ul data-ng-if="urlListExtends"
+ class="w-sdc-distribute-url-block disable-hover">
+ <li data-ng-repeat="distributionComponent in ::urlList | orderBy: '-timestamp'"
+ class="disable-hover">
+ <span
+ data-ng-bind="distributionComponent.timestamp | date: 'MM/dd/yyyy h:mma':'UTC'"
+ class="disable-hover"></span>
+ <span
+ class="disable-hover">{{distributionComponent.status}}</span>
+ <span
+ class="disable-hover reason" data-ng-if="distributionComponent.status == 'NOT_NOTIFIED'">Reason: Component has determined artifact is not needed.</span>
+ <span
+ class="disable-hover reason" data-ng-if="distributionComponent.errorReason">Reason: {{distributionComponent.errorReason}}</span>
+ </li>
+ </ul>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </li>
+ </ul>
+ </li>
+ </ul>
+ </div>
+
+ </perfect-scrollbar>
+</div>
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/distribution/distribution.less b/catalog-ui/src/app/view-models/workspace/tabs/distribution/distribution.less
new file mode 100644
index 0000000000..ee1f7ed2d6
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/distribution/distribution.less
@@ -0,0 +1,362 @@
+
+.w-sdc-distribution-view {
+ text-align: left;
+
+ .g_1;
+ min-height: 500px;
+
+ .w-sdc-distribution-view-distributed-green-text {
+ .l_9;
+ .bold;
+ }
+ .w-sdc-distribution-view-distributed-error-red-text {
+ .h_9;
+ .bold;
+ }
+
+ .bg_c;
+ vertical-align: top;
+ padding: 30px 10px;
+ width: 100%;
+
+ .w-sdc-distribution-view-header {
+ display: flex;
+ -webkit-justify-content: space-between;
+ margin: 0 25px 5px 40px;
+
+ .header-spacer {
+ flex-grow: 5;
+ }
+ }
+
+ .top-search {
+ position: relative;
+ input {
+ &.search-text {
+ height: 26px;
+ line-height: 26px;
+ margin: 0 18px 4px 20px;
+ padding-right: 25px;
+ }
+
+ }
+ .magnification {
+ top: 8px;
+ right: 25px;
+ }
+ }
+
+ .w-sdc-distribution-view-content {
+ .perfect-scrollbar;
+ padding: 0 25px 0 0px;
+ margin-bottom: 25px;
+ height: 700px;
+ overflow: hidden;
+ position: relative;
+
+ }
+
+ .w-sdc-distribution-view-title {
+ .s_14_r;
+
+ line-height: 30px;
+
+ span {
+ padding-left: 5px;
+ }
+ }
+
+ .blue-font {
+ .a_14_m;
+
+ }
+
+ .red-font {
+ .q_14_m;
+ }
+
+ .w-sdc-distribution-view-block {
+ div {
+ display: inline-block;
+ }
+ }
+
+ .w-sdc-distribution-view-content-section {
+ ul {
+ list-style-type: none;
+ }
+
+ .distribution-bth {
+ .hand;
+ &.disabled {
+ cursor: none;
+ }
+ }
+
+ .copy-link {
+ padding-right: 19px;
+ margin-left: 8px;
+ cursor: pointer;
+
+ }
+
+ .w-sdc-distribute-row-extends {
+ border-Left: solid 4px transparent;
+ &.extends {
+ border-left: solid 4px @main_color_c;
+ border-bottom: 1px solid @border_color_f;
+ margin-bottom: 10px;
+ }
+ }
+ .w-sdc-distribute-parent-block {
+ border: 1px solid @main_color_o;;
+ width: 100%;
+ margin-bottom: 6px;
+
+ .status-icon {
+ vertical-align: middle;
+ margin-bottom: 4px;
+ }
+
+ &.extends {
+ background-color: @tlv_color_t;
+ }
+
+ :not(.disable-hover):hover {
+ background-color: @tlv_color_u;
+ }
+
+ .title-section {
+ display: inline-block;
+ margin-right: 10px;
+ flex-basis: 0;
+ }
+
+ .title {
+ .l_12_m;
+ font-weight: bold;
+ }
+ .w-sdc-distribute-content {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-left: 10px;
+ }
+
+ .w-sdc-distribution-arrow-btn {
+ .sprite-new;
+ .arrow-up-small;
+ margin: 0 6px;
+ display: inline-table;
+ }
+ .extends.w-sdc-distribution-arrow-btn {
+ -webkit-transform: rotate(180deg);
+ -ms-transform: rotate(180deg);
+ transform: rotate(180deg);
+ }
+
+ .w-sdc-distribute-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+
+ .w-sdc-distribute-row-content {
+ margin: 15px 31px 10px 0;
+ width: 100%;
+ .w-sdc-distribute-status-block {
+ border-top: solid 1px @main_color_o;
+ }
+ .item-1 {
+ flex-grow: 2;
+ }
+ .item-2 {
+ flex-grow: 1;
+ }
+ .item-3 {
+ flex-grow: 1;
+ }
+ .item-4 {
+ flex-grow: 1;
+ }
+ .item-5 {
+ flex-grow: 1;
+ }
+ }
+ }
+
+ .w-sdc-distribute-status-block {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin: 10px 5px 0 5px;
+ padding: 5px 5px 0 5px;;
+ width: 100%;
+ div {
+ border-left: 1px solid @main_color_o;
+ padding: 0 12px;
+ }
+
+ .link {
+ .a_14_m;
+ cursor: pointer;
+ &:hover{
+ text-decoration: underline;
+ .b_14_m;
+ }
+ }
+
+ span {
+ padding: 2px;
+ }
+
+ .deployed {
+ margin-left: 10px;
+ .sprite-new;
+ .success-circle-small;
+ }
+
+ .error {
+ .q_14_m;
+ margin-left: 10px;
+ .sprite-new;
+ .error-icon;
+ }
+
+ .status-item-1 {
+ border-left: 0;
+ }
+
+ .status-item-6 {
+ flex-grow: 1;
+ border-left: none;
+ text-align: right;
+ }
+ }
+
+ .w-sdc-distribute-components-block {
+ padding: 0;
+ padding-bottom: 5px;
+ list-style-type: none;
+
+ li {
+ margin: 5px 2px;
+ }
+
+ .omf-component-row {
+ border: 1px solid @border_color_f;
+ padding-left: 3px;
+ background-color: white;
+ margin: 0 30px;
+ &.extends {
+ padding-left: 0;
+ border-Left: solid 4px @main_color_c;
+
+ }
+
+ .w-sdc-distribute-status-block {
+ margin: 5px;
+ padding: 5px;
+ }
+
+ .blue-font {
+ .a_16_m;
+
+ }
+
+ &:hover {
+ background-color: @tlv_color_u;
+ }
+
+ }
+
+ }
+
+ .w-sdc-distribute-omfComponent-block {
+ background-color: white;
+ margin: 0 30px;
+ padding: 8px 10px;
+ border: 1px solid @border_color_f;
+
+ .omfComponent-table-head {
+ margin-bottom: 5px;
+ background-color: @tlv_color_u;
+ .title {
+ padding: 6px 10px;
+ border-left: 1px solid @border_color_f;
+ &:first-child {
+ border: none;
+ }
+ }
+ }
+
+ .omfComponent-table-row {
+ border-bottom: 1px solid @border_color_f;
+ &.row-0 {
+ border-top: 1px solid @border_color_f;
+ }
+ .w-sdc-distribute-cell {
+ padding: 10px;
+ border-left: 1px solid @border_color_f;
+ &:last-child {
+ border-right: 1px solid @border_color_f;
+ }
+ &.item-5 {
+ .m_14_m;
+ }
+ }
+ }
+
+ .distribution-url {
+
+ }
+
+ .w-sdc-distribute-row.extends {
+ border-Left: solid 4px @main_color_c;
+ .item-1 {
+ border: none;
+ }
+
+ }
+
+ .item-1 {
+ width: 20%;
+ }
+ .item-2 {
+ width: 20%;
+ }
+
+ .item-3 {
+ width: 24%;
+ display: flex;
+ }
+
+ .item-4 {
+ width: 18%;
+ }
+
+ .item-5 {
+ width: 18%;
+ }
+ }
+
+ .w-sdc-distribute-url-block {
+
+ padding: 10px 15px;
+ border: none;
+ border-right: 1px solid @border_color_f;
+ border-bottom: 1px solid @border_color_f;
+ width: 100%;
+ li {
+ border: none;
+ span {
+ padding-right: 30px;
+ .m_12_r;
+ }
+ }
+
+ }
+ }
+
+ }
+}
+
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/general/general-view-model.ts b/catalog-ui/src/app/view-models/workspace/tabs/general/general-view-model.ts
new file mode 100644
index 0000000000..44953985fc
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/general/general-view-model.ts
@@ -0,0 +1,349 @@
+'use strict';
+import {ModalsHandler, ValidationUtils, EVENTS, CHANGE_COMPONENT_CSAR_VERSION_FLAG, ComponentType, DEFAULT_ICON,
+ ResourceType} from "app/utils";
+import {CacheService, EventListenerService, ProgressService} from "app/services";
+import {IAppConfigurtaion, Product, IValidate, IMainCategory, Resource, ISubCategory,Service} from "app/models";
+import {IWorkspaceViewModelScope} from "app/view-models/workspace/workspace-view-model";
+
+export class Validation {
+ componentNameValidationPattern:RegExp;
+ contactIdValidationPattern:RegExp;
+ tagValidationPattern:RegExp;
+ vendorValidationPattern:RegExp;
+ commentValidationPattern:RegExp;
+ projectCodeValidationPattern:RegExp;
+}
+
+export class componentCategories {//categories field bind to this obj in order to solve this bug: DE242059
+ selectedCategory:string;
+}
+
+export interface IGeneralScope extends IWorkspaceViewModelScope {
+ validation:Validation;
+ editForm:ng.IFormController;
+ categories:Array<IMainCategory>;
+ latestCategoryId:string;
+ latestVendorName:string;
+ importedFileExtension:any;
+ isCreate:boolean;
+ isShowFileBrowse:boolean;
+ isShowOnboardingSelectionBrowse:boolean;
+ importedToscaBrowseFileText:string;
+ importCsarProgressKey:string;
+ browseFileLabel:string;
+ componentCategories:componentCategories;
+
+ onToscaFileChange():void
+ validateField(field:any):boolean;
+ validateName(isInit:boolean):void;
+ calculateUnique(mainCategory:string, subCategory:string):string; // Build unique string from main and sub category
+ onVendorNameChange(oldVendorName:string):void;
+ convertCategoryStringToOneArray(category:string, subcategory:string):Array<IMainCategory>;
+ onCategoryChange():void;
+ onEcompGeneratedNamingChange():void;
+ openOnBoardingModal():void;
+ initCategoreis():void;
+}
+
+export class GeneralViewModel {
+
+ static '$inject' = [
+ '$scope',
+ 'Sdc.Services.CacheService',
+ 'ComponentNameValidationPattern',
+ 'ContactIdValidationPattern',
+ 'TagValidationPattern',
+ 'VendorValidationPattern',
+ 'CommentValidationPattern',
+ 'ValidationUtils',
+ 'sdcConfig',
+ 'ProjectCodeValidationPattern',
+ '$state',
+ 'ModalsHandler',
+ 'EventListenerService',
+ 'Notification',
+ 'Sdc.Services.ProgressService',
+ '$interval',
+ '$filter',
+ '$timeout'
+ ];
+
+ constructor(private $scope:IGeneralScope,
+ private cacheService:CacheService,
+ private ComponentNameValidationPattern:RegExp,
+ private ContactIdValidationPattern:RegExp,
+ private TagValidationPattern:RegExp,
+ private VendorValidationPattern:RegExp,
+ private CommentValidationPattern:RegExp,
+ private ValidationUtils:ValidationUtils,
+ private sdcConfig:IAppConfigurtaion,
+ private ProjectCodeValidationPattern:RegExp,
+ private $state:ng.ui.IStateService,
+ private ModalsHandler:ModalsHandler,
+ private EventListenerService:EventListenerService,
+ private Notification:any,
+ private progressService:ProgressService,
+ protected $interval:any,
+ private $filter:ng.IFilterService,
+ private $timeout:ng.ITimeoutService) {
+
+ this.initScopeValidation();
+ this.initScopeMethods();
+ this.initScope();
+ this.$scope.updateSelectedMenuItem();
+ }
+
+
+
+
+ private initScopeValidation = ():void => {
+ this.$scope.validation = new Validation();
+ this.$scope.validation.componentNameValidationPattern = this.ComponentNameValidationPattern;
+ this.$scope.validation.contactIdValidationPattern = this.ContactIdValidationPattern;
+ this.$scope.validation.tagValidationPattern = this.TagValidationPattern;
+ this.$scope.validation.vendorValidationPattern = this.VendorValidationPattern;
+ this.$scope.validation.commentValidationPattern = this.CommentValidationPattern;
+ this.$scope.validation.projectCodeValidationPattern = this.ProjectCodeValidationPattern;
+ };
+
+ private initScope = ():void => {
+
+ // Work around to change the csar version
+ if (this.cacheService.get(CHANGE_COMPONENT_CSAR_VERSION_FLAG)) {
+ (<Resource>this.$scope.component).csarVersion = this.cacheService.get(CHANGE_COMPONENT_CSAR_VERSION_FLAG);
+ }
+
+ this.$scope.importedToscaBrowseFileText = this.$scope.component.name + " (" + (<Resource>this.$scope.component).csarVersion + ")";
+ this.$scope.importCsarProgressKey = "importCsarProgressKey";
+ this.$scope.browseFileLabel = this.$scope.component.isResource() && (<Resource>this.$scope.component).resourceType === ResourceType.VF ? "Upload file" : "Upload VFC";
+ this.$scope.progressService = this.progressService;
+ this.$scope.componentCategories = new componentCategories();
+ this.$scope.componentCategories.selectedCategory = this.$scope.component.selectedCategory;
+
+ // Workaround to short vendor name to 25 chars
+ // Amdocs send 27 chars, and the validation pattern is 25 chars.
+ if (this.$scope.component.vendorName) {
+ this.$scope.component.vendorName = this.$scope.component.vendorName.substr(0, 25);
+ }
+
+ // Init UIModel
+ this.$scope.component.tags = _.without(this.$scope.component.tags, this.$scope.component.name);
+
+ // Init categories
+ this.$scope.initCategoreis();
+
+ // Init the decision if to show file browse.
+ this.$scope.isShowFileBrowse = false;
+ if (this.$scope.component.isResource()) {
+ let resource:Resource = <Resource>this.$scope.component;
+ console.log(resource.name + ": " + resource.csarUUID);
+ if (resource.importedFile) { // Component has imported file.
+ this.$scope.isShowFileBrowse = true;
+ }
+ if (this.$scope.isEditMode() && resource.resourceType == ResourceType.VF && !resource.csarUUID) {
+ this.$scope.isShowFileBrowse = true;
+ }
+ }
+ ;
+
+ // Init the decision if to show onboarding
+ this.$scope.isShowOnboardingSelectionBrowse = false;
+ if (this.$scope.component.isResource() &&
+ this.$scope.isEditMode() &&
+ (<Resource>this.$scope.component).resourceType == ResourceType.VF &&
+ (<Resource>this.$scope.component).csarUUID) {
+ this.$scope.isShowOnboardingSelectionBrowse = true;
+ }
+
+ //init file extensions based on the file that was imported.
+ if (this.$scope.component.isResource() && (<Resource>this.$scope.component).importedFile) {
+ let fileName:string = (<Resource>this.$scope.component).importedFile.filename;
+ let fileExtension:string = fileName.split(".").pop();
+ if (this.sdcConfig.csarFileExtension.indexOf(fileExtension.toLowerCase()) !== -1) {
+ this.$scope.importedFileExtension = this.sdcConfig.csarFileExtension;
+ (<Resource>this.$scope.component).importedFile.filetype = "csar";
+ } else if (this.sdcConfig.toscaFileExtension.indexOf(fileExtension.toLowerCase()) !== -1) {
+ (<Resource>this.$scope.component).importedFile.filetype = "yaml";
+ this.$scope.importedFileExtension = this.sdcConfig.toscaFileExtension;
+ }
+ } else if (this.$scope.isEditMode() && (<Resource>this.$scope.component).resourceType === ResourceType.VF) {
+ this.$scope.importedFileExtension = this.sdcConfig.csarFileExtension;
+ //(<Resource>this.$scope.component).importedFile.filetype="csar";
+ }
+
+ this.$scope.setValidState(true);
+
+ this.$scope.calculateUnique = (mainCategory:string, subCategory:string):string => {
+ let uniqueId:string = mainCategory;
+ if (subCategory) {
+ uniqueId += "_#_" + subCategory; // Set the select category combobox to show the selected category.
+ }
+ return uniqueId;
+ };
+
+ //TODO remove this after handling contact in UI
+ if (this.$scope.component.isProduct() && this.$scope.isCreateMode()) {
+ (<Product>this.$scope.component).contacts = [];
+ (<Product>this.$scope.component).contacts.push(this.cacheService.get("user").userId);
+ } else if (this.$scope.isCreateMode()) {
+ this.$scope.component.contactId = this.cacheService.get("user").userId;
+ }
+
+ };
+
+ // Convert category string MainCategory_#_SubCategory to Array with one item (like the server except)
+ private convertCategoryStringToOneArray = ():Array<IMainCategory> => {
+ let tmp = this.$scope.component.selectedCategory.split("_#_");
+ let mainCategory = tmp[0];
+ let subCategory = tmp[1];
+
+ // Find the selected category and add the relevant sub category.
+ let selectedMainCategory:IMainCategory = <IMainCategory>_.find(this.$scope.categories, function (item) {
+ return item["name"] === mainCategory;
+
+ });
+
+ let mainCategoryClone = angular.copy(selectedMainCategory);
+ if (subCategory) {
+ let selectedSubcategory = <ISubCategory>_.find(selectedMainCategory.subcategories, function (item) {
+ return item["name"] === subCategory;
+ });
+ mainCategoryClone['subcategories'] = [angular.copy(selectedSubcategory)];
+ }
+ let tmpSelected = <IMainCategory> mainCategoryClone;
+
+ let result:Array<IMainCategory> = [];
+ result.push(tmpSelected);
+
+ return result;
+ };
+
+ private updateComponentNameInBreadcrumbs = ():void => {
+ //update breadcrum after changing name
+ this.$scope.breadcrumbsModel[1].updateSelectedMenuItemText(this.$scope.component.getComponentSubType() + ': ' + this.$scope.component.name);
+ this.$scope.updateMenuComponentName(this.$scope.component.name);
+ };
+
+ private initScopeMethods = ():void => {
+
+ this.$scope.initCategoreis = ():void => {
+ if (this.$scope.componentType === ComponentType.RESOURCE) {
+ this.$scope.categories = this.cacheService.get('resourceCategories');
+
+ }
+ if (this.$scope.componentType === ComponentType.SERVICE) {
+ this.$scope.categories = this.cacheService.get('serviceCategories');
+ }
+ };
+
+ this.$scope.validateField = (field:any):boolean => {
+ if (field && field.$dirty && field.$invalid) {
+ return true;
+ }
+ return false;
+ };
+
+ this.$scope.openOnBoardingModal = ():void => {
+ let csarUUID = (<Resource>this.$scope.component).csarUUID;
+ this.ModalsHandler.openOnboadrdingModal('Update', csarUUID).then(()=> {
+ // OK
+ this.$scope.uploadFileChangedInGeneralTab();
+ }, ()=> {
+ // ERROR
+ });
+ };
+
+ this.$scope.validateName = (isInit:boolean):void => {
+ if (isInit === undefined) {
+ isInit = false;
+ }
+
+ let name = this.$scope.component.name;
+ if (!name || name === "") {
+ if (this.$scope.editForm
+ && this.$scope.editForm["componentName"]
+ && this.$scope.editForm["componentName"].$error) {
+
+ // Clear the error name already exists
+ this.$scope.editForm["componentName"].$setValidity('nameExist', true);
+ }
+
+ return;
+ }
+ //?????????????????????????
+ let subtype:string = ComponentType.RESOURCE == this.$scope.componentType ? this.$scope.component.getComponentSubType() : undefined;
+
+ let onFailed = (response) => {
+ //console.info('onFaild', response);
+ //this.$scope.isLoading = false;
+ };
+
+ let onSuccess = (validation:IValidate) => {
+ this.$scope.editForm["componentName"].$setValidity('nameExist', validation.isValid);
+ if (validation.isValid) {
+ //update breadcrumb after changing name
+ this.updateComponentNameInBreadcrumbs();
+ }
+ };
+
+ if (isInit) {
+ // When page is init after update
+ if (this.$scope.component.name !== this.$scope.originComponent.name) {
+ if (!(this.$scope.componentType === ComponentType.RESOURCE && (<Resource>this.$scope.component).csarUUID !== undefined)
+ ) {
+ this.$scope.component.validateName(name, subtype).then(onSuccess, onFailed);
+ }
+ }
+ } else {
+ // Validating on change (has debounce)
+ if (this.$scope.editForm
+ && this.$scope.editForm["componentName"]
+ && this.$scope.editForm["componentName"].$error
+ && !this.$scope.editForm["componentName"].$error.pattern
+ && (!this.$scope.originComponent.name || this.$scope.component.name.toUpperCase() !== this.$scope.originComponent.name.toUpperCase())
+ ) {
+ if (!(this.$scope.componentType === ComponentType.RESOURCE && (<Resource>this.$scope.component).csarUUID !== undefined)
+ ) {
+ this.$scope.component.validateName(name, subtype).then(onSuccess, onFailed);
+ }
+ } else if (this.$scope.originComponent.name && this.$scope.component.name.toUpperCase() === this.$scope.originComponent.name.toUpperCase()) {
+ // Clear the error
+ this.$scope.editForm["componentName"].$setValidity('nameExist', true);
+ }
+ }
+ };
+
+ this.$scope.$watchCollection('component.name', (newData:any):void => {
+ this.$scope.validateName(false);
+ });
+
+ // Notify the parent if this step valid or not.
+ this.$scope.$watch("editForm.$valid", (newVal, oldVal) => {
+ this.$scope.setValidState(newVal);
+ });
+
+ this.$scope.$watch("editForm.$dirty", (newVal, oldVal) => {
+ if (newVal !== oldVal) {
+ this.$state.current.data.unsavedChanges = newVal && !this.$scope.isCreateMode();
+ }
+ });
+
+ this.$scope.onCategoryChange = ():void => {
+ this.$scope.component.selectedCategory = this.$scope.componentCategories.selectedCategory;
+ this.$scope.component.categories = this.convertCategoryStringToOneArray();
+ this.$scope.component.icon = DEFAULT_ICON;
+ };
+
+ this.$scope.onEcompGeneratedNamingChange = ():void =>{
+ if(!(<Service>this.$scope.component).ecompGeneratedNaming){
+ (<Service>this.$scope.component).namingPolicy = '';
+ }
+ };
+
+ this.$scope.onVendorNameChange = (oldVendorName:string):void => {
+ if (this.$scope.component.icon === oldVendorName) {
+ this.$scope.component.icon = DEFAULT_ICON;
+ }
+ };
+ };
+}
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/general/general-view.html b/catalog-ui/src/app/view-models/workspace/tabs/general/general-view.html
new file mode 100644
index 0000000000..2ad0cbacd6
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/general/general-view.html
@@ -0,0 +1,354 @@
+<div include-padding="true" class="sdc-workspace-general-step">
+
+ <form novalidate class="w-sdc-form" name="editForm" validation-on-load form-to-validate="editForm">
+
+ <div class="w-sdc-form-section-container">
+
+ <!--------------------- IMPORT TOSCA FILE USING BROWSE (ALSO VFC) -------------------->
+ <div class="i-sdc-form-item" ng-if="isShowFileBrowse">
+ <label class="i-sdc-form-label" data-ng-class="{'required':isCreateMode()}">{{browseFileLabel}}</label>
+ <file-upload id="fileUploadElement"
+ class="i-sdc-form-input"
+ element-name="fileElement"
+ element-disabled="{{!isCreateMode()&&!(isEditMode()&&component.resourceType=='VF')}} || {{isViewMode()}}"
+ form-element="editForm"
+ file-model="component.importedFile"
+ on-file-changed-in-directive="uploadFileChangedInGeneralTab"
+ extensions="{{importedFileExtension}}"
+ default-text="'Browse to select file'"
+ data-ng-class="{'error':!(isEditMode()&&component.resourceType=='VF') && (!editForm.fileElement.$valid || !component.importedFile.filename)}"></file-upload>
+ </div>
+
+ <!--------------------- IMPORT TOSCA FILE USING ONBOARDING -------------------->
+ <div class="i-sdc-form-item" ng-if="isShowOnboardingSelectionBrowse">
+ <label class="i-sdc-form-label required">Select VSP</label>
+ <div class="i-sdc-form-file-upload i-sdc-form-input">
+ <span class="i-sdc-form-file-name" data-tests-id="filename">{{(fileModel && fileModel.filename) || importedToscaBrowseFileText}}</span>
+ <div class="i-sdc-form-file-upload-x-btn" ng-click="cancel()" data-ng-show="fileModel.filename && fileModel.filename!=='' && elementDisabled!=='true'"></div>
+ <input type="button" name="fileElement"/>
+ <div class="file-upload-browse-btn" data-ng-click="openOnBoardingModal()" data-tests-id="browseButton">Browse</div>
+ </div>
+ </div>
+
+ <div class="input-error-file-upload" data-ng-show="component.importedFile && (!editForm.fileElement.$valid || !component.importedFile.filename)">
+ <!-- editForm.fileElement.$error.required <== Can not use this, because the browse is done from outside for the first time -->
+ <span ng-show="!(isEditMode()&&component.resourceType=='VF')&&!component.importedFile.filename" translate="NEW_SERVICE_RESOURCE_ERROR_TOSCA_FILE_REQUIRED"></span><!-- Required -->
+ <span ng-show="editForm.fileElement.$error.maxsize" translate="VALIDATION_ERROR_MAX_FILE_SIZE"></span>
+ <span ng-show="editForm.fileElement.$error.filetype" translate="NEW_SERVICE_RESOURCE_ERROR_VALID_TOSCA_EXTENSIONS" translate-values="{'extensions': '{{importedFileExtension}}' }"></span>
+ <span ng-show="editForm.fileElement.$error.emptyFile" translate="VALIDATION_ERROR_EMPTY_FILE"></span>
+ </div>
+ <!--------------------- IMPORT TOSCA FILE -------------------->
+
+ <div class="w-sdc-form-columns-wrapper">
+
+ <div class="w-sdc-form-column">
+
+ <!--------------------- NAME -------------------->
+ <div class="i-sdc-form-item" data-ng-class="{'error': validateField(editForm.componentName)}">
+ <label class="i-sdc-form-label required">Name</label>
+ <input class="i-sdc-form-input"
+ data-ng-class="{'view-mode': isViewMode()}"
+ name="componentName"
+ data-ng-init="isCreateMode() && validateName(true)"
+ data-ng-maxlength="{{component.isProduct()?'25':'50'}}"
+ maxlength="{{component.isProduct()?'25':'50'}}"
+ data-ng-minlength="{{component.isProduct()?'4':'0'}}"
+ minlength="{{component.isProduct()?'4':'0'}}"
+ data-ng-model="component.name"
+ type="text"
+ data-required
+ data-ng-model-options="{ debounce: 500 }"
+ data-ng-pattern="validation.componentNameValidationPattern"
+ data-ng-disabled="component.isAlreadyCertified()"
+ data-tests-id="name"
+ autofocus
+ ng-readonly="isViewMode()"
+ />
+
+ <div class="input-error" data-ng-show="validateField(editForm.componentName)">
+ <span ng-show="editForm.componentName.$error.required" translate="NEW_SERVICE_RESOURCE_ERROR_RESOURCE_NAME_REQUIRED"></span>
+ <span ng-show="editForm.componentName.$error.maxlength" translate="VALIDATION_ERROR_MAX_LENGTH" translate-values="{'max': '50' }"></span>
+ <span ng-show="editForm.componentName.$error.minlength" translate="VALIDATION_ERROR_MIN_LENGTH" translate-values="{'min': '4' }"></span>
+ <span ng-show="editForm.componentName.$error.nameExist" translate="NEW_SERVICE_RESOURCE_ERROR_NAME_EXISTS"></span>
+ <span ng-show="editForm.componentName.$error.pattern" translate="VALIDATION_ERROR_INVALID_NAME"></span>
+ </div>
+ </div>
+ <!--------------------- NAME -------------------->
+
+ <!--------------------- FULL NAME -------------------->
+ <div ng-if="component.isProduct()" class="i-sdc-form-item" data-ng-class="{'error': validateField(editForm.fullName)}">
+ <label class="i-sdc-form-label required">Full Name</label>
+ <input class="i-sdc-form-input"
+ name="fullName"
+ data-ng-class="{'view-mode': isViewMode()}"
+ data-ng-change="validateName()"
+ data-ng-maxlength="100"
+ maxlength="100"
+ data-ng-minlength="4"
+ minlength="4"
+ data-ng-model="component.fullName"
+ type="text"
+ data-required
+ data-ng-model-options="{ debounce: 500 }"
+ data-ng-pattern="validation.commentValidationPattern"
+ data-tests-id="fullName"
+ autofocus
+ ng-readonly="isViewMode()"
+ />
+
+ <div class="input-error" data-ng-show="validateField(editForm.fullName)">
+ <span ng-show="editForm.fullName.$error.required" translate="NEW_SERVICE_RESOURCE_ERROR_RESOURCE_NAME_REQUIRED"></span>
+ <span ng-show="editForm.fullName.$error.maxlength" translate="VALIDATION_ERROR_MAX_LENGTH" translate-values="{'max': '50' }"></span>
+ <span ng-show="editForm.fullName.$error.minlength" translate="VALIDATION_ERROR_MIN_LENGTH" translate-values="{'min': '4' }"></span>
+ <span ng-show="editForm.fullName.$error.nameExist" translate="NEW_SERVICE_RESOURCE_ERROR_NAME_EXISTS"></span>
+ <span ng-show="editForm.fullName.$error.pattern" translate="VALIDATION_ERROR_SPECIAL_CHARS_NOT_ALLOWED"></span>
+ </div>
+ </div>
+ <!--------------------- NAME -------------------->
+
+ <!--------------------- DESCRIPTION -------------------->
+ <div class="i-sdc-form-item"
+ data-ng-class="{'error': validateField(editForm.description)}">
+ <label class="i-sdc-form-label required">Description</label>
+ <textarea class="description"
+ name="description"
+ data-ng-class="{'view-mode': isViewMode()}"
+ data-ng-maxlength="1024"
+ data-required
+ data-ng-model="component.description"
+ data-ng-model-options="{ debounce: 500 }"
+ data-ng-pattern="validation.commentValidationPattern"
+ maxlength="1024"
+ data-tests-id="description"></textarea>
+ <!-- placeholder="Description here..." -->
+
+ <div class="input-error" data-ng-show="validateField(editForm.description)">
+ <span ng-show="editForm.description.$error.required" translate="NEW_SERVICE_RESOURCE_ERROR_RESOURCE_DESCRIPTION_REQUIRED"></span>
+ <span ng-show="editForm.description.$error.maxlength" translate="VALIDATION_ERROR_MAX_LENGTH" translate-values="{'max': '1024' }"></span>
+ <span ng-show="editForm.description.$error.pattern" translate="VALIDATION_ERROR_SPECIAL_CHARS_NOT_ALLOWED"></span>
+ </div>
+ </div>
+ <!--------------------- DESCRIPTION -------------------->
+
+ <!--------------------- CATEGORIES -------------------->
+ <div class="i-sdc-form-item"
+ data-ng-class="{'error': validateField(editForm.category)}"
+ data-ng-if="!component.isProduct()">
+ <loader data-display="!categories && !initCategoreis()" relative="true"></loader>
+ <label class="i-sdc-form-label required">Category</label>
+ <select class="i-sdc-form-select"
+ data-required
+ name="category"
+ data-ng-class="{'view-mode': isViewMode()}"
+ data-ng-change="onCategoryChange()"
+ data-ng-disabled="component.isAlreadyCertified() || (component.isCsarComponent() && component.selectedCategory && component.selectedCategory!=='')"
+ data-ng-model="componentCategories.selectedCategory"
+ data-tests-id="selectGeneralCategory"
+ >
+ <option value="">Select category</option>
+ <optgroup ng-if="component.isResource()" data-ng-repeat="mainCategory in categories | orderBy:['name']" label="{{mainCategory.name}}" data-tests-id="{{mainCategory.name}}">
+ <option data-ng-repeat="subCategory in mainCategory.subcategories track by $index"
+ data-ng-selected="componentCategories.selectedCategory === calculateUnique(mainCategory.name,subCategory.name)"
+ data-tests-id="{{subCategory.name}}"
+ value="{{calculateUnique(mainCategory.name, subCategory.name)}}">{{subCategory.name}}
+
+ </option>
+ </optgroup>
+ <option ng-if="component.isService()" data-ng-repeat="mainCategory in categories | orderBy:['name']"
+ data-ng-selected="component.selectedCategory===mainCategory.name"
+ value="{{mainCategory.name}}"
+ data-tests-id="{{mainCategory.name}}">{{mainCategory.name}}</option>
+ </select>
+
+ <div class="input-error" data-ng-show="validateField(editForm.category)">
+ <span ng-show="editForm.category.$error.required" translate="NEW_SERVICE_RESOURCE_ERROR_CATEGORY_REQUIRED"></span>
+ </div>
+ </div>
+ <!--------------------- CATEGORIES -------------------->
+
+ <!--------------------- PROJECT CODE -------------------->
+ <div class="i-sdc-form-item" data-ng-if="!component.isResource()"
+ data-ng-class="{'error': validateField(editForm.projectCode)}">
+ <label class="i-sdc-form-label required" translate="GENERAL_LABEL_PROJECT_CODE"></label>
+ <input class="i-sdc-form-input" type="text"
+ data-ng-model="component.projectCode"
+ data-ng-class="{'view-mode': isViewMode()}"
+ data-ng-model-options="{ debounce: 500 }"
+ maxlength="50"
+ data-required
+ name="projectCode"
+ data-ng-pattern="validation.projectCodeValidationPattern"
+ data-tests-id="projectCode"
+ />
+
+ <div class="input-error" data-ng-show="validateField(editForm.projectCode)">
+ <span ng-show="editForm.projectCode.$error.required" translate="NEW_SERVICE_RESOURCE_ERROR_PROJECT_CODE_REQUIRED"></span>
+ <span ng-show="editForm.projectCode.$error.pattern" translate="NEW_SERVICE_RESOURCE_ERROR_PROJECT_CODE_NOT_VALID"></span>
+ </div>
+ </div>
+
+
+ <!--------------------- ECOMPGENERATEDNAMING -------------------->
+
+ <div class="i-sdc-form-item"
+ data-ng-class="{'error': validateField(editForm.ecompGeneratedNaming)}"
+ data-ng-if="component.isService()">
+ <label class="i-sdc-form-label">Ecomp Generated Naming</label>
+ <select class="i-sdc-form-select"
+ data-required
+ name="ecompGeneratedNaming"
+ data-ng-change="onEcompGeneratedNamingChange()"
+ data-ng-class="{'view-mode': isViewMode()}"
+ data-ng-model="component.ecompGeneratedNaming"
+ data-tests-id="ecompGeneratedNaming">
+ <option ng-value="true">true</option>
+ <option ng-value="false">false</option>
+ </select>
+ <div class="input-error" data-ng-show="validateField(editForm.ecompGeneratedNaming)">
+
+ </div>
+ </div>
+ <!--------------------- CATEGORIES -------------------->
+
+ <!--------------------- NAMING POLICY -------------------->
+ <div ng-if="component.isService()" class="i-sdc-form-item" data-ng-class="{'error': validateField(editForm.namingPolicy)}">
+ <label class="i-sdc-form-label">Naming policy</label>
+ <input class="i-sdc-form-input"
+ name="fullName"
+ data-ng-class="{'view-mode': isViewMode() || !component.ecompGeneratedNaming}"
+ data-ng-maxlength="100"
+ maxlength="100"
+ data-ng-model="component.namingPolicy"
+ type="text"
+ data-ng-model-options="{ debounce: 500 }"
+ data-ng-pattern="validation.commentValidationPattern"
+ data-tests-id="namingPolicy"
+ autofocus
+ ng-readonly="isViewMode() || !component.ecompGeneratedNaming"
+ />
+
+ <div class="input-error" data-ng-show="validateField(editForm.namingPolicy)">
+ <span ng-show="editForm.namingPolicy.$error.maxlength" translate="VALIDATION_ERROR_MAX_LENGTH" translate-values="{'max': '100' }"></span>
+ <span ng-show="editForm.namingPolicy.$error.pattern" translate="VALIDATION_ERROR_SPECIAL_CHARS_NOT_ALLOWED"></span>
+ </div>
+ </div>
+ <!--------------------- NAMING POLICY -------------------->
+
+
+ <!--------------------- VENDOR NAME -------------------->
+ <div ng-if="component.isResource()" class="i-sdc-form-item" data-ng-class="{'error': validateField(editForm.vendorName)}">
+ <label class="i-sdc-form-label required">Vendor</label>
+ <input class="i-sdc-form-input" type="text"
+ data-ng-class="{'view-mode': isViewMode()}"
+ data-ng-model="component.vendorName"
+ data-ng-model-options="{ debounce: 500 }"
+ data-ng-maxlength="25"
+ data-required
+ ng-click="oldValue = component.vendorName"
+ name="vendorName"
+ data-ng-change="onVendorNameChange(oldValue)"
+ data-ng-pattern="validation.vendorValidationPattern"
+ maxlength="25"
+ data-ng-disabled="component.isAlreadyCertified() || (component.isCsarComponent() && component.vendorName && component.vendorName!=='')"
+ data-tests-id="vendorName"
+ />
+
+ <div class="input-error" data-ng-show="validateField(editForm.vendorName)">
+ <span ng-show="editForm.vendorName.$error.required" translate="NEW_SERVICE_RESOURCE_ERROR_VENDOR_NAME_REQUIRED"></span>
+ <span ng-show="editForm.vendorName.$error.maxlength" translate="VALIDATION_ERROR_MAX_LENGTH" translate-values="{'max': '25' }"></span>
+ <span ng-show="editForm.vendorName.$error.pattern" translate="VALIDATION_ERROR_SPECIAL_CHARS_NOT_ALLOWED"></span>
+ </div>
+
+ </div>
+
+ <!--------------------- VENDOR NAME -------------------->
+
+ <!--------------------- VENDOR RELEASE -------------------->
+ <div ng-if="component.isResource()"
+ class="i-sdc-form-item"
+ data-ng-class="{'error': validateField(editForm.vendorRelease)}">
+ <label class="i-sdc-form-label required">Vendor Release</label>
+ <input class="i-sdc-form-input" type="text"
+ data-ng-class="{'view-mode': isViewMode()}"
+ data-ng-model="component.vendorRelease"
+ data-ng-model-options="{ debounce: 500 }"
+ data-ng-maxlength="25"
+ data-required
+ name="vendorRelease"
+ data-ng-pattern="validation.vendorValidationPattern"
+ maxlength="25"
+ data-ng-disabled="component.isCsarComponent() && component.vendorRelease && component.vendorRelease!==''"
+ data-tests-id="vendorRelease"
+ />
+
+ <div class="input-error" data-ng-show="validateField(editForm.vendorRelease)">
+ <span ng-show="editForm.vendorRelease.$error.required" translate="NEW_SERVICE_RESOURCE_ERROR_VENDOR_RELEASE_REQUIRED"></span>
+ <span ng-show="editForm.vendorRelease.$error.maxlength" translate="VALIDATION_ERROR_MAX_LENGTH" translate-values="{'max': '128' }"></span>
+ <span ng-show="editForm.vendorRelease.$error.pattern" translate="VALIDATION_ERROR_SPECIAL_CHARS_NOT_ALLOWED"></span>
+ </div>
+ </div>
+ <!--------------------- VENDOR RELEASE -------------------->
+
+
+
+ </div><!-- Close w-sdc-form-column -->
+
+ <div class="w-sdc-form-column">
+
+ <!--------------------- RESOURCE TAGS -------------------->
+ <div class="i-sdc-form-item" data-ng-class="{'error': validateField(editForm.tags)}">
+ <label class="i-sdc-form-label">Tags</label>
+
+ <sdc-tags form-element="editForm" element-name="tags" max-tags="20" class="i-sdc-form-item-tags"
+ sdc-disabled="isViewMode()"
+ tags="component.tags"
+ pattern="validation.tagValidationPattern"
+ special-tag="component.name"></sdc-tags>
+
+ <div class="input-error" data-ng-show="validateField(editForm.tags)">
+ <span ng-show="editForm.tags.$error.pattern" translate="VALIDATION_ERROR_SPECIAL_CHARS_NOT_ALLOWED"></span>
+ <span ng-show="editForm.tags.$error.nameExist" translate="NEW_SERVICE_RESOURCE_ERROR_TAG_NAME_EXIST"></span>
+ </div>
+ </div>
+ <!--------------------- RESOURCE TAGS -------------------->
+
+ <!--------------------- USER ID -------------------->
+ <div class="i-sdc-form-item" data-ng-class="{'error': validateField(editForm.contactId)}">
+ <label class="i-sdc-form-label " data-ng-class="{'required':!component.isProduct()}" translate="GENERAL_LABEL_CONTACT_ID"></label>
+ <input class="i-sdc-form-input" type="text" data-ng-if="!component.isProduct()"
+ data-ng-model="component.contactId"
+ data-ng-class="{'view-mode': isViewMode()}"
+ data-ng-required="!component.isProduct()"
+ name="contactId"
+ data-ng-pattern="validation.contactIdValidationPattern"
+ data-ng-model-options="{ debounce: 500 }"
+ data-tests-id="contactId"
+ maxlength="50"
+ />
+ <input class="i-sdc-form-input" type="text" data-ng-if="component.isProduct()"
+ data-ng-model="component.contacts[0]"
+ data-ng-class="{'view-mode': isViewMode()}"
+ data-ng-required="!component.isProduct()"
+ name="contactId"
+ data-ng-pattern="validation.contactIdValidationPattern"
+ data-ng-model-options="{ debounce: 500 }"
+ data-tests-id="contactId"
+ maxlength="50"
+ />
+
+ <div class="input-error" data-ng-show="validateField(editForm.contactId)">
+ <span ng-show="editForm.contactId.$error.required" translate="NEW_SERVICE_RESOURCE_ERROR_CONTACT_REQUIRED"></span>
+ <span ng-show="editForm.contactId.$error.pattern" translate="NEW_SERVICE_RESOURCE_ERROR_CONTACT_NOT_VALID"></span>
+ </div>
+ </div>
+ <!--------------------- USER ID -------------------->
+
+
+ </div><!-- Close w-sdc-form-column -->
+ </div>
+
+ </div><!-- Close w-sdc-form-section-container -->
+
+ </form>
+
+</div>
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/general/general.less b/catalog-ui/src/app/view-models/workspace/tabs/general/general.less
new file mode 100644
index 0000000000..1861d02e98
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/general/general.less
@@ -0,0 +1,64 @@
+.sdc-workspace-general-step {
+
+ .w-sdc-form {
+ padding: 0;
+
+ .i-sdc-form-file-upload{
+ input[type="button"] {
+ cursor: pointer;
+ display: block;
+ filter: alpha(opacity=0);
+ width: 100px;
+ height: 30px;
+ opacity: 0;
+ position: absolute;
+ right: 0;
+ text-align: right;
+ top: 0;
+ }
+
+ .file-upload-browse-btn {
+ .noselect;
+ .bg_n;
+ padding: 4px 6px;
+ cursor: pointer;
+ z-index: 999;
+ width: 100px;
+ height: 28px;
+ text-align: center;
+
+ &.disabled {
+ cursor: default;
+ }
+ }
+ }
+
+ .w-sdc-form-section-container {
+ text-align: center;
+ }
+
+ .i-sdc-form-item {
+ &.upload {
+ margin-top: 0;
+ width: auto;
+ padding: 10px;
+ }
+ }
+
+ .template-desc {
+ border: 1px dashed @border_color_f;
+ height: 130px;
+ overflow: hidden;
+ padding: 10px 6px 6px 6px;
+ margin-top: 10px;
+ }
+
+ .sdc-tag .tag {
+ max-width: 225px;
+ }
+
+ }
+
+}
+
+
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/icons/icons-view-model.ts b/catalog-ui/src/app/view-models/workspace/tabs/icons/icons-view-model.ts
new file mode 100644
index 0000000000..03dad2cc06
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/icons/icons-view-model.ts
@@ -0,0 +1,111 @@
+/**
+ * Created by obarda on 4/4/2016.
+ */
+'use strict';
+import {ComponentFactory} from "app/utils";
+import {AvailableIconsService} from "app/services";
+import {IWorkspaceViewModelScope} from "app/view-models/workspace/workspace-view-model";
+import {IMainCategory, ISubCategory} from "app/models";
+
+export interface IIconsScope extends IWorkspaceViewModelScope {
+ icons:Array<string>;
+ iconSprite:string;
+ setComponentIcon(iconSrc:string):void;
+}
+
+export class IconsViewModel {
+
+ static '$inject' = [
+ '$scope',
+ 'Sdc.Services.AvailableIconsService',
+ 'ComponentFactory',
+ '$state'
+ ];
+
+ constructor(private $scope:IIconsScope,
+ private availableIconsService:AvailableIconsService,
+ private ComponentFactory:ComponentFactory,
+ private $state:ng.ui.IStateService) {
+
+
+ this.initScope();
+ this.initIcons();
+ this.$scope.updateSelectedMenuItem();
+ this.$scope.iconSprite = this.$scope.component.iconSprite;
+
+ if (this.$scope.component.isResource()) {
+ this.initVendor();
+ }
+ }
+
+ private initialIcon:string = this.$scope.component.icon;
+ private initIcons = ():void => {
+
+ // For subcategories that where created by admin, there is no icons
+ this.$scope.icons = new Array<string>();
+ if (this.$scope.component.categories && this.$scope.component.categories.length > 0) {
+
+ _.forEach(this.$scope.component.categories, (category:IMainCategory):void => {
+ if (category.icons) {
+ this.$scope.icons = this.$scope.icons.concat(category.icons);
+ }
+ if (category.subcategories) {
+ _.forEach(category.subcategories, (subcategory:ISubCategory):void => {
+ if (subcategory.icons) {
+ this.$scope.icons = this.$scope.icons.concat(subcategory.icons);
+ }
+ });
+ }
+ });
+ }
+
+ if (this.$scope.component.isResource()) {
+ let resourceType:string = this.$scope.component.getComponentSubType();
+ if (resourceType === 'VL') {
+ this.$scope.icons = ['vl'];
+ }
+ if (resourceType === 'CP') {
+ this.$scope.icons = ['cp'];
+ }
+ }
+
+ if (this.$scope.icons.length === 0) {
+ this.$scope.icons = this.availableIconsService.getIcons(this.$scope.component.componentType);
+ }
+ //we always add the defual icon to the list
+ this.$scope.icons.push('defaulticon');
+ };
+
+ private initVendor = ():void => {
+ let vendors:Array<string> = this.availableIconsService.getIcons(this.$scope.component.componentType).slice(5, 19);
+ let vendorName = this.$scope.component.vendorName.toLowerCase();
+ if ('at&t' === vendorName) {
+ vendorName = 'att';
+ }
+ if ('nokia' === vendorName) {
+ vendorName = 'nokiasiemens';
+ }
+ let vendor:string = _.find(vendors, (vendor:string)=> {
+ return vendor.replace(/[_]/g, '').toLowerCase() === vendorName;
+ });
+
+ if (vendor && this.$scope.icons.indexOf(vendor) === -1) {
+ this.$scope.icons.push(vendor);
+ }
+ };
+
+ private initScope():void {
+ this.$scope.icons = [];
+ this.$scope.setValidState(true);
+ //if(this.$scope.component.icon === DEFAULT_ICON){
+ // //this.$scope.setValidState(false);
+ //}
+
+ this.$scope.setComponentIcon = (iconSrc:string):void => {
+ this.$state.current.data.unsavedChanges = !this.$scope.isViewMode() && (iconSrc != this.initialIcon);
+ this.$scope.component.icon = iconSrc;
+ // this.$scope.setValidState(true);
+ };
+
+ }
+}
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/icons/icons-view.html b/catalog-ui/src/app/view-models/workspace/tabs/icons/icons-view.html
new file mode 100644
index 0000000000..aac14e0e84
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/icons/icons-view.html
@@ -0,0 +1,26 @@
+<div class="workspace-icons">
+
+ <form novalidate class="w-sdc-form" name="iconForm">
+ <label class="i-sdc-form-label icons-label required">Icons</label>
+ <div class="selected-icon-container" data-ng-class="{'view-mode': isViewMode()}">
+ <div class="i-sdc-form-item-suggested-icon large selected-icon {{iconSprite}} {{component.icon}}"
+ data-ng-class="{ 'disable': isViewMode() }"
+ ng-model="component.icon"
+ tooltips tooltip-content='{{component.icon | translate}}'
+ >
+ </div>
+ </div>
+ <div data-ng-class="{'view-mode': isViewMode()}" class="icons-text">Select one of the icons below for the asset</div>
+ <div class="i-sdc-form-item suggested-icons-container" data-ng-class="{'view-mode no-pointer-events' : isViewMode()}">
+ <div class ="suggested-icon-wrapper" ng-class="component.icon==='{{iconSrc}}' ? 'selected' : '' " data-ng-repeat="iconSrc in icons track by $index">
+ <div class="i-sdc-form-item-suggested-icon large {{iconSprite}} {{iconSrc}}" data-ng-class="component.isAlreadyCertified() || isViewMode() ? 'disable':'hand'"
+ ng-model="component.icon"
+ data-tests-id="{{iconSrc}} iconBox"
+ data-ng-click="!component.isAlreadyCertified() && setComponentIcon(iconSrc)"
+ tooltips tooltip-content='{{iconSrc | translate}}'
+ >
+ </div>
+ </div>
+ </div>
+ </form>
+</div>
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/icons/icons.less b/catalog-ui/src/app/view-models/workspace/tabs/icons/icons.less
new file mode 100644
index 0000000000..65f946f395
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/icons/icons.less
@@ -0,0 +1,65 @@
+.workspace-icons {
+
+ width: 89%;
+ display: inline-block;
+ text-align: center;
+ align-items: center;
+
+ .w-sdc-form {
+ padding-top: 0px;
+ padding-bottom: 0px;
+ .selected-icon-container {
+ text-align: left;
+ border: 1px solid #cfcfcf;
+ clear: both;
+ margin-bottom: 30px;
+ padding: 2px 0px 5px 5px;
+ .selected-icon {
+ margin: 8px 5px 0px 6px;
+ }
+ }
+
+ .suggested-icons-container {
+ text-align: left;
+ border: 1px solid #cfcfcf;
+ clear: both;
+ padding: 2px 0px 5px 5px;
+ height: 340px;
+ margin-bottom: 0px;
+
+ .suggested-icon-wrapper {
+ margin: 8px 5px 0px 6px;
+ display: inline-block;
+
+ &.selected {
+ border: 2px solid @main_color_a;
+ border-radius: 35px;
+ display: inline-block;
+ line-height: 0px;
+ padding: 3px;
+ }
+
+ }
+ .suggested-icon {
+ // margin: 8px 5px 0px 6px;
+ display: inline-block;
+ &.disable{
+ opacity: 0.4;
+ }
+ }
+ }
+
+ .icons-label {
+ float: left;
+ }
+
+ .icons-text {
+ text-align: left;
+ line-height: 32px;
+ padding-left: 10px;
+ width: 100%;
+ border: 1px solid #cfcfcf;
+ border-bottom: none;
+ }
+ }
+}
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/information-artifacts/information-artifacts-view-model.ts b/catalog-ui/src/app/view-models/workspace/tabs/information-artifacts/information-artifacts-view-model.ts
new file mode 100644
index 0000000000..e10a9944d3
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/information-artifacts/information-artifacts-view-model.ts
@@ -0,0 +1,138 @@
+'use strict';
+import {ModalsHandler} from "app/utils";
+import {SharingService} from "app/services";
+import {IAppConfigurtaion, ArtifactModel, IFileDownload} from "app/models";
+import {IWorkspaceViewModelScope} from "app/view-models/workspace/workspace-view-model";
+import {ComponentServiceNg2} from "../../../../ng2/services/component-services/component.service";
+import {ArtifactGroupModel} from "../../../../models/artifacts";
+import {ComponentGenericResponse} from "../../../../ng2/services/responses/component-generic-response";
+
+export interface IInformationArtifactsScope extends IWorkspaceViewModelScope {
+ artifacts:Array<ArtifactModel>;
+ tableHeadersList:Array<any>;
+ artifactType:string;
+ isResourceInstance:boolean;
+ downloadFile:IFileDownload;
+ isLoading:boolean;
+ sortBy:string;
+ reverse:boolean;
+
+ getTitle():string;
+ addOrUpdate(artifact:ArtifactModel):void;
+ delete(artifact:ArtifactModel):void;
+ download(artifact:ArtifactModel):void;
+ clickArtifactName(artifact:any):void;
+ openEditEnvParametersModal(artifactResource:ArtifactModel):void;
+ sort(sortBy:string):void;
+ showNoArtifactMessage():boolean;
+}
+
+export class InformationArtifactsViewModel {
+
+ static '$inject' = [
+ '$scope',
+ '$filter',
+ '$state',
+ 'sdcConfig',
+ 'ModalsHandler',
+ 'ComponentServiceNg2'
+ ];
+
+ constructor(private $scope:IInformationArtifactsScope,
+ private $filter:ng.IFilterService,
+ private $state:any,
+ private sdcConfig:IAppConfigurtaion,
+ private ModalsHandler:ModalsHandler,
+ private ComponentServiceNg2: ComponentServiceNg2) {
+ this.initInformationalArtifacts();
+ this.$scope.updateSelectedMenuItem();
+ }
+
+ private initInformationalArtifacts = ():void => {
+ if(!this.$scope.component.artifacts) {
+ this.$scope.isLoading = true;
+ this.ComponentServiceNg2.getComponentInformationalArtifacts(this.$scope.component).subscribe((response:ComponentGenericResponse) => {
+ this.$scope.component.artifacts = response.artifacts;
+ this.initScope();
+ this.$scope.isLoading = false;
+ });
+ } else {
+ this.initScope();
+ }
+ }
+
+ private initScope = ():void => {
+
+ this.$scope.isLoading = false;
+ this.$scope.sortBy = 'artifactDisplayName';
+ this.$scope.reverse = false;
+ this.$scope.setValidState(true);
+ this.$scope.artifactType = 'informational';
+ this.$scope.getTitle = ():string => {
+ return this.$filter("resourceName")(this.$scope.component.name) + ' Artifacts';
+
+ };
+
+ this.$scope.tableHeadersList = [
+ {title: 'Name', property: 'artifactDisplayName'},
+ {title: 'Type', property: 'artifactType'},
+ {title: 'Version', property: 'artifactVersion'},
+ {title: 'UUID', property: 'artifactUUID'}
+ ];
+
+ this.$scope.artifacts = <ArtifactModel[]>_.values(this.$scope.component.artifacts);
+ this.$scope.sort = (sortBy:string):void => {
+ this.$scope.reverse = (this.$scope.sortBy === sortBy) ? !this.$scope.reverse : false;
+ this.$scope.sortBy = sortBy;
+ };
+
+
+ this.$scope.addOrUpdate = (artifact:ArtifactModel):void => {
+ artifact.artifactGroupType = 'INFORMATIONAL';
+ this.ModalsHandler.openArtifactModal(artifact, this.$scope.component).then(() => {
+ this.$scope.artifacts = <ArtifactModel[]>_.values(this.$scope.component.artifacts);
+ });
+ };
+
+ this.$scope.showNoArtifactMessage = ():boolean => {
+ let artifacts:any = [];
+ artifacts = _.filter(this.$scope.artifacts, (artifact:ArtifactModel)=> {
+ return artifact.esId;
+ });
+
+ if (artifacts.length === 0) {
+ return true;
+ }
+ return false;
+ };
+
+ this.$scope.delete = (artifact:ArtifactModel):void => {
+
+ let onOk = ():void => {
+ this.$scope.isLoading = true;
+ let onSuccess = ():void => {
+ this.$scope.isLoading = false;
+ this.$scope.artifacts = <ArtifactModel[]>_.values(this.$scope.component.artifacts);
+ };
+
+ let onFailed = (error:any):void => {
+ console.log('Delete artifact returned error:', error);
+ this.$scope.isLoading = false;
+ };
+
+ this.$scope.component.deleteArtifact(artifact.uniqueId, artifact.artifactLabel).then(onSuccess, onFailed);
+ };
+
+ let title:string = this.$filter('translate')("ARTIFACT_VIEW_DELETE_MODAL_TITLE");
+ let message:string = this.$filter('translate')("ARTIFACT_VIEW_DELETE_MODAL_TEXT", "{'name': '" + artifact.artifactDisplayName + "'}");
+ this.ModalsHandler.openConfirmationModal(title, message, false).then(onOk);
+ };
+
+ this.$scope.clickArtifactName = (artifact:any) => {
+ if (!artifact.esId) {
+ this.$scope.addOrUpdate(artifact);
+ }
+
+ };
+ }
+}
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/information-artifacts/information-artifacts-view.html b/catalog-ui/src/app/view-models/workspace/tabs/information-artifacts/information-artifacts-view.html
new file mode 100644
index 0000000000..7c843e9fe8
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/information-artifacts/information-artifacts-view.html
@@ -0,0 +1,66 @@
+<div class="workspace-information-artifact">
+ <div data-tests-id="add-information-artifact-button" ng-if="!isViewMode()"
+ data-ng-class="{'disabled': isDisableMode()}"
+ data-tests-id="addGrey" class="add-btn" data-ng-click="addOrUpdate({})" type="button">Add </div>
+ <div class="table-container-flex">
+ <div class="table" data-ng-class="{'view-mode': isViewMode()}">
+ <div class="head flex-container">
+ <div class="table-header head-row hand flex-item" ng-repeat="header in tableHeadersList track by $index" data-ng-click="sort(header.property)">{{header.title}}
+ <span data-ng-show="sortBy === header.property" class="table-header-sort-arrow" data-ng-class="{'down': reverse, 'up':!reverse}"> </span>
+ </div>
+ <div class="table-no-text-header head-row flex-item"></div>
+ </div>
+ <div class="body">
+ <perfect-scrollbar suppress-scroll-x="true" scroll-y-margin-offset="0" include-padding="true" class="scrollbar-container">
+ <div data-ng-if="showNoArtifactMessage()" class="no-row-text" data-ng-class="{'disabled': isDisableMode()}">
+ There are no information artifacts to display
+ </div>
+ <div data-ng-repeat-start="artifact in artifacts| orderBy:sortBy:reverse track by $index" data-tests-id="InformationalArtifactRow"
+ class="flex-container data-row"
+ data-ng-class="{'selected': artifact.selected}"
+ data-ng-if="artifact.esId">
+
+ <div class="table-col-general flex-item" data-ng-click="artifact.selected = !artifact.selected" data-tests-id="artifactDisplayName_{{artifact.artifactDisplayName}}">
+ <span class="sprite table-arrow" data-ng-class="{'opened': artifact.selected}" data-tests-id="artifact_arrow_{{artifact.artifactDisplayName}}"></span>
+ {{artifact.artifactDisplayName}}
+ </div>
+
+ <div class="table-col-general flex-item" data-ng-click="artifact.selected = !artifact.selected" data-tests-id="artifactType_{{artifact.artifactDisplayName}}">
+ {{artifact.artifactType}}
+ </div>
+
+ <div class="table-col-general flex-item" data-ng-click="artifact.selected = !artifact.selected" data-tests-id="artifactVersion_{{artifact.artifactDisplayName}}">
+ {{artifact.artifactVersion}}
+ </div>
+
+ <div class="table-col-general flex-item text" data-ng-click="artifact.selected = !artifact.selected" data-tests-id="artifactUUID_{{artifact.artifactDisplayName}}"
+ tooltips tooltip-content="{{artifact.artifactUUID}}">
+ <span>{{artifact.artifactUUID}}</span>
+ </div>
+
+ <div class="table-btn-col flex-item">
+ <button class="table-edit-btn" data-tests-id="edit_{{artifact.artifactDisplayName}}" data-ng-if="!isViewMode() && !artifact.isThirdParty()" data-ng-click="addOrUpdate(artifact)" data-ng-class="{'disabled': isDisableMode()}"></button>
+ <button class="table-delete-btn" data-tests-id="delete_{{artifact.artifactDisplayName}}" data-ng-if="!isViewMode() && !artifact.isThirdParty()" data-ng-click="delete(artifact)" data-ng-class="{'disabled': isDisableMode()}"> </button>
+ <button class="table-download-btn" download-artifact data-tests-id="download_{{artifact.artifactDisplayName}}"
+ data-ng-if="artifact.artifactName" component="component" artifact="artifact"></button>
+ </div>
+ </div>
+ <div data-ng-repeat-end="" data-ng-if="artifact.selected" class="item-opened" data-tests-id="{{artifact.artifactDisplayName}}Description" data-ng-bind="artifact.description"></div>
+ <button class="add-button" data-ng-repeat="artifact in artifacts track by $index"
+ data-ng-show="!artifact.esId"
+ data-tests-id="{{artifact.artifactDisplayName}}"
+ ng-if="!isViewMode()"
+ data-ng-class="{'disabled': isDisableMode()}"
+ translate="DEPLOYMENT_ARTIFACT_BUTTON_ADD_HEAT"
+ translate-values="{'name': '{{artifact.artifactDisplayName}}'}"
+ data-ng-click="addOrUpdate(artifact)"></button>
+ <button class="add-button"
+ ng-if="!isViewMode()"
+ data-ng-class="{'disabled': isDisableMode()}"
+ translate="DEPLOYMENT_ARTIFACT_BUTTON_ADD_OTHER"
+ data-ng-click="addOrUpdate({})"></button>
+ </perfect-scrollbar>
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/information-artifacts/information-artifacts.less b/catalog-ui/src/app/view-models/workspace/tabs/information-artifacts/information-artifacts.less
new file mode 100644
index 0000000000..3ba9cf47d5
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/information-artifacts/information-artifacts.less
@@ -0,0 +1,57 @@
+.workspace-information-artifact {
+ width: 93%;
+ display: inline-block;
+ .w-sdc-classic-btn {
+ float: right;
+ margin-bottom: 10px;
+ }
+
+ .table{
+ height: 490px;
+ margin-bottom: 0;
+ }
+
+ .table-container-flex {
+ margin-top: 27px;
+
+ .item-opened{
+ word-wrap: break-word;
+ }
+
+ .text{
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: inline-block;
+ white-space: nowrap;
+ }
+
+ .flex-item:nth-child(1) {
+ flex-grow: 15;
+ .hand;
+ span.table-arrow {
+ margin-right: 7px;
+ }
+ }
+
+ .flex-item:nth-child(2) {
+ flex-grow: 6;
+ }
+
+ .flex-item:nth-child(3) {
+ flex-grow: 3;
+ }
+
+ .flex-item:nth-child(4) {
+ flex-grow: 20;
+ }
+
+ .flex-item:nth-child(5) {
+ flex-grow: 5;
+ padding-top: 10px;
+ }
+
+ }
+
+}
+
+
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/inputs/inputs.less b/catalog-ui/src/app/view-models/workspace/tabs/inputs/inputs.less
new file mode 100644
index 0000000000..eff5c5395b
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/inputs/inputs.less
@@ -0,0 +1,225 @@
+.workspace-inputs {
+
+ .sdc-workspace-container .w-sdc-main-right-container .w-sdc-main-container-body-content {
+ padding: 25px 8% 25px 8%;
+ }
+
+ .w-sdc-main-container .w-sdc-main-right-container > div:first-child {
+ /* scroll fix */
+ padding-bottom: 0px;
+ }
+
+
+ width: 100%;
+ display: flex;
+
+ .text {
+ padding-left: 15px;
+ .no-overflow;
+ }
+
+ .title-text {
+ color: @main_color_m;
+ .f-type._13_m;
+ .bold;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ }
+
+ .no-overflow {
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ }
+
+ .title-blue-text {
+ color: @main_color_a;
+ .f-type._13_m;
+
+ &.property-name-text {
+ padding-right: 13px;
+ border-right: 1px solid rgba(120, 136, 148, 0.26);
+ flex-grow: 1;
+ .no-overflow;
+ }
+ }
+
+ .instance-name-text {
+ flex-grow: 1;
+ }
+
+ ng-include {
+ width: 45%;
+ }
+
+ .w-sdc-inputs-search {
+ padding: 10px 20px 20px 0;
+ white-space: nowrap;
+ position: relative;
+ width: 60%;
+ height: 64px;
+
+ .inputs-search-icon {
+ top: 9px;
+ right: 11px;
+ }
+
+ .magnification-white {
+ .sprite-new;
+ .search-white-icon;
+ .hand;
+ }
+
+ .search-icon-container {
+ width: 35px;
+ height: 30px;
+ background-color: @main_color_a;
+ white-space: nowrap;
+ float: right;
+ position: relative;
+ bottom: 31px;
+ right: 1px;
+ border-radius: 0px 4px 4px 0px;
+ .hand
+ }
+ }
+
+ .total-inputs-count {
+ width: 100%;
+ font-weight: bold;
+ text-align: left;
+ }
+
+ .new-input-button {
+ margin: 9px 0 0 0;
+ }
+
+ .w-sdc-inputs-search-input {
+ border: 1px solid @color_e;
+ .border-radius(4px);
+ height: 32px;
+ margin: 0;
+ padding: 0px 28px 3px 10px;
+ vertical-align: 4px;
+ width: 100%;
+ outline: none;
+ font-style: italic;
+ }
+
+ .w-sdc-classic-btn {
+ float: right;
+ margin-bottom: 10px;
+ }
+
+ .prop-to-input-button {
+ position: absolute;
+ top: 50%;
+ margin-right: -20px;
+ margin-bottom: -10px;
+
+ }
+
+ .table-container-flex {
+
+ .flex-item {
+ line-height: 22px;
+ }
+ .expand-collapse-table-row {
+
+ .data-row {
+ background: @tlv_color_u;
+ .hand;
+ align-items: center;
+ padding: 0 12px;
+ margin: 1px 0px 1px 0px;
+ min-height: 40px;
+ }
+
+ .data-row:hover {
+ .bg_j;
+ }
+ }
+ }
+
+ .table {
+ height: 640px;
+ margin-bottom: 0;
+ clear: both;
+
+ .empty-row {
+ padding: 3px;
+ }
+
+ .flex-item {
+ line-height: 22px;
+ }
+
+ .table-header {
+
+ line-height: 14px;
+ background-color: @main_color_a;
+ color: @main_color_p;
+ text-align: left;
+ padding: 7px 5px 7px 10px;
+ .f-type._14_m;
+ }
+ .head {
+ background-color: #e6e6e6;
+ }
+
+ .body {
+ .scrollbar-container {
+ .perfect-scrollbar;
+ max-height: 610px;
+ }
+
+ .expand-collapse-inputs-table-icon {
+ .hand;
+ .sprite-new;
+ .arrow-up;
+ transition: .3s all;
+ position: relative;
+ left: 8px;
+ border: none !important;
+ padding: 0px 10px 0px 10px;
+ }
+
+ .table-col-text {
+ margin-left: 14px;
+ }
+ }
+ }
+
+ .inputs-header {
+ width: 100%;
+ position: relative;
+ bottom: 31px;
+ }
+
+ .inputs-tables-container {
+ width: 100%;
+ min-width: 100%;
+ display: flex;
+ }
+
+ .inputs-button-container {
+ width: 8%;
+ min-width: 8%;
+ display: flex;
+
+ .right-arrow-btn {
+ .sprite-new;
+ .blue-right-arrow-circle;
+ margin: auto;
+ cursor: pointer;
+ }
+ }
+
+ .table-container-flex {
+ margin-top: 27px;
+ width: 46%;
+ min-width: 46%;
+ display: inline-block;
+ float: left;
+ }
+}
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/inputs/resource-input/resource-inputs-view-model.ts b/catalog-ui/src/app/view-models/workspace/tabs/inputs/resource-input/resource-inputs-view-model.ts
new file mode 100644
index 0000000000..49fedd6e21
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/inputs/resource-input/resource-inputs-view-model.ts
@@ -0,0 +1,117 @@
+'use strict';
+import {IWorkspaceViewModelScope} from "app/view-models/workspace/workspace-view-model";
+import {ComponentInstance, InstancesInputsOrPropertiesMapData, Resource, PropertyModel, InputModel} from "app/models";
+import {ModalsHandler} from "app/utils";
+
+export interface IInputsViewModelScope extends IWorkspaceViewModelScope {
+ InstanceInputsProperties:InstancesInputsOrPropertiesMapData; //this is tha map object that hold the selected inputs and the inputs we already used
+ vfInstancesList:Array<ComponentInstance>;
+ component:Resource;
+
+ onArrowPressed():void;
+ getInputPropertiesForInstance(instanceId:string, instance:ComponentInstance):ng.IPromise<boolean> ;
+ loadInputPropertiesForInstance(instanceId:string, input:InputModel):ng.IPromise<boolean> ;
+ openEditValueModal(input:InputModel):void;
+ openEditPropertyModal(property:PropertyModel):void;
+}
+
+export class ResourceInputsViewModel {
+
+ static '$inject' = [
+ '$scope',
+ '$q',
+ 'ModalsHandler'
+ ];
+
+ constructor(private $scope:IInputsViewModelScope, private $q:ng.IQService, private ModalsHandler:ModalsHandler) {
+ this.initScope();
+ this.$scope.updateSelectedMenuItem();
+ }
+
+ private initScope = ():void => {
+
+ this.$scope.InstanceInputsProperties = new InstancesInputsOrPropertiesMapData();
+ this.$scope.vfInstancesList = this.$scope.component.componentInstances;
+
+ // Need to cast all inputs to InputModel for the search to work
+ let tmpInputs:Array<InputModel> = new Array<InputModel>();
+ _.each(this.$scope.component.inputs, (input):void => {
+ tmpInputs.push(new InputModel(input));
+ });
+ this.$scope.component.inputs = tmpInputs;
+ // This function is not supported for resource
+ //this.$scope.component.getComponentInputs();
+
+ /*
+ * When clicking on instance input in the left or right table, this function will load all properties of the selected input
+ */
+ this.$scope.getInputPropertiesForInstance = (instanceId:string, instance:ComponentInstance):ng.IPromise<boolean> => {
+ let deferred = this.$q.defer();
+ instance.properties = this.$scope.component.componentInstancesProperties[instanceId];
+ deferred.resolve(true);
+ return deferred.promise;
+ };
+
+ /*
+ * When clicking on instance input in the left or right table, this function will load all properties of the selected input
+ */
+ this.$scope.loadInputPropertiesForInstance = (instanceId:string, input:InputModel):ng.IPromise<boolean> => {
+ let deferred = this.$q.defer();
+
+ let onSuccess = (properties:Array<PropertyModel>) => {
+ input.properties = properties;
+ deferred.resolve(true);
+ };
+
+ let onError = () => {
+ deferred.resolve(false)
+ };
+
+ if (!input.properties) {
+ this.$scope.component.getComponentInstanceInputProperties(instanceId, input.uniqueId).then(onSuccess, onError);
+ } else {
+ deferred.resolve(true);
+ }
+ return deferred.promise;
+ };
+
+ /*
+ * When pressing the arrow, we create service inputs from the inputs selected
+ */
+ this.$scope.onArrowPressed = ():void => {
+ let onSuccess = (inputsCreated:Array<InputModel>) => {
+
+ //disabled all the inputs in the left table
+ _.forEach(this.$scope.InstanceInputsProperties, (properties:Array<PropertyModel>) => {
+ _.forEach(properties, (property:PropertyModel) => {
+ property.isAlreadySelected = true;
+ });
+ });
+
+ // Adding color to the new inputs (right table)
+ _.forEach(inputsCreated, (input) => {
+ input.isNew = true;
+ });
+
+ // Removing color to the new inputs (right table)
+ setTimeout(() => {
+ _.forEach(inputsCreated, (input) => {
+ input.isNew = false;
+ });
+ this.$scope.$apply();
+ }, 3000);
+ };
+
+ this.$scope.component.createInputsFormInstances(this.$scope.InstanceInputsProperties).then(onSuccess);
+ };
+
+ this.$scope.openEditValueModal = (input:InputModel) => {
+ this.ModalsHandler.openEditInputValueModal(input);
+ };
+
+ this.$scope.openEditPropertyModal = (property:PropertyModel):void => {
+ this.ModalsHandler.openEditPropertyModal(property, this.$scope.component, this.$scope.component.componentInstancesProperties[property.resourceInstanceUniqueId], false).then(() => {
+ });
+ }
+ }
+}
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/inputs/resource-input/resource-inputs-view.html b/catalog-ui/src/app/view-models/workspace/tabs/inputs/resource-input/resource-inputs-view.html
new file mode 100644
index 0000000000..a0715973ab
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/inputs/resource-input/resource-inputs-view.html
@@ -0,0 +1,86 @@
+<div class="workspace-inputs">
+ <div class="table-container-flex">
+ <div class="w-sdc-inputs-search pull-left hideme">
+ <input type="text" class="w-sdc-inputs-search-input" placeholder="Search"/>
+ <div class="search-icon-container">
+ <span class="w-sdc-search-icon inputs-search-icon magnification-white"></span>
+ </div>
+ </div>
+ <div class="table">
+ <div class="table-header">VFC instances inputs</div>
+ <div class="body">
+ <div class="table-loader" ng-class="{'tlv-loader large loader': isLoading}"></div>
+ <perfect-scrollbar scroll-y-margin-offset="0" class="scrollbar-container">
+
+ <expand-collapse expanded-selector=".vf-instance-list.{{$index}}"
+ class="expand-collapse-table-row"
+ load-data-function="getInputPropertiesForInstance(instance.uniqueId, instance)"
+ is-close-on-init="true"
+ data-ng-repeat-start="instance in vfInstancesList track by $index">
+ <div class="flex-container data-row" data-tests-id="input-instance-{{$index}}">
+ <div class="expand-collapse-inputs-table-icon"></div>
+ <div class="table-col-general flex-item text">
+ <span class="title-text">{{instance.name}}</span>
+ </div>
+ </div>
+
+ </expand-collapse>
+
+ <div data-ng-repeat-end="" class="vf-instance-list {{$index}}">
+ <div class="empty-row" data-tests-id="empty-row" ng-if="instance.properties.length===0">No properties to display </div>
+ <div ng-repeat="property in instance.properties track by $index">
+ <property-row property="property" instance-name="instance.name" on-name-clicked="openEditPropertyModal(property)"></property-row>
+ </div>
+
+ </div>
+
+ </perfect-scrollbar>
+ </div>
+ </div>
+ </div>
+
+ <div class="inputs-button-container pull-left">
+ <!--<div ng-click="onArrowPressed()" class="right-arrow-btn"></div>-->
+ </div>
+
+ <div class="table-container-flex">
+ <div class="w-sdc-inputs-search pull-left">
+ <input type="text" class="w-sdc-inputs-search-input" data-ng-model="search.filterTerm" placeholder="Search"
+ data-ng-model-options="{debounce: 200}"/>
+ <div class="search-icon-container">
+ <span class="w-sdc-search-icon inputs-search-icon magnification-white"></span>
+ </div>
+ </div>
+ <div class="table">
+ <div class="body">
+ <div class="table-header">VF inputs</div>
+ <perfect-scrollbar scroll-y-margin-offset="0" include-padding="true" class="scrollbar-container">
+ <expand-collapse expanded-selector=".resource-inputs.{{$index}}"
+ class="expand-collapse-table-row"
+ load-data-function="loadInputPropertiesForInstance(resourceInput.uniqueId, resourceInput)"
+ is-close-on-init="true"
+ data-ng-repeat-start="resourceInput in component.inputs | filter:search track by $index ">
+ <div class="input-row service-input-row">
+ <input-row input="resourceInput" is-view-only='isViewOnly'
+ instance-name='resourceInput.name'
+ data-tests-id="resource-input-{{$index}}"
+ class="service-input-row"
+ on-name-clicked="openEditValueModal(resourceInput)"
+ ng-class="resourceInput.isNew ? 'new-input': ''">
+
+ </input-row>
+ </div>
+ </expand-collapse>
+
+ <div data-ng-repeat-end="" class="input-inputs-list resource-inputs {{$index}}">
+ <div class="empty-row" ng-if="resourceInput.properties.length===0">No properties to display </div>
+ <div ng-repeat="property in resourceInput.properties track by $index">
+ <property-row property="property" instance-name="property.name"></property-row>
+ </div>
+ </div>
+
+ </perfect-scrollbar>
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/inputs/resource-input/resource-inputs.less b/catalog-ui/src/app/view-models/workspace/tabs/inputs/resource-input/resource-inputs.less
new file mode 100644
index 0000000000..ebb32fbdb2
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/inputs/resource-input/resource-inputs.less
@@ -0,0 +1,9 @@
+.workspace-inputs {
+
+ .property-row {
+ .input-check-box {
+ text-align: center;
+ }
+ }
+
+}
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/inputs/service-input/service-inputs-view-model.ts b/catalog-ui/src/app/view-models/workspace/tabs/inputs/service-input/service-inputs-view-model.ts
new file mode 100644
index 0000000000..f3b2de0943
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/inputs/service-input/service-inputs-view-model.ts
@@ -0,0 +1,378 @@
+'use strict';
+import {IWorkspaceViewModelScope} from "app/view-models/workspace/workspace-view-model";
+import {ComponentInstance, InstancesInputsOrPropertiesMapData, Service, IAppMenu, InputModel, PropertyModel, InputPropertyBase} from "app/models";
+import {DataTypesService} from "app/services";
+import {ModalsHandler, ResourceType} from "app/utils";
+
+
+interface IServiceInputsViewModelScope extends IWorkspaceViewModelScope {
+
+ vfInstancesList:Array<ComponentInstance>;
+ instanceInputsMap:InstancesInputsOrPropertiesMapData; //this is tha map object that hold the selected inputs and the inputs we already used
+ instancePropertiesMap:InstancesInputsOrPropertiesMapData;
+ component:Service;
+ sdcMenu:IAppMenu;
+ isViewOnly:boolean;
+ isArrowDisabled:boolean;
+ onArrowPressed():void;
+ checkArrowState():void;
+ loadComponentInputs():void;
+ loadInstanceInputs(instance:ComponentInstance):ng.IPromise<boolean> ;
+ loadInstanceProperties(instance:ComponentInstance):ng.IPromise<boolean> ;
+ loadInputPropertiesForInstance(instanceId:string, input:InputModel):ng.IPromise<boolean> ;
+ loadInputInputs(input:InputModel):ng.IPromise<boolean>;
+ deleteInput(input:InputModel):void
+ openEditValueModal(input:InputModel):void;
+ openSelectPropertyDataTypeViewModel(instanceId:string, property:PropertyModel):void;
+ openEditPropertyDataTypeViewModel(property:PropertyModel):void;
+ dataTypesService:DataTypesService;
+}
+
+export class ServiceInputsViewModel {
+
+ static '$inject' = [
+ '$scope',
+ '$q',
+ 'ModalsHandler',
+ 'Sdc.Services.DataTypesService'
+ ];
+
+ constructor(private $scope:IServiceInputsViewModelScope,
+ private $q:ng.IQService,
+ private ModalsHandler:ModalsHandler,
+ private DataTypesService:DataTypesService) {
+ this.initScope();
+ this.$scope.updateSelectedMenuItem();
+ this.$scope.isViewOnly = this.$scope.isViewMode();
+ }
+
+
+ private getInputsOrPropertiesAlreadySelected = (instanceNormalizeName:string, arrayToFilter:Array<InputPropertyBase>):Array<any> => {
+ let alreadySelectedInput = [];
+ _.forEach(arrayToFilter, (inputOrProperty:InputPropertyBase) => {
+ let expectedServiceInputName = instanceNormalizeName + '_' + inputOrProperty.name;
+ let inputAlreadyInService = _.find(this.$scope.component.inputs, (serviceInput:InputModel) => {
+ //Checking if the input prefix is the instance name + '_' + property/input name (prefix because we don't need to check full name for complex dataType)
+ return serviceInput.name.substring(0, expectedServiceInputName.length) === expectedServiceInputName;
+ });
+ if (inputAlreadyInService) {
+ inputOrProperty.isAlreadySelected = true;
+ alreadySelectedInput.push(inputOrProperty);
+ } else {
+ inputOrProperty.isAlreadySelected = false;
+ }
+ });
+ return alreadySelectedInput;
+ };
+
+
+ /*
+ * When loading the screen again, we need to disabled the inputs that already created on the service,
+ * we do that by comparing the service input name, to the instance name + '_' + the resource instance input name.
+ */
+ private disableEnableSelectedInputsOrPropertiesOnInit = (instance:ComponentInstance):void => {
+
+ if (instance.originType === ResourceType.VF) {
+ this.$scope.instanceInputsMap[instance.uniqueId] = this.getInputsOrPropertiesAlreadySelected(instance.normalizedName, instance.inputs);
+ } else {
+ this.$scope.instancePropertiesMap[instance.uniqueId] = this.getInputsOrPropertiesAlreadySelected(instance.normalizedName, instance.properties);
+ }
+ };
+
+ /*
+ * Enable Input/Property after delete
+ */
+ private enableInputsAfterDelete = (propertiesOrInputsDeletes:Array<InputPropertyBase>):void => {
+
+ _.forEach(propertiesOrInputsDeletes, (deletedInputInput:InputPropertyBase) => { //Enable all component instance inputs deleted
+
+ let inputOrPropertyDeleted:InputPropertyBase = _.find(this.$scope.instanceInputsMap[deletedInputInput.componentInstanceId], (inputOrProperty:InputPropertyBase) => {
+ return inputOrProperty.uniqueId === deletedInputInput.uniqueId;
+ });
+ inputOrPropertyDeleted.isAlreadySelected = false;
+ delete _.remove(this.$scope.instanceInputsMap[deletedInputInput.componentInstanceId], {uniqueId: inputOrPropertyDeleted.uniqueId})[0];
+ });
+ };
+
+ /*
+ * Enable Input/Property after delete
+ */
+ private enablePropertiesAfterDelete = (propertiesOrInputsDeletes:Array<InputPropertyBase>):void => {
+
+ _.forEach(propertiesOrInputsDeletes, (deletedInputInput:InputPropertyBase) => { //Enable all component instance inputs deleted
+ let componentInstance = _.find(this.$scope.vfInstancesList, (instance:ComponentInstance) => {
+ return instance.uniqueId === deletedInputInput.componentInstanceId;
+ });
+ let inputOrPropertyDeleted:InputPropertyBase = _.find(this.$scope.instancePropertiesMap[deletedInputInput.componentInstanceId], (inputOrProperty:InputPropertyBase) => {
+ return inputOrProperty.uniqueId === deletedInputInput.uniqueId;
+ });
+
+ let expectedName = componentInstance.normalizedName + '_' + inputOrPropertyDeleted.name;
+ let isAnotherInputExist = _.find(this.$scope.component.inputs, (input:InputModel) => {
+ return input.name.substring(0, expectedName.length) === expectedName;
+ });
+ if (!isAnotherInputExist) {
+ inputOrPropertyDeleted.isAlreadySelected = false;
+ delete _.remove(this.$scope.instancePropertiesMap[deletedInputInput.componentInstanceId], {uniqueId: inputOrPropertyDeleted.uniqueId})[0];
+ }
+ });
+ };
+
+ private initScope = ():void => {
+
+ this.$scope.instanceInputsMap = new InstancesInputsOrPropertiesMapData();
+ this.$scope.instancePropertiesMap = new InstancesInputsOrPropertiesMapData();
+ this.$scope.isLoading = true;
+ this.$scope.isArrowDisabled = true;
+ // Why do we need this? we call this later.
+ //this.$scope.component.getComponentInputs();
+
+ let onSuccess = (componentInstances:Array<ComponentInstance>) => {
+ console.log("component instances loaded: ", componentInstances);
+ this.$scope.vfInstancesList = componentInstances;
+ this.$scope.isLoading = false;
+ };
+
+ //This function will get al component instance for the left table - in
+ // future the instances will be filter according to search text
+ this.$scope.component.getComponentInstancesFilteredByInputsAndProperties().then(onSuccess);
+
+ // This function will get the service inputs for the right table
+ this.$scope.component.getComponentInputs();
+
+ /*
+ * When clicking on instance in the left table, this function will load all instance inputs
+ */
+ this.$scope.loadInstanceInputs = (instance:ComponentInstance):ng.IPromise<boolean> => {
+ let deferred = this.$q.defer();
+
+ let onSuccess = (inputs:Array<InputModel>) => {
+ instance.inputs = inputs;
+ this.disableEnableSelectedInputsOrPropertiesOnInit(instance);
+ deferred.resolve(true);
+ };
+
+ let onError = () => {
+ deferred.resolve(false);
+ };
+
+ if (!instance.inputs) {
+ this.$scope.component.getComponentInstanceInputs(instance.uniqueId, instance.componentUid).then(onSuccess, onError);
+ //this.disableEnableSelectedInputs(instance);
+ } else {
+ deferred.resolve(true);
+ }
+ return deferred.promise;
+ };
+
+
+ this.$scope.loadInstanceProperties = (instance:ComponentInstance):ng.IPromise<boolean> => {
+ let deferred = this.$q.defer();
+
+ let onSuccess = (properties:Array<PropertyModel>) => {
+ instance.properties = properties;
+ this.disableEnableSelectedInputsOrPropertiesOnInit(instance);
+ deferred.resolve(true);
+ };
+
+ let onError = () => {
+ deferred.resolve(false);
+ };
+
+ if (!instance.properties) {
+ this.$scope.component.getComponentInstanceProperties(instance.uniqueId).then(onSuccess, onError);
+ } else {
+ deferred.resolve(true);
+ }
+ return deferred.promise;
+ };
+
+ /*
+ * When clicking on instance input in the left or right table, this function will load all properties of the selected input
+ */
+ this.$scope.loadInputPropertiesForInstance = (instanceId:string, input:InputModel):ng.IPromise<boolean> => {
+ let deferred = this.$q.defer();
+
+ let onSuccess = (properties:Array<PropertyModel>) => {
+ input.properties = properties;
+ deferred.resolve(true);
+ };
+
+ let onError = () => {
+ deferred.resolve(false)
+ };
+
+ if (!input.properties) {
+ this.$scope.component.getComponentInstanceInputProperties(instanceId, input.uniqueId).then(onSuccess, onError);
+ } else {
+ deferred.resolve(true);
+ }
+ return deferred.promise;
+ };
+
+ /*
+ * When clicking on input in the right table, this function will load all inputs of the selected input
+ */
+ this.$scope.loadInputInputs = (input:InputModel):ng.IPromise<boolean> => {
+ let deferred = this.$q.defer();
+
+ let onSuccess = () => {
+ deferred.resolve(true);
+ };
+ let onError = () => {
+ deferred.resolve(false);
+ };
+
+ if (!input.inputs) { // Caching, if exists do not get it.
+ this.$scope.component.getServiceInputInputsAndProperties(input.uniqueId).then(onSuccess, onError);
+ } else {
+ deferred.resolve(true);
+ }
+ return deferred.promise;
+ };
+
+ /*
+ * When pressing the arrow, we create service inputs from the inputs selected
+ */
+ this.$scope.onArrowPressed = ():void => {
+ let onSuccess = (inputsCreated:Array<InputModel>) => {
+
+ //disabled all the inputs in the left table
+ _.forEach(this.$scope.instanceInputsMap, (inputs:Array<InputModel>, instanceId:string) => {
+ _.forEach(inputs, (input:InputModel) => {
+ input.isAlreadySelected = true;
+ });
+ });
+ _.forEach(this.$scope.instancePropertiesMap, (properties:Array<PropertyModel>, instanceId:string) => {
+ _.forEach(properties, (property:PropertyModel) => {
+ property.isAlreadySelected = true;
+ });
+ });
+ this.addColorToItems(inputsCreated);
+ };
+
+ let onFailed = (error:any) => {
+ this.$scope.isArrowDisabled = false;
+ console.log("Error declaring input/property");
+ };
+
+ this.$scope.isArrowDisabled = true;
+ this.$scope.component.createInputsFormInstances(this.$scope.instanceInputsMap, this.$scope.instancePropertiesMap).then(onSuccess, onFailed);
+ };
+
+
+ /* Iterates through array of selected inputs and properties and returns true if there is at least one new selection on left */
+ this.$scope.checkArrowState = ()=> {
+
+ let newInputSelected:boolean = _.some(this.$scope.instanceInputsMap, (inputs:Array<InputModel>) => {
+ return _.some(inputs, (input:InputModel)=> {
+ return input.isAlreadySelected === false;
+ });
+ });
+
+ let newPropSelected:boolean = _.some(this.$scope.instancePropertiesMap, (properties:Array<PropertyModel>) => {
+ return _.some(properties, (property:PropertyModel) => {
+ return property.isAlreadySelected === false;
+ });
+ });
+
+ this.$scope.isArrowDisabled = !(newInputSelected || newPropSelected);
+
+ };
+
+ this.$scope.deleteInput = (inputToDelete:InputModel):void => {
+
+ let onDelete = ():void => {
+
+ let onSuccess = (deletedInput:InputModel):void => {
+ if (deletedInput.inputs && deletedInput.inputs.length > 0) { // Enable input declared from input
+ this.enableInputsAfterDelete(deletedInput.inputs);
+ }
+
+ if (deletedInput.properties && deletedInput.properties.length > 0) { // Enable properties
+ this.enablePropertiesAfterDelete(deletedInput.properties);
+ }
+ deletedInput.isDeleteDisabled = false;
+ this.$scope.checkArrowState();
+
+ };
+
+ let onFailed = (error:any):void => {
+ console.log("Error deleting input");
+ inputToDelete.isDeleteDisabled = false;
+ };
+
+ inputToDelete.isDeleteDisabled = true;
+ this.addColorToItems([inputToDelete]);
+ this.$scope.component.deleteServiceInput(inputToDelete.uniqueId).then((deletedInput:InputModel):void => {
+ onSuccess(deletedInput);
+ }, onFailed);
+ };
+
+ // Get confirmation modal text from menu.json
+ let state = "deleteInput";
+ let title:string = this.$scope.sdcMenu.alertMessages[state].title;
+ let message:string = this.$scope.sdcMenu.alertMessages[state].message.format([inputToDelete.name]);
+
+ // Open confirmation modal
+ this.ModalsHandler.openAlertModal(title, message).then(onDelete);
+ };
+
+ this.$scope.openEditValueModal = (input:InputModel) => {
+ this.ModalsHandler.openEditInputValueModal(input);
+ };
+
+ this.$scope.openSelectPropertyDataTypeViewModel = (instanceId:string, property:PropertyModel) => {
+ //to open the select data type modal
+ let selectedInstance = _.find(this.$scope.vfInstancesList, {uniqueId: instanceId});
+ this.DataTypesService.selectedInstance = selectedInstance; //set the selected instance on the service for compering the input name on the service & the complex property
+ this.DataTypesService.selectedComponentInputs = this.$scope.component.inputs; // set all the service inputs on the data type service
+ let filteredPropertiesMap = _.filter(this.$scope.instancePropertiesMap[instanceId], (instanceProperty)=> {
+ return instanceProperty.name == property.name;
+ });//get all properties under the specific property
+ this.DataTypesService.selectedPropertiesName = property.propertiesName;
+
+ this.ModalsHandler.openSelectDataTypeModal(property, this.$scope.component, this.$scope.component.properties, filteredPropertiesMap).then((selectedProperty:PropertyModel)=> {
+ if (selectedProperty && selectedProperty.propertiesName) {
+ let propertyToUpdate:PropertyModel = _.find(selectedInstance.properties, {uniqueId: selectedProperty.uniqueId});
+ let existingProperty:PropertyModel = (<PropertyModel>_.find(this.$scope.instancePropertiesMap[instanceId], {uniqueId: propertyToUpdate.uniqueId}));
+
+ if (existingProperty) {
+ existingProperty.propertiesName = selectedProperty.propertiesName;
+ existingProperty.input = selectedProperty.input;
+ existingProperty.isAlreadySelected = false;
+ } else {
+ propertyToUpdate.propertiesName = selectedProperty.propertiesName;
+ propertyToUpdate.input = selectedProperty.input;
+ this.$scope.instancePropertiesMap[instanceId].push(propertyToUpdate);
+
+ }
+ this.$scope.checkArrowState();
+
+ }
+ });
+ };
+
+
+ this.$scope.openEditPropertyDataTypeViewModel = (property:PropertyModel)=> {
+ this.ModalsHandler.openEditPropertyModal(property, this.$scope.component, this.$scope.component.properties, false).then(() => {
+ });
+ }
+ };
+
+ private addColorToItems = (inputsCreated:Array<InputModel>):void => {
+
+ // Adding color to the new inputs (right table)
+ _.forEach(inputsCreated, (input) => {
+ input.isNew = true;
+ });
+
+ // Removing color to the new inputs (right table)
+ setTimeout(() => {
+ _.forEach(inputsCreated, (input) => {
+ input.isNew = false;
+ });
+ this.$scope.$apply();
+ }, 3000);
+ };
+}
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/inputs/service-input/service-inputs-view.html b/catalog-ui/src/app/view-models/workspace/tabs/inputs/service-input/service-inputs-view.html
new file mode 100644
index 0000000000..cb4d853f58
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/inputs/service-input/service-inputs-view.html
@@ -0,0 +1,134 @@
+<div class="workspace-inputs">
+ <div class="table-container-flex">
+ <div class="w-sdc-inputs-search pull-left hideme">
+ <input type="text" class="w-sdc-inputs-search-input" placeholder="Search"/>
+ <div class="search-icon-container">
+ <span class="w-sdc-search-icon inputs-search-icon magnification-white"></span>
+ </div>
+ </div>
+ <div class="table">
+ <div class="table-header">Resource instance inputs</div>
+ <div class="body">
+ <div class="table-loader" ng-class="{'tlv-loader large loader': isLoading}"></div>
+ <perfect-scrollbar scroll-y-margin-offset="0" class="scrollbar-container">
+
+ <expand-collapse expanded-selector=".vf-instance-list.{{$index}}"
+ class="expand-collapse-table-row"
+ load-data-function="instance.originType=='VF' ? loadInstanceInputs(instance):loadInstanceProperties(instance)"
+ is-close-on-init="true"
+ data-ng-repeat-start="instance in vfInstancesList track by $index">
+ <div class="flex-container data-row">
+ <div class="expand-collapse-inputs-table-icon"></div>
+ <div class="table-col-general flex-item text" data-tests-id="inputs-vf-instance-{{$index}}">
+ <span class="title-text">{{instance.name}}</span>
+ </div>
+ </div>
+ </expand-collapse>
+
+ <div data-ng-repeat-end="" class="vf-instance-list {{$index}}">
+ <div data-ng-if="instance.originType=='VF'">
+
+ <expand-collapse expanded-selector=".input-list.{{$parent.$index}}-{{$index}}"
+ class="expand-collapse-table-row"
+ load-data-function="loadInputPropertiesForInstance(instance.uniqueId, input)"
+ is-close-on-init="true"
+ data-ng-repeat-start="input in instance.inputs track by $index">
+ <input-row input="input"
+ is-view-only='isViewOnly'
+ instance-id='instance.uniqueId'
+ instance-name='instance.name'
+ instance-inputs-map="instanceInputsMap"
+ on-checkbox-clicked="checkArrowState()"></input-row>
+ </expand-collapse>
+
+ <div data-ng-repeat-end="" class="input-list {{$parent.$index}}-{{$index}}">
+ <div class="empty-row" ng-if="input.properties.length===0">No properties to display
+ </div>
+ <div ng-repeat="property in input.properties track by $index">
+ <property-row property="property" instance-name="instance.name"></property-row>
+ </div>
+ </div>
+ </div>
+ <div data-ng-if="instance.originType!='VF'">
+ <div class="empty-row" data-tests-id="empty-row" ng-if="instance.properties.length===0"> No
+ properties to display
+ </div>
+ <div ng-repeat="property in instance.properties track by $index">
+ <property-row instance-properties-map="instancePropertiesMap" property="property"
+ on-name-clicked="openSelectPropertyDataTypeViewModel(instance.uniqueId,property)"
+ on-checkbox-clicked="checkArrowState()"
+ instance-name="instance.name"
+ instance-id='instance.uniqueId'></property-row>
+ </div>
+ </div>
+ </div>
+
+ </perfect-scrollbar>
+ </div>
+ </div>
+ </div>
+
+ <div class="inputs-button-container pull-left">
+ <div ng-click="onArrowPressed()" ng-class="{disabled: isArrowDisabled || isViewOnly}" data-ng-disabled="isArrowDisabled || isViewOnly" class="right-arrow-btn" data-tests-id="add-inputs-to-service-button"></div>
+ </div>
+
+ <div class="table-container-flex">
+ <div class="w-sdc-inputs-search pull-left">
+ <input type="text" class="w-sdc-inputs-search-input" data-ng-model="search.filterTerm" placeholder="Search"
+ data-ng-model-options="{debounce: 200}"/>
+ <div class="search-icon-container">
+ <span class="w-sdc-search-icon inputs-search-icon magnification-white"></span>
+ </div>
+ </div>
+ <div class="table">
+ <div class="body">
+ <div class="table-header">Service inputs</div>
+ <perfect-scrollbar scroll-y-margin-offset="0" include-padding="true" class="scrollbar-container">
+ <expand-collapse expanded-selector=".service-inputs.{{$index}}"
+ class="expand-collapse-table-row"
+ load-data-function="loadInputInputs(serviceInput)"
+ is-close-on-init="true"
+ data-ng-repeat-start="serviceInput in component.inputs | filter:search track by $index ">
+ <input-row input="serviceInput" is-view-only='isViewOnly' instance-name='serviceInput.name'
+ delete-input='deleteInput(serviceInput)'
+ data-tests-id="service-input-{{$index}}"
+ class="service-input-row"
+ on-name-clicked="openEditValueModal(serviceInput)"
+ ng-class="serviceInput.isNew ? 'new-input': ''"
+ ></input-row>
+ </expand-collapse>
+
+ <div data-ng-repeat-end="" class="service-inputs {{$index}}">
+ <div ng-if="serviceInput.inputs.length > 0">
+ <expand-collapse expanded-selector=".input-inputs-list.{{$parent.$index}}-{{$index}}"
+ class="expand-collapse-table-row"
+ load-data-function="loadInputPropertiesForInstance(input.componentInstanceId, input)"
+ is-close-on-init="true"
+ data-ng-repeat-start="input in serviceInput.inputs track by $index">
+ <input-row input="input"
+ is-view-only='isViewOnly'
+ instance-name='input.componentInstanceName'></input-row>
+ </expand-collapse>
+
+ <div data-ng-repeat-end="" class="input-inputs-list {{$parent.$index}}-{{$index}}">
+ <div class="empty-row" ng-if="input.properties.length===0">No properties to display
+ </div>
+ <div ng-repeat="property in input.properties track by $index">
+ <property-row property="property" instance-name="property.name" is-clickable="false"></property-row>
+ </div>
+ </div>
+ </div>
+ <div ng-if="serviceInput.properties.length > 0">
+ <div class="empty-row" ng-if="serviceInput.properties.length===0">No properties to display</div>
+ <div ng-repeat="property in serviceInput.properties track by $index">
+ <property-row property="property" instance-name="property.name" is-clickable="false"></property-row>
+ </div>
+ </div>
+ </div>
+
+
+ </perfect-scrollbar>
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/inputs/service-input/service-inputs.less b/catalog-ui/src/app/view-models/workspace/tabs/inputs/service-input/service-inputs.less
new file mode 100644
index 0000000000..f783d0b6d6
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/inputs/service-input/service-inputs.less
@@ -0,0 +1,47 @@
+.workspace-inputs {
+
+ .service-inputs-view {
+
+ .table-container-flex {
+ width:100% !important;
+ }
+
+ .table-loader {
+ position: relative;
+ top:215px;
+ }
+
+ }
+
+ .infinite-scroll {
+
+ overflow-y: scroll;
+ overflow-x: hidden;
+ max-height: 400px;
+ }
+
+ .class_with_css_props_leading_to_a_scroll {
+ height: 100%;
+ overflow-y: auto;
+ }
+
+ .table-container-flex {
+ .expand-collapse-table-row {
+ .service-input-row {
+ padding-left: 15px;
+ border: none;
+ border-bottom: rgba(120, 136, 148, 0.26) solid 1px;
+
+ &.service-input-row {
+ background: @tlv_color_u;
+ &.new-input {
+ background: @tlv_color_v;
+ }
+ }
+ }
+
+
+ }
+ }
+
+}
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/management-workflow/management-workflow-view-model.ts b/catalog-ui/src/app/view-models/workspace/tabs/management-workflow/management-workflow-view-model.ts
new file mode 100644
index 0000000000..b7428e990b
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/management-workflow/management-workflow-view-model.ts
@@ -0,0 +1,127 @@
+ 'use strict';
+ import {ArtifactType} from "app/utils";
+ import {ArtifactGroupModel} from "app/models";
+ import {participant} from "app/view-models/workspace/tabs/network-call-flow/network-call-flow-view-model";
+ import {IWorkspaceViewModelScope} from "app/view-models/workspace/workspace-view-model";
+ import {ComponentGenericResponse} from "../../../../ng2/services/responses/component-generic-response";
+ import {ComponentServiceNg2} from "../../../../ng2/services/component-services/component.service";
+
+ export interface IManagementWorkflowViewModelScope extends IWorkspaceViewModelScope {
+ vendorModel:VendorModel;
+ }
+
+ export class VendorModel {
+ artifacts: ArtifactGroupModel;
+ serviceID: string;
+ readonly: boolean;
+ sessionID: string;
+ requestID: string;
+ diagramType: string;
+ participants:Array<participant>;
+
+ constructor(artifacts: ArtifactGroupModel, serviceID:string, readonly:boolean, sessionID:string,
+ requestID:string, diagramType:string, participants:Array<participant>){
+ this.artifacts = artifacts;
+ this.serviceID = serviceID;
+ this.readonly = readonly;
+ this.sessionID = sessionID;
+ this.requestID = requestID;
+ this.diagramType = diagramType;
+ this.participants = participants;
+ }
+ }
+
+ export class ManagementWorkflowViewModel {
+
+ static '$inject' = [
+ '$scope',
+ 'uuid4',
+ 'ComponentServiceNg2'
+ ];
+
+ constructor(private $scope:IManagementWorkflowViewModelScope,
+ private uuid4:any,
+ private ComponentServiceNg2: ComponentServiceNg2) {
+
+ this.initInformationalArtifacts();
+ this.$scope.updateSelectedMenuItem();
+ }
+
+
+ private initInformationalArtifacts = ():void => {
+ if(!this.$scope.component.artifacts) {
+ this.$scope.isLoading = true;
+ this.ComponentServiceNg2.getComponentInformationalArtifacts(this.$scope.component).subscribe((response:ComponentGenericResponse) => {
+ this.$scope.component.artifacts = response.artifacts;
+ this.initScope();
+ this.$scope.isLoading = false;
+ });
+ } else {
+ this.initScope();
+ }
+ }
+
+ private static getParticipants():Array<participant> {
+ return [
+ {
+ "id": "1",
+ "name": "Customer"},
+ {
+ "id": "2",
+ "name": "CCD"
+ },
+ {
+ "id": "3",
+ "name": "Infrastructure"
+ },
+ {
+ "id": "4",
+ "name": "MSO"
+ },
+ {
+ "id": "5",
+ "name": "SDN-C"
+ },
+ {
+ "id": "6",
+ "name": "A&AI"
+ },
+ {
+ "id": "7",
+ "name": "APP-C"
+ },
+ {
+ "id": "8",
+ "name": "Cloud"
+ },
+ {
+ "id": "9",
+ "name": "DCAE"
+ },
+ {
+ "id": "10",
+ "name": "ALTS"
+ },
+ {
+ "id": "11",
+ "name": "VF"
+ }
+ ]
+ }
+
+
+ private initScope():void {
+ this.$scope.vendorModel = new VendorModel(
+ this.$scope.component.artifacts.filteredByType(ArtifactType.THIRD_PARTY_RESERVED_TYPES.WORKFLOW),
+ this.$scope.component.uniqueId,
+ this.$scope.isViewMode(),
+ this.$scope.user.userId,
+ this.uuid4.generate(),
+ ArtifactType.THIRD_PARTY_RESERVED_TYPES.WORKFLOW,
+ ManagementWorkflowViewModel.getParticipants()
+ );
+
+ this.$scope.thirdParty = true;
+ this.$scope.setValidState(true);
+ }
+ }
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/management-workflow/management-workflow-view.html b/catalog-ui/src/app/view-models/workspace/tabs/management-workflow/management-workflow-view.html
new file mode 100644
index 0000000000..bd196daec8
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/management-workflow/management-workflow-view.html
@@ -0,0 +1,3 @@
+<div class="workspace-management-workflow">
+ <punch-out name="'sequence-diagram'" data="vendorModel" user="user" on-event="onVendorEvent"></punch-out>
+</div>
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/network-call-flow/network-call-flow-view-model.ts b/catalog-ui/src/app/view-models/workspace/tabs/network-call-flow/network-call-flow-view-model.ts
new file mode 100644
index 0000000000..511c17eb60
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/network-call-flow/network-call-flow-view-model.ts
@@ -0,0 +1,79 @@
+'use strict';
+import {IWorkspaceViewModelScope} from "app/view-models/workspace/workspace-view-model";
+import {VendorModel} from "app/view-models/workspace/tabs/management-workflow/management-workflow-view-model";
+import {ResourceType, ArtifactType} from "app/utils";
+import {ComponentInstance} from "app/models";
+import {ComponentGenericResponse} from "../../../../ng2/services/responses/component-generic-response";
+import {ComponentServiceNg2} from "../../../../ng2/services/component-services/component.service";
+
+export interface INetworkCallFlowViewModelScope extends IWorkspaceViewModelScope {
+ vendorMessageModel:VendorModel;
+}
+
+export class participant {
+ name:string;
+ id:string;
+
+ constructor(instance:ComponentInstance) {
+ this.name = instance.name;
+ this.id = instance.uniqueId;
+ }
+}
+
+
+export class NetworkCallFlowViewModel {
+
+ static '$inject' = [
+ '$scope',
+ 'uuid4',
+ 'ComponentServiceNg2'
+ ];
+
+ constructor(private $scope:INetworkCallFlowViewModelScope,
+ private uuid4:any,
+ private ComponentServiceNg2: ComponentServiceNg2) {
+
+ this.initComponentInstancesAndInformationalArtifacts();
+ this.$scope.updateSelectedMenuItem();
+ }
+
+ private getVFParticipantsFromInstances(instances:Array<ComponentInstance>):Array<participant> {
+ let participants = [];
+ _.forEach(instances, (instance)=> {
+ if (ResourceType.VF == instance.originType) {
+ participants.push(new participant(instance));
+ }
+ });
+ return participants;
+ }
+
+ private initComponentInstancesAndInformationalArtifacts = ():void => {
+ if(!this.$scope.component.artifacts || !this.$scope.component.componentInstances) {
+ this.$scope.isLoading = true;
+ this.ComponentServiceNg2.getComponentInformationalArtifactsAndInstances(this.$scope.component).subscribe((response:ComponentGenericResponse) => {
+ this.$scope.component.artifacts = response.artifacts;
+ this.$scope.component.componentInstances = response.componentInstances;
+ this.initScope();
+ this.$scope.isLoading = false;
+ });
+ } else {
+ this.initScope();
+ }
+ }
+
+ private initScope():void {
+ this.$scope.vendorMessageModel = new VendorModel(
+ this.$scope.component.artifacts.filteredByType(ArtifactType.THIRD_PARTY_RESERVED_TYPES.NETWORK_CALL_FLOW),
+ this.$scope.component.uniqueId,
+ this.$scope.isViewMode(),
+ this.$scope.user.userId,
+ this.uuid4.generate(),
+ ArtifactType.THIRD_PARTY_RESERVED_TYPES.NETWORK_CALL_FLOW,
+ this.getVFParticipantsFromInstances(this.$scope.component.componentInstances)
+ );
+
+ this.$scope.thirdParty = true;
+ this.$scope.setValidState(true);
+ }
+
+}
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/network-call-flow/network-call-flow-view.html b/catalog-ui/src/app/view-models/workspace/tabs/network-call-flow/network-call-flow-view.html
new file mode 100644
index 0000000000..6ce3e8e2b7
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/network-call-flow/network-call-flow-view.html
@@ -0,0 +1,3 @@
+<div class="workspace-network-call-flow">
+ <punch-out name="'sequence-diagram'" data="vendorMessageModel" user="user" on-event="onVendorEvent"></punch-out>
+</div>
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/product-hierarchy/product-hierarchy-view-model.ts b/catalog-ui/src/app/view-models/workspace/tabs/product-hierarchy/product-hierarchy-view-model.ts
new file mode 100644
index 0000000000..233b12952b
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/product-hierarchy/product-hierarchy-view-model.ts
@@ -0,0 +1,109 @@
+'use strict';
+import {ComponentFactory} from "app/utils";
+import {Product, IGroup, ISubCategory, IMainCategory} from "app/models";
+import {IWorkspaceViewModelScope} from "app/view-models/workspace/workspace-view-model";
+import {CacheService} from "app/services";
+
+export interface IProductHierarchyScope extends IWorkspaceViewModelScope {
+
+ categoriesOptions:Array<IMainCategory>;
+ product:Product;
+ isLoading:boolean;
+ showDropDown:boolean;
+
+ onInputTextClicked():void;
+ onGroupSelected(category:IMainCategory, subcategory:ISubCategory, group:IGroup):void;
+ clickOutside():void;
+ deleteGroup(uniqueId:string):void;
+}
+
+export class ProductHierarchyViewModel {
+
+ static '$inject' = [
+ '$scope',
+ 'Sdc.Services.CacheService',
+ 'ComponentFactory',
+ '$state'
+ ];
+
+ constructor(private $scope:IProductHierarchyScope,
+ private cacheService:CacheService,
+ private ComponentFactory:ComponentFactory,
+ private $state:ng.ui.IStateService) {
+
+
+ this.$scope.product = <Product>this.$scope.getComponent();
+ this.$scope.setValidState(true);
+ this.initScope();
+ this.$scope.updateSelectedMenuItem();
+ }
+
+ private initCategories = () => {
+ this.$scope.categoriesOptions = angular.copy(this.cacheService.get('productCategories'));
+ let selectedGroup:Array<IGroup> = [];
+ _.forEach(this.$scope.product.categories, (category:IMainCategory) => {
+ _.forEach(category.subcategories, (subcategory:ISubCategory) => {
+ selectedGroup = selectedGroup.concat(subcategory.groupings);
+ });
+ });
+ _.forEach(this.$scope.categoriesOptions, (category:IMainCategory) => {
+ _.forEach(category.subcategories, (subcategory:ISubCategory) => {
+ _.forEach(subcategory.groupings, (group:ISubCategory) => {
+ let componentGroup:IGroup = _.find(selectedGroup, (componentGroupObj) => {
+ return componentGroupObj.uniqueId == group.uniqueId;
+ });
+ if (componentGroup) {
+ group.isDisabled = true;
+ }
+ });
+ });
+ });
+ };
+
+ private setFormValidation = ():void => {
+
+ this.$scope.setValidState(true);
+
+ };
+
+ private initScope = ():void => {
+ this.$scope.isLoading = false;
+ this.$scope.showDropDown = false;
+ this.initCategories();
+ this.setFormValidation();
+
+ this.$scope.onGroupSelected = (category:IMainCategory, subcategory:ISubCategory, group:IGroup):void => {
+ this.$scope.product.addGroup(category, subcategory, group);
+ this.$state.current.data.unsavedChanges = !this.$scope.isViewMode();
+ group.isDisabled = true;
+ this.$scope.showDropDown = false;
+ this.setFormValidation();
+ };
+
+ this.$scope.onInputTextClicked = ():void => {//just edit the component in place, no pop up nor server update ?
+ this.$scope.showDropDown = !this.$scope.showDropDown;
+ };
+
+ this.$scope.clickOutside = ():any => {
+ this.$scope.showDropDown = false;
+ };
+
+ this.$scope.deleteGroup = (uniqueId:string):void => {
+ //delete group from component
+ this.$scope.product.deleteGroup(uniqueId);
+ this.$state.current.data.unsavedChanges = !this.$scope.isViewMode();
+ this.setFormValidation();
+ //enabled group
+ _.forEach(this.$scope.categoriesOptions, (category:IMainCategory) => {
+ _.forEach(category.subcategories, (subcategory:ISubCategory) => {
+ let groupObj:IGroup = _.find(subcategory.groupings, (group) => {
+ return group.uniqueId === uniqueId;
+ });
+ if (groupObj) {
+ groupObj.isDisabled = false;
+ }
+ });
+ });
+ }
+ };
+}
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/product-hierarchy/product-hierarchy-view.html b/catalog-ui/src/app/view-models/workspace/tabs/product-hierarchy/product-hierarchy-view.html
new file mode 100644
index 0000000000..2335ad7c74
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/product-hierarchy/product-hierarchy-view.html
@@ -0,0 +1,40 @@
+<div class="workspace-hierarchy">
+ <div class="dropdown-container" clicked-outside="{onClickedOutside: 'clickOutside()', clickedOutsideEnable: 'true'}" >
+ <input placeholder="Add Group" data-ng-click="onInputTextClicked()" class="dropdown-input-text" data-ng-model="search.filterTerms" data-ng-disabled="isViewMode()" data-ng-class="{'view-mode': isViewMode()}" data-ng-model-options="{debounce: 200}"/>
+ <div data-ng-class="{'show': showDropDown}" class="dropdown-content" >
+ <perfect-scrollbar scroll-y-margin-offset="0" include-padding="false" class="scrollbar-container">
+ <div ng-repeat="category in categoriesOptions track by $index">
+ <div ng-repeat="subcategory in category.subcategories track by $index">
+ <div class="dropdown-option" ng-show="!category.filteredGroup || category.filteredGroup.length > 0">
+ <div class="category-container">
+ <div class="category">{{category.name}}</div>
+ <div class="subcategory">{{subcategory.name}}</div>
+ </div>
+ <div class="groupings-container">
+ <div ng-init="group.filterTerms = group.name + ' ' + category.name + ' ' + subcategory.name"
+ ng-repeat="group in (category.filteredGroup = (subcategory.groupings | filter:search )) track by $index">
+ <div class="group" data-ng-disabled="group.isDisabled" data-ng-class="{'disabled-group': group.isDisabled}" ng-click="onGroupSelected(category, subcategory, group)">
+ <span >{{group.name}}</span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </perfect-scrollbar>
+ </div>
+ </div>
+ <div class="hierarchy-groups-container no-border-top" data-ng-class="{'view-mode': isViewMode()}">
+ <perfect-scrollbar scroll-y-margin-offset="0" include-padding="true" class="scrollbar-container">
+ <div ng-if="!product.categories.length || product.categories.length === 0" class="no-groups-text" translate="NEW_PRODUCT_NO_CATEGORIES_TO_DISPLAY"></div>
+ <div ng-repeat="category in product.categories track by $index">
+ <div ng-repeat="subcategory in category.subcategories track by $index">
+ <div class="group-tag" ng-repeat="group in subcategory.groupings track by $index"
+ data-ng-init="tooltip = '<b>' + category.name + '</b><br />' + subcategory.name">
+ <sdc-tag sdc-disable="isViewMode()" data-on-delete="deleteGroup(uniqueId)" data-tag-data="{tag: group.name, tooltip: tooltip, id: group.uniqueId }"></sdc-tag>
+ </div>
+ </div>
+ </div>
+ </perfect-scrollbar>
+ </div>
+</div>
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/product-hierarchy/product-hierarchy.less b/catalog-ui/src/app/view-models/workspace/tabs/product-hierarchy/product-hierarchy.less
new file mode 100644
index 0000000000..c992558ed2
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/product-hierarchy/product-hierarchy.less
@@ -0,0 +1,130 @@
+.workspace-hierarchy {
+ display: inline-block;
+ width: 93%;
+
+ .scrollbar-container{
+ max-height:400px;
+ .perfect-scrollbar;
+ }
+
+ .dropdown-container {
+ position: relative;
+ display: inline-block;
+ width: 100%;
+
+ &:after{
+ top: 47%;
+ right: 1%;
+ border: solid transparent;
+ content: " ";
+ height: 0;
+ width: 0;
+ position: absolute;
+ pointer-events: none;
+ border-color: rgba(0, 0, 0, 0);
+ border-top-color: black;
+ border-width: 4px;
+ margin-left: -4px;
+ }
+
+ .dropdown-input-text {
+ width: 100%;
+ padding: 4px 10px;
+ }
+
+ .dropdown-content {
+ .perfect-scrollbar;
+ border: 1px solid #d8d8d8;
+ display: none;
+ position: absolute;
+ overflow: hidden;
+ width: 100%;
+ .bg_c;
+ max-height: 400px;
+ z-index: 999999;
+
+ .dropdown-option {
+ border-bottom: 1px solid #d8d8d8;
+ display: inline-block;
+ width: 100%;
+ }
+
+ .category-container{
+ width: 250px;
+ float: left;
+ padding-left: 5px;
+
+ .category {
+ .bold;
+ padding: 3px 3px 2px 3px;
+ &:after{
+ .sprite;
+ .arrow-left;
+ content: '';
+ margin-left: 5px;
+ transform: rotate(180deg);
+ }
+ }
+ .subcategory {
+ padding-left: 3px;
+ }
+ }
+
+ .groupings-container{
+ display: inline-block;
+ width: 424px;
+ border-left: 1px solid #d8d8d8;
+ min-height: 55px;
+ .group{
+ padding: 3px 3px 3px 10px;
+ &:hover{
+ .hand;
+ .bg_n;
+ }
+ &.disabled-group {
+ opacity: 0.5;
+ &:hover{
+ cursor: auto;
+ .bg_c;
+ }
+ }
+ }
+ }
+
+ .seperator {
+ height: 1px;
+ width: 100%;
+ .bg_j;
+ margin: 5px 0px;
+ }
+ }
+ .show {
+ display: block;
+ }
+ }
+
+ .hierarchy-groups-container{
+ .b_9;
+ width: 100%;
+ border: 1px solid #d8d8d8;
+ height: 425px;
+ padding: 15px;
+ text-align: center;
+
+ .scrollbar-container {
+ z-index: 0;
+ }
+
+ .no-group-text{
+ text-align: center;
+ margin-top:25px;
+ a {
+ cursor: pointer;
+ }
+ }
+ .group-tag{
+ display: inline-block;
+ float: left;
+ }
+ }
+}
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/properties/properties-view-model.ts b/catalog-ui/src/app/view-models/workspace/tabs/properties/properties-view-model.ts
new file mode 100644
index 0000000000..923cc58a7e
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/properties/properties-view-model.ts
@@ -0,0 +1,92 @@
+'use strict';
+import {IWorkspaceViewModelScope} from "app/view-models/workspace/workspace-view-model";
+import {PropertyModel} from "app/models";
+import {ModalsHandler} from "app/utils";
+import {COMPONENT_FIELDS} from "../../../../utils/constants";
+import {ComponentGenericResponse} from "../../../../ng2/services/responses/component-generic-response";
+import {ComponentServiceNg2} from "../../../../ng2/services/component-services/component.service";
+
+interface IPropertiesViewModelScope extends IWorkspaceViewModelScope {
+ tableHeadersList:Array<any>;
+ reverse:boolean;
+ sortBy:string;
+ filteredProperties:Array<PropertyModel>;
+
+ addOrUpdateProperty(property?:PropertyModel):void;
+ delete(property:PropertyModel):void;
+ sort(sortBy:string):void;
+}
+
+export class PropertiesViewModel {
+
+ static '$inject' = [
+ '$scope',
+ '$filter',
+ 'ModalsHandler',
+ 'ComponentServiceNg2'
+ ];
+
+
+ constructor(private $scope:IPropertiesViewModelScope,
+ private $filter:ng.IFilterService,
+ private ModalsHandler:ModalsHandler,
+ private ComponentServiceNg2:ComponentServiceNg2) {
+ this.initComponentProperties();
+ this.$scope.updateSelectedMenuItem();
+ }
+
+ private initComponentProperties = ():void => {
+
+ if(!this.$scope.component.properties) {
+ this.$scope.isLoading = true;
+ this.ComponentServiceNg2.getComponentProperties(this.$scope.component).subscribe((response:ComponentGenericResponse) => {
+ this.$scope.component.properties = response.properties;
+ this.initScope();
+ this.$scope.isLoading = false;
+ }, () => {
+ this.$scope.isLoading = false;
+ });
+ } else {
+ this.initScope();
+ }
+ }
+
+ private openEditPropertyModal = (property:PropertyModel):void => {
+ this.ModalsHandler.openEditPropertyModal(property, this.$scope.component, this.$scope.filteredProperties, false).then(() => {
+ });
+ };
+
+ private initScope = ():void => {
+
+ //let self = this;
+ this.$scope.filteredProperties = this.$scope.component.properties;
+ this.$scope.sortBy = 'name';
+ this.$scope.reverse = false;
+ this.$scope.setValidState(true);
+ this.$scope.tableHeadersList = [
+ {title: 'Name', property: 'name'},
+ {title: 'Type', property: 'type'},
+ {title: 'Schema', property: 'schema.property.type'},
+ {title: 'Description', property: 'description'},
+ ];
+ this.$scope.sort = (sortBy:string):void => {
+ this.$scope.reverse = (this.$scope.sortBy === sortBy) ? !this.$scope.reverse : false;
+ this.$scope.sortBy = sortBy;
+ };
+
+
+ this.$scope.addOrUpdateProperty = (property?:PropertyModel):void => {
+ this.openEditPropertyModal(property ? property : new PropertyModel());
+ };
+
+ this.$scope.delete = (property:PropertyModel):void => {
+
+ let onOk = ():void => {
+ this.$scope.component.deleteProperty(property.uniqueId);
+ };
+ let title:string = this.$filter('translate')("PROPERTY_VIEW_DELETE_MODAL_TITLE");
+ let message:string = this.$filter('translate')("PROPERTY_VIEW_DELETE_MODAL_TEXT", "{'name': '" + property.name + "'}");
+ this.ModalsHandler.openConfirmationModal(title, message, false).then(onOk);
+ };
+ }
+}
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/properties/properties-view.html b/catalog-ui/src/app/view-models/workspace/tabs/properties/properties-view.html
new file mode 100644
index 0000000000..d1e0582386
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/properties/properties-view.html
@@ -0,0 +1,59 @@
+<div class="workspace-properties">
+ <div id="left-top-bar">
+ <span id="properties-count">Total Properties: {{component.properties.length}}</span>
+ <input id="search-by-name" type="search" placeholder="Search" data-ng-model-options="{debounce: 200}" data-ng-model="filterTerms"/>
+ <span class="sprite magnification-glass search-button"></span>
+ </div>
+ <div class="add-btn" data-tests-id="addGrey" ng-if="!isViewMode()"
+ data-ng-class="{'disabled': isDisableMode()}" data-ng-click="addOrUpdateProperty()">Add Property</div>
+ <div class="table-container-flex">
+ <div class="table" data-ng-class="{'view-mode': isViewMode()}">
+ <div class="head flex-container">
+ <div class="table-header head-row hand flex-item" data-ng-repeat="header in tableHeadersList track by $index" data-ng-click="sort(header.property)">{{header.title}}
+ <span data-ng-if="sortBy === header.property" class="table-header-sort-arrow" data-ng-class="{'down': reverse, 'up':!reverse}"> </span>
+ </div>
+ <div class="table-no-text-header head-row flex-item" ng-if="!isViewMode()"><span class="delete-col-header"></span></div>
+ <!--div class="table-no-text-header head-row flex-item"></div-->
+ </div>
+
+ <div class="body">
+ <perfect-scrollbar scroll-y-margin-offset="0" include-padding="true" class="scrollbar-container">
+ <div data-ng-if="component.properties.length === 0" class="no-row-text" data-ng-class="{'disabled': isDisableMode()}">
+ There are no properties to display <br>
+ <span ng-if="!isViewMode()"> click <a data-ng-click="addOrUpdateProperty()">here</a> to add one </span>
+
+ </div>
+ <div data-ng-repeat="property in filteredProperties=(component.properties | orderBy:sortBy:reverse | filter: {filterTerm:filterTerms}) track by $index"
+ data-tests-id="propertyRow" data-ng-class="{'selected': property.selected}"
+ class="flex-container data-row">
+
+ <div class="table-col-general flex-item text" tooltips tooltip-content="{{property.name}}">
+ <a data-tests-id="propertyName_{{property.name}}"
+ data-ng-click="addOrUpdateProperty(property); $event.stopPropagation();"
+ data-ng-class="{'disabled': isViewMode()}">{{property.name}}</a>
+
+ </div>
+
+ <div class="table-col-general flex-item text"
+ tooltips tooltip-content="{{property.type}}">
+ <span data-tests-id="propertyType_{{property.name}}"> {{property.type.replace("org.openecomp.datatypes.heat.","")}}</span>
+ </div>
+ <div class="table-col-general flex-item text"
+ tooltips tooltip-content="{{property.schema.property.type}}">
+ <span data-tests-id="propertySchema_{{property.name}}"> {{property.schema.property.type.replace("org.openecomp.datatypes.heat.","")}}</span>
+ </div>
+ <div class="table-col-general flex-item text" tooltips tooltip-content="{{property.description}}">
+ <span data-tests-id="propertyDescription_{{property.name}}" data-ng-bind="property.description"></span>
+ </div>
+ <div class="table-btn-col flex-item" ng-if="!isViewMode()">
+ <button class="table-delete-btn" data-tests-id="delete_{{property.name}}" data-ng-if="property.parentUniqueId==component.uniqueId"
+ data-ng-click="delete(property); $event.stopPropagation();" data-ng-class="{'disabled': isViewMode()}"> </button>
+ </div>
+ </div>
+ </perfect-scrollbar>
+ </div>
+
+ </div>
+ </div>
+
+</div>
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/properties/properties.less b/catalog-ui/src/app/view-models/workspace/tabs/properties/properties.less
new file mode 100644
index 0000000000..3e8d6c3fbd
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/properties/properties.less
@@ -0,0 +1,115 @@
+.workspace-properties {
+
+ width: 93%;
+ display: inline-block;
+
+ #left-top-bar{
+ float: left;
+ width: 155px;
+ ::-webkit-input-placeholder {
+ font-style: italic;
+ }
+ :-moz-placeholder {
+ font-style: italic;
+ }
+ ::-moz-placeholder {
+ font-style: italic;
+ }
+ :-ms-input-placeholder {
+ font-style: italic;
+ }
+
+ #properties-count{
+ font-weight: bold;
+ float: left;
+ }
+
+ #search-by-name{
+ -webkit-border-radius: 2px;
+ -moz-border-radius: 2px;
+ border-radius: 2px;
+ width: 245px;
+ height: 32px;
+ line-height: 32px;
+ border: 1px solid @main_color_o;
+ text-indent: 10px;
+ }
+
+ .search-button{
+ .hand;
+ cursor: pointer;
+ float: right;
+ position: relative;
+ top: -22px;
+ right: -80px;
+ }
+ }
+
+ .add-btn {
+ margin: 36px 0 0 0;
+ }
+
+ .delete-col-header{
+ .sprite;
+ .sprite.e-sdc-small-icon-delete;
+ }
+
+ .w-sdc-classic-btn {
+ float: right;
+ margin-bottom: 10px;
+ }
+
+ .table{
+ height:490px;
+ margin-bottom: 0;
+ }
+
+ .data-row{
+ .table-delete-btn{
+ display: none !important;
+ }
+ &:hover{
+ .table-delete-btn{
+ display: inline-block !important;
+ }
+ }
+ }
+
+ .table-container-flex {
+ margin-top: 27px;
+
+ .text{
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: inline-block;
+ white-space: nowrap;
+ }
+
+ .flex-item:nth-child(1) {
+ flex-grow: 15;
+ a{
+ .hand
+ }
+ }
+
+ .flex-item:nth-child(2) {
+ flex-grow: 6;
+ }
+
+ .flex-item:nth-child(3) {
+ flex-grow: 6;
+ }
+
+ .flex-item:nth-child(4) {
+ flex-grow: 20;
+
+ }
+ .flex-item:nth-child(5) {
+ flex-grow: 3;
+ padding-top: 10px;
+ }
+
+
+ }
+
+}
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/req-and-capabilities/req-and-capabilities-view-model.ts b/catalog-ui/src/app/view-models/workspace/tabs/req-and-capabilities/req-and-capabilities-view-model.ts
new file mode 100644
index 0000000000..4ac99edf8a
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/req-and-capabilities/req-and-capabilities-view-model.ts
@@ -0,0 +1,147 @@
+/**
+ * Created by rcohen on 9/22/2016.
+ */
+'use strict';
+import {IWorkspaceViewModelScope} from "app/view-models/workspace/workspace-view-model";
+import {ModalsHandler} from "app/utils";
+import {Capability, PropertyModel, Requirement} from "app/models";
+import {ComponentGenericResponse} from "../../../../ng2/services/responses/component-generic-response";
+import {ComponentServiceNg2} from "../../../../ng2/services/component-services/component.service";
+
+export class SortTableDefined {
+ reverse:boolean;
+ sortByField:string;
+}
+
+interface IReqAndCapabilitiesViewModelScope extends IWorkspaceViewModelScope {
+ requirementsTableHeadersList:Array<any>;
+ capabilitiesTableHeadersList:Array<any>;
+ capabilityPropertiesTableHeadersList:Array<any>;
+ requirementsSortTableDefined:SortTableDefined;
+ capabilitiesSortTableDefined:SortTableDefined;
+ propertiesSortTableDefined:SortTableDefined;
+ requirements:Array<Requirement>;
+ capabilities:Array<Capability>;
+ mode:string;
+ filteredProperties:Array<Array<PropertyModel>>;
+ searchText:string;
+
+ sort(sortBy:string, sortByTableDefined:SortTableDefined):void;
+ updateProperty(property:PropertyModel, indexInFilteredProperties:number):void;
+ allCapabilitiesSelected(selected:boolean):void;
+}
+
+export class ReqAndCapabilitiesViewModel {
+
+ static '$inject' = [
+ '$scope',
+ '$filter',
+ 'ModalsHandler',
+ 'ComponentServiceNg2'
+ ];
+
+
+ constructor(private $scope:IReqAndCapabilitiesViewModelScope,
+ private $filter:ng.IFilterService,
+ private ModalsHandler:ModalsHandler,
+ private ComponentServiceNg2: ComponentServiceNg2) {
+
+ this.initCapabilitiesAndRequirements();
+ this.$scope.updateSelectedMenuItem();
+ }
+
+ private initCapabilitiesAndRequirements = (): void => {
+
+ if(!this.$scope.component.capabilities || !this.$scope.component.requirements) {
+ this.$scope.isLoading = true;
+ this.ComponentServiceNg2.getCapabilitiesAndRequirements(this.$scope.component.componentType, this.$scope.component.uniqueId).subscribe((response:ComponentGenericResponse) => {
+ this.$scope.component.capabilities = response.capabilities;
+ this.$scope.component.requirements = response.requirements;
+ this.initScope();
+ this.$scope.isLoading = false;
+ }, () => {
+ this.$scope.isLoading = false;
+ });
+ } else {
+ this.initScope();
+ }
+
+ }
+
+ private openEditPropertyModal = (property:PropertyModel, indexInFilteredProperties:number):void => {
+ //...because there is not be api
+ _.forEach(this.$scope.filteredProperties[indexInFilteredProperties], (prop:PropertyModel)=> {
+ prop.readonly = true;
+ });
+ this.ModalsHandler.openEditPropertyModal(property, this.$scope.component, this.$scope.filteredProperties[indexInFilteredProperties], false).then(() => {
+
+ });
+ };
+
+ private initScope = ():void => {
+
+ this.$scope.requirementsSortTableDefined = {
+ reverse: false,
+ sortByField: 'name'
+ };
+ this.$scope.capabilitiesSortTableDefined = {
+ reverse: false,
+ sortByField: 'name'
+ };
+ this.$scope.propertiesSortTableDefined = {
+ reverse: false,
+ sortByField: 'name'
+ };
+
+ this.$scope.setValidState(true);
+ this.$scope.requirementsTableHeadersList = [
+ {title: 'Name', property: 'name'},
+ {title: 'Capability', property: 'capability'},
+ {title: 'Node', property: 'node'},
+ {title: 'Relationship', property: 'relationship'},
+ {title: 'Connected To', property: ''},
+ {title: 'Occurrences', property: ''}
+ ];
+ this.$scope.capabilitiesTableHeadersList = [
+ {title: 'Name', property: 'name'},
+ {title: 'Type', property: 'type'},
+ {title: 'Description', property: ''},
+ {title: 'Valid Source', property: ''},
+ {title: 'Occurrences', property: ''}
+ ];
+ this.$scope.capabilityPropertiesTableHeadersList = [
+ {title: 'Name', property: 'name'},
+ {title: 'Type', property: 'type'},
+ {title: 'Schema', property: 'schema.property.type'},
+ {title: 'Description', property: 'description'},
+ ];
+ this.$scope.filteredProperties = [];
+
+ this.$scope.mode = 'requirements';
+ this.$scope.requirements = [];
+ _.forEach(this.$scope.component.requirements, (req:Array<Requirement>, capName)=> {
+ this.$scope.requirements = this.$scope.requirements.concat(req);
+ });
+
+ this.$scope.capabilities = [];
+ _.forEach(this.$scope.component.capabilities, (cap:Array<Capability>, capName)=> {
+ this.$scope.capabilities = this.$scope.capabilities.concat(cap);
+ });
+
+ this.$scope.sort = (sortBy:string, sortByTableDefined:SortTableDefined):void => {
+ sortByTableDefined.reverse = (sortByTableDefined.sortByField === sortBy) ? !sortByTableDefined.reverse : false;
+ sortByTableDefined.sortByField = sortBy;
+ };
+
+ this.$scope.updateProperty = (property:PropertyModel, indexInFilteredProperties:number):void => {
+ this.openEditPropertyModal(property, indexInFilteredProperties);
+ };
+
+ this.$scope.allCapabilitiesSelected = (selected:boolean):void => {
+ _.forEach(this.$scope.capabilities, (cap:Capability)=> {
+ cap.selected = selected;
+ });
+ };
+ }
+}
+
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/req-and-capabilities/req-and-capabilities-view.html b/catalog-ui/src/app/view-models/workspace/tabs/req-and-capabilities/req-and-capabilities-view.html
new file mode 100644
index 0000000000..3d6ccb9d00
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/req-and-capabilities/req-and-capabilities-view.html
@@ -0,0 +1,143 @@
+<div class="workspace-req-and-cap">
+ <div class="tabs">
+ <button data-tests-id="req-tab" data-ng-click="mode='requirements';filterTerms='';" class="tlv-btn" data-ng-class="{'selected':mode=='requirements'}">Requirements({{requirements.length||'0'}})</button>
+ <button data-tests-id="cap-tab" data-ng-click="mode='capabilities';filterTerms='';" class="tlv-btn" data-ng-class="{'selected':mode=='capabilities'}">Capabilities({{capabilities.length||'0'}})</button>
+ </div>
+ <div class="expand-collapse-buttons" data-ng-if="mode=='capabilities'">
+ <span class="sprite-new expand-all" data-ng-click="allCapabilitiesSelected(true)"></span>
+ <span class="sprite-new collapse-all" data-ng-click="allCapabilitiesSelected(false)"></span>
+ </div>
+ <div class="search">
+ <input id="search-box" data-tests-id="search-box" type="search" placeholder="Search" data-ng-model-options="{debounce: 200}" data-ng-model="filterTerms"/>
+ <div class="search-icon-container">
+ <span class="search-icon sprite-new search-white-icon"></span>
+ </div>
+ </div>
+ <div class="table-container-flex requirements-table" data-ng-if="mode=='requirements'">
+ <div class="table" data-ng-class="{'view-mode': isViewMode()}">
+ <div class="head flex-container">
+ <div class="table-header head-row hand flex-item" data-ng-repeat="header in requirementsTableHeadersList track by $index" data-ng-click="sort(header.property, requirementsSortTableDefined)">{{header.title}}
+ <span data-ng-if="requirementsSortTableDefined.sortByField === header.property" class="table-header-sort-arrow" data-ng-class="{'down': requirementsSortTableDefined.reverse, 'up':!requirementsSortTableDefined.reverse}"> </span>
+ </div>
+ </div>
+
+ <div class="body">
+ <perfect-scrollbar scroll-y-margin-offset="0" include-padding="true" class="scrollbar-container">
+ <div data-ng-if="requirements.length === 0" class="no-row-text">
+ There are no requirements to display
+
+ </div>
+ <div data-ng-repeat="req in requirements | orderBy:requirementsSortTableDefined.sortByField:requirementsSortTableDefined.reverse | filter: {filterTerm:filterTerms} track by $index"
+ class="flex-container data-row" data-tests-id="reqRow">
+
+ <div class="table-col-general flex-item text" tooltips tooltip-content="{{req.name}}">
+ <span data-tests-id="{{req.name}}">{{req.name}}</span>
+ </div>
+ <div class="table-col-general flex-item text" tooltips tooltip-content="{{req.capability}}">
+ <span data-tests-id="{{req.capability}}">{{req.capability.substring("tosca.capabilities.".length)}}</span>
+ </div>
+ <div class="table-col-general flex-item text" tooltips tooltip-content="{{req.node}}">
+ <span data-tests-id="{{req.node}}">{{req.node.substring("tosca.nodes.".length)}}</span>
+ </div>
+ <div class="table-col-general flex-item text" tooltips tooltip-content="{{req.relationship}}">
+ <span data-tests-id="{{req.relationship}}">{{req.relationship.substring("tosca.relationships.".length)}}</span>
+ </div>
+ <div class="table-col-general flex-item text" data-tests-id="{{}}" data-ng-bind=""></div>
+ <div class="table-col-general flex-item text" tooltips tooltip-content="{{req.minOccurrences}},{{req.maxOccurrences}}">
+ <span data-tests-id="{{req.minOccurrences}},{{req.maxOccurrences}}">{{req.minOccurrences}},{{req.maxOccurrences}}</span>
+ </div>
+ </div>
+ </perfect-scrollbar>
+ </div>
+
+ </div>
+ </div>
+ <div class="table-container-flex capabilities-table" data-ng-if="mode=='capabilities'">
+ <div class="table" data-ng-class="{'view-mode': isViewMode()}">
+ <div class="head flex-container">
+ <div class="table-header head-row hand flex-item" data-ng-repeat="header in capabilitiesTableHeadersList track by $index" data-ng-click="sort(header.property, capabilitiesSortTableDefined)">{{header.title}}
+ <span data-ng-if="capabilitiesSortTableDefined.sortByField === header.property" class="table-header-sort-arrow" data-ng-class="{'down': capabilitiesSortTableDefined.reverse, 'up':!capabilitiesSortTableDefined.reverse}"> </span>
+ </div>
+ </div>
+
+ <div class="body">
+ <perfect-scrollbar scroll-y-margin-offset="0" include-padding="true" class="scrollbar-container">
+ <div data-ng-if="capabilities.length === 0" class="no-row-text">
+ There are no capabilities to display
+
+ </div>
+ <div data-ng-repeat-start="capability in capabilities | orderBy:capabilitiesSortTableDefined.sortByField:capabilitiesSortTableDefined.reverse | filter: {filterTerm:filterTerms} track by $index"
+ class="flex-container data-row" data-ng-class="{'selected': capability.selected}"
+ data-ng-click="capability.selected = !capability.selected" data-tests-id="capabilities-table-row">
+
+ <div class="table-col-general flex-item text" tooltips tooltip-content="{{capability.name}}">
+ <span class="sprite-new arrow-up-small" data-ng-class="{'opened': capability.selected}"></span>
+ <span data-tests-id="{{capability.name}}">{{capability.name}}</span>
+ </div>
+ <div class="table-col-general flex-item text" tooltips tooltip-content="{{capability.type}}">
+ <span data-tests-id="{{capability.type}}">{{capability.type.replace("tosca.capabilities.","")}}</span>
+ </div>
+
+ <div class="table-col-general flex-item text" tooltips tooltip-content="{{capability.description}}">
+ <span data-tests-id="{{capability.description}}">{{capability.description}}</span>
+ </div>
+
+ <div class="table-col-general flex-item text" tooltips tooltip-content="{{capability.validSourceTypes.join(',')}}">
+ <span data-tests-id="{{capability.validSourceTypes.join(',')}}">{{capability.validSourceTypes.join(',')}}</span>
+ </div>
+
+ <div class="table-col-general flex-item text" tooltips tooltip-content="{{capability.minOccurrences}},{{capability.maxOccurrences}}">
+ <span data-tests-id="{{capability.minOccurrences}},{{capability.maxOccurrences}}">{{capability.minOccurrences}},{{capability.maxOccurrences}}</span>
+ </div>
+ </div>
+ <div data-ng-repeat-end="" data-ng-if="capability.selected" class="item-opened">
+ <p class="properties-title">Properties</p>
+ <div class="table-container-flex properties-table">
+ <div class="table" data-ng-class="{'view-mode': isViewMode()}">
+ <div class="head flex-container">
+ <div class="table-header head-row hand flex-item" data-ng-repeat="header in capabilityPropertiesTableHeadersList track by $index" data-ng-click="sort(header.property, propertiesSortTableDefined)">{{header.title}}
+ <span data-ng-if="propertiesSortTableDefined.sortByField === header.property" class="table-header-sort-arrow" data-ng-class="{'down': propertiesSortTableDefined.reverse, 'up':!propertiesSortTableDefined.reverse}"> </span>
+ </div>
+ </div>
+
+ <div class="body">
+ <div data-ng-if="capability.properties.length === 0" class="no-row-text">
+ There are no properties to display
+ </div>
+ <div data-ng-repeat="property in filteredProperties[$parent.$index]=(capability.properties | orderBy:propertiesSortTableDefined.sortByField:propertiesSortTableDefined.reverse) track by $index"
+ data-tests-id="propertyRow"
+ class="flex-container data-row">
+
+ <div class="table-col-general flex-item text" tooltips tooltip-content="{{property.name}}">
+ <a data-tests-id="{{property.name}}"
+ data-ng-click="updateProperty(property, $parent.$index); $event.stopPropagation();"
+ data-ng-class="{'disabled': isViewMode()}">{{property.name}}</a>
+
+ </div>
+
+ <div class="table-col-general flex-item text"
+ data-tests-id="{{property.type}}"
+ tooltips tooltip-content="{{property.type}}"
+ data-ng-bind="property.type">
+ </div>
+ <div class="table-col-general flex-item text"
+ data-tests-id="{{property.schema.property.type}}"
+ tooltips tooltip-content="{{property.schema.property.type}}"
+ data-ng-bind="property.schema.property.type">
+ </div>
+ <div class="table-col-general flex-item text" tooltips tooltip-content="{{property.description}}">
+ <span data-tests-id="{{property.description}}" data-ng-bind="property.description"></span>
+ </div>
+ </div>
+ </div>
+
+ </div>
+ </div>
+ </div>
+ </perfect-scrollbar>
+ </div>
+
+ </div>
+ </div>
+</div>
+
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/req-and-capabilities/req-and-capabilities.less b/catalog-ui/src/app/view-models/workspace/tabs/req-and-capabilities/req-and-capabilities.less
new file mode 100644
index 0000000000..9b52fad411
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/req-and-capabilities/req-and-capabilities.less
@@ -0,0 +1,196 @@
+.workspace-req-and-cap {
+
+ width: 93%;
+ display: inline-block;
+
+ .tabs{
+ float: left;
+ position: relative;
+ top: 6px;
+ button{
+ float: left;
+ width: 233px;
+ height: 38px;
+ background-color: @tlv_color_t;
+ border: 1px solid @main_color_o;
+ color: black;
+ &:nth-child(1){
+ border-radius: 10px 0 0 0;
+ }
+ &:nth-child(2){
+ border-radius: 0 10px 0 0;
+ }
+ &.selected{
+ background-color: @main_color_a;
+ border: 1px solid @main_color_a;
+ color: white;
+ }
+ }
+ }
+ .search{
+ margin-bottom: 12px;
+ float: right;
+ ::-webkit-input-placeholder {
+ font-style: italic;
+ }
+ :-moz-placeholder {
+ font-style: italic;
+ }
+ ::-moz-placeholder {
+ font-style: italic;
+ }
+ :-ms-input-placeholder {
+ font-style: italic;
+ }
+ #search-box{
+ -webkit-border-radius: 2px 0 0 2px;
+ -moz-border-radius: 2px 0 0 2px;
+ border-radius: 2px 0 0 2px;
+ width: 213px;
+ height: 32px;
+ line-height: 32px;
+ border: 1px solid @main_color_o;
+ text-indent: 10px;
+ float: left;
+ }
+ .search-icon-container{
+ background-color: @main_color_a;
+ height: 32px;
+ width: 32px;
+ border-radius: 0 2px 2px 0;
+ float: left;
+ .search-icon{
+ position: relative;
+ top: 9px;
+ }
+ }
+ }
+ .expand-collapse-buttons{
+ float: right;
+ width: 44px;
+ margin-left: 11px;
+ margin-top: 10px;
+ span{
+ vertical-align: bottom;
+ .hand;
+ }
+ }
+
+
+
+ .table{
+ height:490px;
+ margin-bottom: 0;
+ }
+
+ .arrow-up-small{
+ &.opened{
+ .arrow-up-small-hover;
+ }
+ }
+
+ .item-opened{
+ background-color: @tlv_color_t;
+ }
+
+ .properties-title{
+ margin:0;
+ font-weight: bold;
+ }
+
+ .table-container-flex {
+ margin-top: 10px;
+
+ .text{
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: inline-block;
+ white-space: nowrap;
+ }
+
+ &.requirements-table{
+ border-top: 4px solid @main_color_a;
+ .flex-item:nth-child(1) {
+ flex-grow: 20;
+ }
+
+ .flex-item:nth-child(2) {
+ flex-grow: 20;
+ }
+
+ .flex-item:nth-child(3) {
+ flex-grow: 20;
+ }
+
+ .flex-item:nth-child(4) {
+ flex-grow: 20;
+ }
+
+ .flex-item:nth-child(5) {
+ flex-grow: 20;
+ }
+
+ .flex-item:nth-child(6) {
+ flex-grow: 20;
+ }
+ }
+
+ &.capabilities-table{
+ border-top: 4px solid @main_color_a;
+ .selected{
+ .flex-item:nth-child(1) {
+ border-left: 4px solid @main_color_a;
+ padding-right: 11px;
+ }
+ }
+ .flex-item:nth-child(1) {
+ flex-grow: 10;
+ }
+
+ .flex-item:nth-child(2) {
+ flex-grow: 10;
+ }
+
+ .flex-item:nth-child(3) {
+ flex-grow: 10;
+ }
+
+ .flex-item:nth-child(4) {
+ flex-grow: 10;
+ }
+
+ .flex-item:nth-child(5) {
+ flex-grow: 10;
+ }
+
+ }
+
+ &.properties-table{
+ .table{
+ height: auto;
+ }
+
+ .flex-item:nth-child(1) {
+ flex-grow: 15;
+ a{
+ .hand
+ }
+ }
+
+ .flex-item:nth-child(2) {
+ flex-grow: 6;
+ }
+
+ .flex-item:nth-child(3) {
+ flex-grow: 6;
+ }
+
+ .flex-item:nth-child(4) {
+ flex-grow: 20;
+
+ }
+ }
+
+ }
+
+}
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/tosca-artifacts/tosca-artifacts-view-model.ts b/catalog-ui/src/app/view-models/workspace/tabs/tosca-artifacts/tosca-artifacts-view-model.ts
new file mode 100644
index 0000000000..06022786ee
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/tosca-artifacts/tosca-artifacts-view-model.ts
@@ -0,0 +1,84 @@
+'use strict';
+import {ArtifactModel, IFileDownload} from "app/models";
+import {IWorkspaceViewModelScope} from "app/view-models/workspace/workspace-view-model";
+import {ComponentGenericResponse} from "../../../../ng2/services/responses/component-generic-response";
+import {ComponentServiceNg2} from "../../../../ng2/services/component-services/component.service";
+export interface IToscaArtifactsScope extends IWorkspaceViewModelScope {
+ artifacts:Array<ArtifactModel>;
+ tableHeadersList:Array<any>;
+ artifactType:string;
+ downloadFile:IFileDownload;
+ isLoading:boolean;
+ sortBy:string;
+ reverse:boolean;
+
+ getTitle():string;
+ download(artifact:ArtifactModel):void;
+ sort(sortBy:string):void;
+ showNoArtifactMessage():boolean;
+}
+
+export class ToscaArtifactsViewModel {
+
+ static '$inject' = [
+ '$scope',
+ '$filter',
+ 'ComponentServiceNg2'
+ ];
+
+ constructor(private $scope:IToscaArtifactsScope,
+ private $filter:ng.IFilterService,
+ private ComponentServiceNg2:ComponentServiceNg2) {
+ this.initToscaArtifacts();
+ this.$scope.updateSelectedMenuItem();
+ }
+
+ private initToscaArtifacts = (): void => {
+
+ if(!this.$scope.component.toscaArtifacts) {
+ this.$scope.isLoading = true;
+ this.ComponentServiceNg2.getComponentToscaArtifacts(this.$scope.component).subscribe((response:ComponentGenericResponse) => {
+ this.$scope.component.toscaArtifacts = response.toscaArtifacts;
+ this.initScope();
+ this.$scope.isLoading = false;
+ }, () => {
+ this.$scope.isLoading = false;
+ });
+ } else {
+ this.initScope();
+ }
+ }
+
+ private initScope = ():void => {
+ this.$scope.isLoading = false;
+ this.$scope.sortBy = 'artifactDisplayName';
+ this.$scope.reverse = false;
+ this.$scope.setValidState(true);
+ this.$scope.artifactType = 'informational';
+ this.$scope.getTitle = ():string => {
+ return this.$filter("resourceName")(this.$scope.component.name) + ' Artifacts';
+
+ };
+
+ this.$scope.tableHeadersList = [
+ {title: 'Name', property: 'artifactDisplayName'},
+ {title: 'Type', property: 'artifactType'},
+ {title: 'Version', property: 'artifactVersion'}
+ ];
+
+ this.$scope.artifacts = <ArtifactModel[]>_.values(this.$scope.component.toscaArtifacts);
+ this.$scope.sort = (sortBy:string):void => {
+ this.$scope.reverse = (this.$scope.sortBy === sortBy) ? !this.$scope.reverse : false;
+ this.$scope.sortBy = sortBy;
+ };
+
+
+ this.$scope.showNoArtifactMessage = ():boolean => {
+ if (this.$scope.artifacts.length === 0) {
+ return true;
+ }
+ return false;
+ };
+
+ }
+}
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/tosca-artifacts/tosca-artifacts-view.html b/catalog-ui/src/app/view-models/workspace/tabs/tosca-artifacts/tosca-artifacts-view.html
new file mode 100644
index 0000000000..5df6b5c11f
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/tosca-artifacts/tosca-artifacts-view.html
@@ -0,0 +1,49 @@
+<div class="workspace-tosca-artifact">
+ <div class="table-container-flex">
+ <div class="table" data-ng-class="{'view-mode': isViewMode()}">
+ <div class="head flex-container">
+ <div class="table-header head-row hand flex-item" ng-repeat="header in tableHeadersList track by $index" data-ng-click="sort(header.property)">{{header.title}}
+ <span data-ng-show="sortBy === header.property" class="table-header-sort-arrow" data-ng-class="{'down': reverse, 'up':!reverse}"> </span>
+ </div>
+ <div class="table-no-text-header head-row flex-item"></div>
+ </div>
+ <div class="body">
+ <perfect-scrollbar suppress-scroll-x="true" scroll-y-margin-offset="0" include-padding="true" class="scrollbar-container">
+ <div data-ng-if="showNoArtifactMessage()" class="no-row-text" data-ng-class="{'disabled': isDisableMode()}">
+ There are no TOSCA artifacts to display
+ </div>
+ <div data-ng-repeat-start="artifact in artifacts| orderBy:sortBy:reverse track by $index"
+ class="flex-container data-row" data-tests-id="{{artifact.artifactDisplayName}}"
+ data-ng-class="{'selected': artifact.selected}">
+
+ <div class="table-col-general flex-item" data-ng-click="artifact.selected = !artifact.selected" data-tests-id="name-{{$index}}">
+ <span class="sprite table-arrow" data-ng-class="{'opened': artifact.selected}"></span>
+ {{artifact.artifactDisplayName}}
+ </div>
+
+ <div class="table-col-general flex-item" data-ng-click="artifact.selected = !artifact.selected" data-tests-id="type-{{$index}}">
+ {{artifact.artifactType}}
+ </div>
+
+ <div class="table-col-general flex-item" data-ng-click="artifact.selected = !artifact.selected" data-tests-id="version-{{$index}}">
+ {{artifact.artifactVersion}}
+ </div>
+
+ <div class="table-btn-col flex-item download-icon-container">
+ <button class="table-download-btn tosca" download-artifact data-tests-id="download-{{artifact.artifactDisplayName}}"
+ data-ng-if="artifact.artifactName" component="component" artifact="artifact" show-loader="true"></button>
+ </div>
+ </div>
+ <div data-ng-repeat-end="" data-ng-if="artifact.selected" class="item-opened" data-tests-id="details-{{$index}}">
+ <div><span class="details-title">Label:</span> {{artifact.artifactLabel}}</div>
+ <div><span class="details-title">UUID:</span> {{artifact.uniqueId}}</div>
+ <div><span class="details-title">Description:</span> {{artifact.description}}</div>
+
+
+ </div>
+
+ </perfect-scrollbar>
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/catalog-ui/src/app/view-models/workspace/tabs/tosca-artifacts/tosca-artifacts.less b/catalog-ui/src/app/view-models/workspace/tabs/tosca-artifacts/tosca-artifacts.less
new file mode 100644
index 0000000000..6dfec2980f
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/tabs/tosca-artifacts/tosca-artifacts.less
@@ -0,0 +1,78 @@
+.workspace-tosca-artifact {
+ width: 93%;
+ display: inline-block;
+ .w-sdc-classic-btn {
+ float: right;
+ margin-bottom: 10px;
+ }
+
+ .details-title{
+ font-weight: bold;
+ margin-right: 5px;
+ }
+
+ .table{
+ height: 490px;
+ margin-bottom: 0;
+ }
+
+
+ .table-container-flex {
+ margin-top: 27px;
+
+ .item-opened{
+ word-wrap: break-word;
+ }
+
+
+ .flex-item:nth-child(1) {
+ flex-grow: 15;
+ .hand;
+ span.table-arrow {
+ margin-right: 7px;
+ }
+ }
+
+ .flex-item:nth-child(2) {
+ flex-grow: 6;
+ }
+
+ .flex-item:nth-child(3) {
+ flex-grow: 1;
+ }
+
+ .flex-item:nth-child(4) {
+ flex-grow: 1;
+ }
+
+
+
+
+ .table-download-btn{
+ &.tosca{
+ margin-left: 0;
+ margin-top: 8px;
+ }
+ }
+
+ .download-icon-container{
+ position: relative;
+
+ .loader{
+ left: 60%;
+ top: 45px;
+ border: none;
+ background-color: transparent;
+ height: 0px;
+ width: 63px;
+ outline: none;
+
+ }
+ }
+
+
+ }
+
+}
+
+
diff --git a/catalog-ui/src/app/view-models/workspace/workspace-view-model.ts b/catalog-ui/src/app/view-models/workspace/workspace-view-model.ts
new file mode 100644
index 0000000000..ff4636e002
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/workspace-view-model.ts
@@ -0,0 +1,718 @@
+/**
+ * Created by obarda on 3/30/2016.
+ */
+'use strict';
+import {IUserProperties, IAppMenu, Resource, Component} from "app/models";
+import {
+ WorkspaceMode, ComponentFactory, ChangeLifecycleStateHandler, Role, ComponentState, MenuItemGroup, MenuHandler,
+ MenuItem, ModalsHandler, States, EVENTS, CHANGE_COMPONENT_CSAR_VERSION_FLAG, ResourceType
+} from "app/utils";
+import {
+ EventListenerService,
+ EntityService,
+ ProgressService,
+ CacheService,
+ LeftPaletteLoaderService
+} from "app/services";
+import {FileUploadModel} from "../../directives/file-upload/file-upload";
+
+
+export interface IWorkspaceViewModelScope extends ng.IScope {
+
+ isLoading:boolean;
+ isCreateProgress:boolean;
+ component:Component;
+ originComponent:Component;
+ componentType:string;
+ importFile:any;
+ leftBarTabs:MenuItemGroup;
+ isNew:boolean;
+ isFromImport:boolean;
+ isValidForm:boolean;
+ mode:WorkspaceMode;
+ breadcrumbsModel:Array<MenuItemGroup>;
+ sdcMenu:IAppMenu;
+ changeLifecycleStateButtons:any;
+ version:string;
+ versionsList:Array<any>;
+ changeVersion:any;
+ isComposition:boolean;
+ isDeployment:boolean;
+ $state:ng.ui.IStateService;
+ user:IUserProperties;
+ thirdParty:boolean;
+ disabledButtons:boolean;
+ menuComponentTitle:string;
+ progressService:ProgressService;
+ progressMessage:string;
+ // leftPanelComponents:Array<Models.Components.Component>; //this is in order to load the left panel once, and not wait long time when moving to composition
+
+ showChangeStateButton():boolean;
+ getComponent():Component;
+ setComponent(component:Component):void;
+ onMenuItemPressed(state:string):ng.IPromise<boolean>;
+ save():ng.IPromise<boolean>;
+ setValidState(isValid:boolean):void;
+ revert():void;
+ changeLifecycleState(state:string):void;
+ enabledTabs():void
+ isDesigner():boolean;
+ isProductManager():boolean;
+ isViewMode():boolean;
+ isEditMode():boolean;
+ isCreateMode():boolean;
+ isDisableMode():boolean;
+ showFullIcons():boolean;
+ goToBreadcrumbHome():void;
+ onVersionChanged(selectedId:string):void;
+ getLatestVersion():void;
+ getStatus():string;
+ showLifecycleIcon():boolean;
+ updateSelectedMenuItem():void;
+ uploadFileChangedInGeneralTab():void;
+ updateMenuComponentName(ComponentName:string):void;
+}
+
+export class WorkspaceViewModel {
+
+ static '$inject' = [
+ '$scope',
+ 'injectComponent',
+ 'ComponentFactory',
+ '$state',
+ 'sdcMenu',
+ '$q',
+ 'MenuHandler',
+ 'Sdc.Services.CacheService',
+ 'ChangeLifecycleStateHandler',
+ 'ModalsHandler',
+ 'LeftPaletteLoaderService',
+ '$filter',
+ 'EventListenerService',
+ 'Sdc.Services.EntityService',
+ 'Notification',
+ '$stateParams',
+ 'Sdc.Services.ProgressService'
+ ];
+
+ constructor(private $scope:IWorkspaceViewModelScope,
+ private injectComponent:Component,
+ private ComponentFactory:ComponentFactory,
+ private $state:ng.ui.IStateService,
+ private sdcMenu:IAppMenu,
+ private $q:ng.IQService,
+ private MenuHandler:MenuHandler,
+ private cacheService:CacheService,
+ private ChangeLifecycleStateHandler:ChangeLifecycleStateHandler,
+ private ModalsHandler:ModalsHandler,
+ private LeftPaletteLoaderService:LeftPaletteLoaderService,
+ private $filter:ng.IFilterService,
+ private EventListenerService:EventListenerService,
+ private EntityService:EntityService,
+ private Notification:any,
+ private $stateParams:any,
+ private progressService:ProgressService) {
+
+ this.initScope();
+ this.initAfterScope();
+ }
+
+ private role:string;
+ private components:Array<Component>;
+
+ private initViewMode = ():WorkspaceMode => {
+ let mode = WorkspaceMode.VIEW;
+
+ if (!this.$state.params['id']) { //&& !this.$state.params['vspComponent']
+ mode = WorkspaceMode.CREATE;
+ } else {
+ if (this.$scope.component.lifecycleState === ComponentState.NOT_CERTIFIED_CHECKOUT &&
+ this.$scope.component.lastUpdaterUserId === this.cacheService.get("user").userId) {
+ if (this.$scope.component.isProduct() && this.role == Role.PRODUCT_MANAGER) {
+ mode = WorkspaceMode.EDIT;
+ }
+ if ((this.$scope.component.isService() || this.$scope.component.isResource()) && this.role == Role.DESIGNER) {
+ mode = WorkspaceMode.EDIT;
+ }
+ }
+ }
+ return mode;
+ };
+
+ private initChangeLifecycleStateButtons = ():void => {
+ let state = this.$scope.component.isService() && (Role.OPS == this.role || Role.GOVERNOR == this.role) ? this.$scope.component.distributionStatus : this.$scope.component.lifecycleState;
+ this.$scope.changeLifecycleStateButtons = this.sdcMenu.roles[this.role].changeLifecycleStateButtons[state];
+ };
+
+ private isNeedSave = ():boolean => {
+ return this.$scope.isEditMode() &&
+ this.$state.current.data && this.$state.current.data.unsavedChanges;
+ };
+
+ private initLeftPalette = ():void => {
+ this.LeftPaletteLoaderService.loadLeftPanel(this.$scope.component.componentType);
+ };
+
+ private initScope = ():void => {
+
+ this.$scope.component = this.injectComponent;
+ this.initLeftPalette();
+ this.$scope.menuComponentTitle = this.$scope.component.name;
+ this.$scope.disabledButtons = false;
+ this.$scope.originComponent = this.ComponentFactory.createComponent(this.$scope.component);
+ this.$scope.componentType = this.$scope.component.componentType;
+ this.$scope.version = this.cacheService.get('version');
+ this.$scope.user = this.cacheService.get("user");
+ this.role = this.$scope.user.role;
+ this.$scope.mode = this.initViewMode();
+ this.$scope.isValidForm = true;
+ this.initChangeLifecycleStateButtons();
+ this.initVersionObject();
+ this.$scope.$state = this.$state;
+ this.$scope.isLoading = false;
+ this.$scope.isComposition = (this.$state.current.name.indexOf(States.WORKSPACE_COMPOSITION) > -1);
+ this.$scope.isDeployment = (this.$state.current.name.indexOf(States.WORKSPACE_DEPLOYMENT) > -1);
+ this.$scope.progressService = this.progressService;
+
+ this.$scope.getComponent = ():Component => {
+ return this.$scope.component;
+ };
+
+ this.$scope.updateMenuComponentName = (ComponentName:string):void => {
+ this.$scope.menuComponentTitle = ComponentName;
+ };
+
+ this.$scope.sdcMenu = this.sdcMenu;
+ // Will be called from each step after save to update the resource.
+ this.$scope.setComponent = (component:Component):void => {
+ this.$scope.component = component;
+ };
+
+ this.$scope.uploadFileChangedInGeneralTab = ():void => {
+ // In case user select browse file, and in update mode, need to disable submit for testing and checkin buttons.
+ if (this.$scope.isEditMode() && this.$scope.component.isResource() && (<Resource>this.$scope.component).resourceType == ResourceType.VF) {
+ this.$scope.disabledButtons = true;
+ }
+ };
+
+ this.$scope.onMenuItemPressed = (state:string):ng.IPromise<boolean> => {
+ let deferred = this.$q.defer();
+ let goToState = ():void => {
+ this.$state.go(state, {
+ id: this.$scope.component.uniqueId,
+ type: this.$scope.component.componentType.toLowerCase(),
+ components: this.components
+ });
+ deferred.resolve(true);
+ };
+ if (this.isNeedSave()) {
+ if (this.$scope.isValidForm) {
+ this.$scope.save().then(goToState);
+ } else {
+ console.log('form is not valid');
+ deferred.reject(false);
+ }
+ } else if (this.$scope.isEditMode() && //this is a workaround for amdocs - we need to get the artifact in order to avoid saving the vf when moving from their tabs
+ (this.$state.current.name === States.WORKSPACE_MANAGEMENT_WORKFLOW || this.$state.current.name === States.WORKSPACE_NETWORK_CALL_FLOW)) {
+ let onGetSuccess = (component:Component) => {
+ this.$scope.isLoading = false;
+ // Update the components
+ this.$scope.component = component;
+ goToState();
+ };
+ let onFailed = () => {
+ this.EventListenerService.notifyObservers(EVENTS.ON_WORKSPACE_SAVE_BUTTON_ERROR);
+ this.$scope.isLoading = false; // stop the progress.
+ deferred.reject(false);
+ };
+ this.$scope.component.getComponent().then(onGetSuccess, onFailed);
+ } else {
+ goToState();
+ }
+ return deferred.promise;
+ };
+
+ this.$scope.setValidState = (isValid:boolean):void => {
+ this.$scope.isValidForm = isValid;
+ };
+
+ this.$scope.onVersionChanged = (selectedId:string):void => {
+ if (this.$state.current.data && this.$state.current.data.unsavedChanges) {
+ this.$scope.changeVersion.selectedVersion = _.find(this.$scope.versionsList, {versionId: this.$scope.component.uniqueId});
+ }
+ this.$scope.isLoading = true;
+ this.$state.go(this.$state.current.name, {
+ id: selectedId,
+ type: this.$scope.componentType.toLowerCase(),
+ mode: WorkspaceMode.VIEW,
+ components: this.$state.params['components']
+ });
+
+ };
+
+ this.$scope.getLatestVersion = ():void => {
+ this.$scope.onVersionChanged(_.first(this.$scope.versionsList).versionId);
+ };
+
+ this.$scope.save = (state?:string):ng.IPromise<boolean> => {
+ this.EventListenerService.notifyObservers(EVENTS.ON_WORKSPACE_SAVE_BUTTON_CLICK);
+
+ this.progressService.initCreateComponentProgress(this.$scope.component.uniqueId);
+
+ let deferred = this.$q.defer();
+ let modalInstance:ng.ui.bootstrap.IModalServiceInstance;
+
+ let onFailed = () => {
+ _.first(this.$scope.leftBarTabs.menuItems).isDisabled = false;//enabled click on general tab (DE246274)
+ this.EventListenerService.notifyObservers(EVENTS.ON_WORKSPACE_SAVE_BUTTON_ERROR);
+ this.progressService.deleteProgressValue(this.$scope.component.uniqueId);
+ modalInstance && modalInstance.close(); // Close the modal in case it is opened.
+ this.$scope.component.tags = _.without(this.$scope.component.tags, this.$scope.component.name);// for fix DE246217
+ this.$scope.isCreateProgress = false;
+ this.$scope.isLoading = false; // stop the progress.
+
+ this.$scope.setValidState(true); // Set the form valid (if sent form is valid, the error from server).
+ if (!this.$scope.isCreateMode()) {
+ this.$scope.component = this.ComponentFactory.createComponent(this.$scope.originComponent); // Set the component back to the original.
+ this.enableMenuItems(); // Enable the menu items (left tabs), so user can press on them.
+ this.$scope.disabledButtons = false; // Enable "submit for testing" & checking buttons.
+ }
+
+ deferred.reject(false);
+ };
+
+ let onSuccessCreate = (component:Component) => {
+
+ this.showSuccessNotificationMessage();
+ this.progressService.deleteProgressValue(this.$scope.component.uniqueId);
+ //update components for breadcrumbs
+ this.components.unshift(component);
+ this.$state.go(States.WORKSPACE_GENERAL, {
+ id: component.uniqueId,
+ type: component.componentType.toLowerCase(),
+ components: this.components
+ });
+
+ deferred.resolve(true);
+ };
+
+ let onSuccessUpdate = (component:Component) => {
+ this.$scope.isCreateProgress = false;
+ this.$scope.disabledButtons = false;
+ this.showSuccessNotificationMessage();
+ this.progressService.deleteProgressValue(this.$scope.component.uniqueId);
+
+ // Stop the circle loader.
+ this.$scope.isLoading = false;
+
+ component.tags = _.reject(component.tags, (item)=> {
+ return item === component.name
+ });
+
+ // Update the components
+ this.$scope.component = component;
+ this.$scope.originComponent = this.ComponentFactory.createComponent(this.$scope.component);
+
+ //update components for breadcrumbs
+ this.components.unshift(component);
+
+ // Enable left tags
+ this.$scope.enabledTabs();
+
+
+ if (this.$state.current.data) {
+ this.$state.current.data.unsavedChanges = false;
+ }
+
+ deferred.resolve(true);
+ };
+
+ if (this.$scope.isCreateMode()) {
+ this.$scope.progressMessage = "Creating Asset...";
+ // CREATE MODE
+ this.$scope.isCreateProgress = true;
+
+ _.first(this.$scope.leftBarTabs.menuItems).isDisabled = true;//disabled click on general tab (DE246274)
+
+ // Start creating the component
+ this.ComponentFactory.createComponentOnServer(this.$scope.component).then(onSuccessCreate, onFailed);
+
+ // In case we import CSAR. Notify user that import VF will take long time (the create is performed in the background).
+ if (this.$scope.component.isResource() && (<Resource>this.$scope.component).csarUUID) {
+ this.Notification.info({
+ message: this.$filter('translate')("IMPORT_VF_MESSAGE_CREATE_TAKES_LONG_TIME_DESCRIPTION"),
+ title: this.$filter('translate')("IMPORT_VF_MESSAGE_CREATE_TAKES_LONG_TIME_TITLE")
+ });
+ }
+ } else {
+ // UPDATE MODE
+ this.$scope.isCreateProgress = true;
+ this.$scope.progressMessage = "Updating Asset...";
+ this.disableMenuItems();
+
+
+ // Work around to change the csar version
+ if (this.cacheService.get(CHANGE_COMPONENT_CSAR_VERSION_FLAG)) {
+ (<Resource>this.$scope.component).csarVersion = this.cacheService.get(CHANGE_COMPONENT_CSAR_VERSION_FLAG);
+ this.cacheService.remove(CHANGE_COMPONENT_CSAR_VERSION_FLAG);
+ }
+
+ this.$scope.component.updateComponent().then(onSuccessUpdate, onFailed);
+ }
+ return deferred.promise;
+ };
+
+ this.$scope.revert = ():void => {
+ //in state of import file leave the file in place
+ if (this.$scope.component.isResource() && (<Resource>this.$scope.component).importedFile) {
+ let tempFile:FileUploadModel = (<Resource>this.$scope.component).importedFile;
+ this.$scope.component = this.ComponentFactory.createComponent(this.$scope.originComponent);
+ (<Resource>this.$scope.component).importedFile = tempFile;
+ } else {
+ this.$scope.component = this.ComponentFactory.createComponent(this.$scope.originComponent);
+ }
+
+ };
+
+ this.$scope.changeLifecycleState = (state:string):void => {
+ if (this.isNeedSave() && state !== 'deleteVersion') {
+ this.$scope.save().then(() => {
+ changeLifecycleState(state);
+ })
+ } else {
+ changeLifecycleState(state);
+ }
+ };
+
+ let defaultActionAfterChangeLifecycleState = ():void => {
+ if (this.$state.current.data && this.$state.current.data.unsavedChanges) {
+ this.$state.current.data.unsavedChanges = false;
+ }
+ this.$state.go('dashboard');
+ };
+
+ let changeLifecycleState = (state:string) => {
+ if ('monitor' === state) {
+ this.$state.go('workspace.distribution');
+ return;
+ }
+
+ let data = this.$scope.changeLifecycleStateButtons[state];
+ let onSuccess = (component:Component, url:string):void => {
+ //Updating the component from server response
+
+ //the server returns only metaData (small component) except checkout (Full component) ,so we update only the statuses of distribution & lifecycle
+ this.$scope.component.lifecycleState = component.lifecycleState;
+ this.$scope.component.distributionStatus = component.distributionStatus;
+
+ switch (url) {
+ case 'lifecycleState/CHECKOUT':
+ // only checkOut get the full component from server
+ this.$scope.component = component;
+ // Work around to change the csar version
+ if (this.cacheService.get(CHANGE_COMPONENT_CSAR_VERSION_FLAG)) {
+ (<Resource>this.$scope.component).csarVersion = this.cacheService.get(CHANGE_COMPONENT_CSAR_VERSION_FLAG);
+ }
+
+ //when checking out a minor version uuid remains
+ let bcComponent:Component = _.find(this.components, (item) => {
+ return item.uuid === component.uuid;
+ });
+ if (bcComponent) {
+ this.components[this.components.indexOf(bcComponent)] = component;
+ } else {
+ //when checking out a major(certified) version
+ this.components.unshift(component);
+ }
+
+ this.$state.go(this.$state.current.name, {
+ id: component.uniqueId,
+ type: component.componentType.toLowerCase(),
+ components: this.components
+ });
+ this.Notification.success({
+ message: this.$filter('translate')("CHECKOUT_SUCCESS_MESSAGE_TEXT"),
+ title: this.$filter('translate')("CHECKOUT_SUCCESS_MESSAGE_TITLE")
+ });
+ break;
+ case 'lifecycleState/CHECKIN':
+ defaultActionAfterChangeLifecycleState();
+ this.Notification.success({
+ message: this.$filter('translate')("CHECKIN_SUCCESS_MESSAGE_TEXT"),
+ title: this.$filter('translate')("CHECKIN_SUCCESS_MESSAGE_TITLE")
+ });
+ break;
+ case 'lifecycleState/UNDOCHECKOUT':
+ defaultActionAfterChangeLifecycleState();
+ this.Notification.success({
+ message: this.$filter('translate')("DELETE_SUCCESS_MESSAGE_TEXT"),
+ title: this.$filter('translate')("DELETE_SUCCESS_MESSAGE_TITLE")
+ });
+ break;
+ case 'lifecycleState/certificationRequest':
+ defaultActionAfterChangeLifecycleState();
+ this.Notification.success({
+ message: this.$filter('translate')("SUBMIT_FOR_TESTING_SUCCESS_MESSAGE_TEXT"),
+ title: this.$filter('translate')("SUBMIT_FOR_TESTING_SUCCESS_MESSAGE_TITLE")
+ });
+ break;
+ //Tester Role
+ case 'lifecycleState/failCertification':
+ defaultActionAfterChangeLifecycleState();
+ this.Notification.success({
+ message: this.$filter('translate')("REJECT_SUCCESS_MESSAGE_TEXT"),
+ title: this.$filter('translate')("REJECT_SUCCESS_MESSAGE_TITLE")
+ });
+ break;
+ case 'lifecycleState/certify':
+ defaultActionAfterChangeLifecycleState();
+ this.Notification.success({
+ message: this.$filter('translate')("ACCEPT_TESTING_SUCCESS_MESSAGE_TEXT"),
+ title: this.$filter('translate')("ACCEPT_TESTING_SUCCESS_MESSAGE_TITLE")
+ });
+ break;
+ //DE203504 Bug Fix Start
+ case 'lifecycleState/startCertification':
+ this.initChangeLifecycleStateButtons();
+ this.Notification.success({
+ message: this.$filter('translate')("START_TESTING_SUCCESS_MESSAGE_TEXT"),
+ title: this.$filter('translate')("START_TESTING_SUCCESS_MESSAGE_TITLE")
+ });
+ break;
+ case 'lifecycleState/cancelCertification':
+ this.initChangeLifecycleStateButtons();
+ this.Notification.success({
+ message: this.$filter('translate')("CANCEL_TESTING_SUCCESS_MESSAGE_TEXT"),
+ title: this.$filter('translate')("CANCEL_TESTING_SUCCESS_MESSAGE_TITLE")
+ });
+ break;
+ //Ops Role
+ case 'distribution/PROD/activate':
+ this.initChangeLifecycleStateButtons();
+ this.Notification.success({
+ message: this.$filter('translate')("DISTRIBUTE_SUCCESS_MESSAGE_TEXT"),
+ title: this.$filter('translate')("DISTRIBUTE_SUCCESS_MESSAGE_TITLE")
+ });
+ break;
+ //Governor Role
+ case 'distribution-state/reject':
+ this.initChangeLifecycleStateButtons();
+ this.Notification.success({
+ message: this.$filter('translate')("REJECT_SUCCESS_MESSAGE_TEXT"),
+ title: this.$filter('translate')("REJECT_SUCCESS_MESSAGE_TITLE")
+ });
+ break;
+ case 'distribution-state/approve':
+ this.initChangeLifecycleStateButtons();
+ this.$state.go('catalog');
+ this.Notification.success({
+ message: this.$filter('translate')("APPROVE_SUCCESS_MESSAGE_TEXT"),
+ title: this.$filter('translate')("APPROVE_SUCCESS_MESSAGE_TITLE")
+ });
+ break;
+ //DE203504 Bug Fix End
+
+ default :
+ defaultActionAfterChangeLifecycleState();
+
+ }
+ if (data.url != 'lifecycleState/CHECKOUT') {
+ this.$scope.isLoading = false;
+ }
+ };
+ //this.$scope.isLoading = true;
+ this.ChangeLifecycleStateHandler.changeLifecycleState(this.$scope.component, data, this.$scope, onSuccess);
+ };
+
+ this.$scope.enabledTabs = ():void => {
+ this.$scope.leftBarTabs.menuItems.forEach((item:MenuItem) => {
+ item.isDisabled = false;
+ });
+ };
+
+ this.$scope.isViewMode = ():boolean => {
+ return this.$scope.mode === WorkspaceMode.VIEW;
+ };
+
+ this.$scope.isDesigner = ():boolean => {
+ return this.role == Role.DESIGNER;
+ };
+
+ this.$scope.isProductManager = ():boolean => {
+ return this.role == Role.PRODUCT_MANAGER;
+ };
+
+ this.$scope.isDisableMode = ():boolean => {
+ return this.$scope.mode === WorkspaceMode.VIEW && this.$scope.component.lifecycleState === ComponentState.NOT_CERTIFIED_CHECKIN;
+ };
+
+ this.$scope.showFullIcons = ():boolean => {
+ //we show revert and save icons only in general\icon view
+ return this.$state.current.name === States.WORKSPACE_GENERAL ||
+ this.$state.current.name === States.WORKSPACE_ICONS;
+ };
+
+ this.$scope.isCreateMode = ():boolean => {
+ return this.$scope.mode === WorkspaceMode.CREATE;
+ };
+
+ this.$scope.isEditMode = ():boolean => {
+ return this.$scope.mode === WorkspaceMode.EDIT;
+ };
+
+ this.$scope.goToBreadcrumbHome = ():void => {
+ let bcHome:MenuItemGroup = this.$scope.breadcrumbsModel[0];
+ this.$state.go(bcHome.menuItems[bcHome.selectedIndex].state);
+ };
+
+ this.$scope.showLifecycleIcon = ():boolean => {
+ return this.role == Role.DESIGNER ||
+ this.role == Role.PRODUCT_MANAGER;
+ };
+
+ this.$scope.getStatus = ():string => {
+ if (this.$scope.isCreateMode()) {
+ return 'IN DESIGN';
+ }
+
+ return this.$scope.component.getStatus(this.sdcMenu);
+ };
+
+ this.initMenuItems();
+
+ this.$scope.showChangeStateButton = ():boolean => {
+ let result:boolean = true;
+ if (!this.$scope.component.isLatestVersion() && Role.OPS != this.role && Role.GOVERNOR != this.role) {
+ result = false;
+ }
+ if (this.role === Role.PRODUCT_MANAGER && !this.$scope.component.isProduct()) {
+ result = false;
+ }
+ if ((this.role === Role.DESIGNER || this.role === Role.TESTER)
+ && this.$scope.component.isProduct()) {
+ result = false;
+ }
+ if (ComponentState.NOT_CERTIFIED_CHECKOUT === this.$scope.component.lifecycleState && this.$scope.isViewMode()) {
+ result = false;
+ }
+ if (ComponentState.CERTIFIED != this.$scope.component.lifecycleState &&
+ (Role.OPS == this.role || Role.GOVERNOR == this.role)) {
+ result = false;
+ }
+ return result;
+ };
+
+ this.$scope.updateSelectedMenuItem = ():void => {
+ let selectedItem:MenuItem = _.find(this.$scope.leftBarTabs.menuItems, (item:MenuItem) => {
+ return item.state === this.$state.current.name;
+ });
+ this.$scope.leftBarTabs.selectedIndex = selectedItem ? this.$scope.leftBarTabs.menuItems.indexOf(selectedItem) : 0;
+ };
+
+ this.$scope.$watch('$state.current.name', (newVal:string):void => {
+ if (newVal) {
+ this.$scope.isComposition = (newVal.indexOf(States.WORKSPACE_COMPOSITION) > -1);
+ this.$scope.isDeployment = (newVal.indexOf(States.WORKSPACE_DEPLOYMENT) > -1);
+ }
+ });
+ };
+
+ private initAfterScope = ():void => {
+ // In case user select csar from the onboarding modal, need to disable checkout and submit for testing.
+ if (this.$state.params['disableButtons'] === true) {
+ this.$scope.uploadFileChangedInGeneralTab();
+ }
+ };
+
+ private initVersionObject = ():void => {
+ this.$scope.versionsList = (this.$scope.component.getAllVersionsAsSortedArray()).reverse();
+ this.$scope.changeVersion = {selectedVersion: _.find(this.$scope.versionsList, {versionId: this.$scope.component.uniqueId})};
+ };
+
+ private getNewComponentBreadcrumbItem = ():MenuItem => {
+ let text = "";
+ if (this.$scope.component.isResource() && (<Resource>this.$scope.component).isCsarComponent()) {
+ text = this.$scope.component.getComponentSubType() + ': ' + this.$scope.component.name;
+ } else {
+ text = 'Create new ' + this.$state.params['type'];
+ }
+ return new MenuItem(text, null, States.WORKSPACE_GENERAL, 'goToState', [this.$state.params]);
+ };
+
+ private updateMenuItemByRole = (menuItems:Array<MenuItem>, role:string) => {
+ let tempMenuItems:Array<MenuItem> = new Array<MenuItem>();
+ menuItems.forEach((item:MenuItem) => {
+ //remove item if role is disabled
+ if (!(item.disabledRoles && item.disabledRoles.indexOf(role) > -1)) {
+ tempMenuItems.push(item);
+ }
+ });
+ return tempMenuItems;
+ };
+
+ private initBreadcrumbs = () => {
+ this.components = this.cacheService.get('breadcrumbsComponents');
+ let breadcrumbsComponentsLvl = this.MenuHandler.generateBreadcrumbsModelFromComponents(this.components, this.$scope.component);
+
+ if (this.$scope.isCreateMode()) {
+ let createItem = this.getNewComponentBreadcrumbItem();
+ if (!breadcrumbsComponentsLvl.menuItems) {
+ breadcrumbsComponentsLvl.menuItems = [];
+ }
+ breadcrumbsComponentsLvl.menuItems.unshift(createItem);
+ breadcrumbsComponentsLvl.selectedIndex = 0;
+ }
+
+ this.$scope.breadcrumbsModel = [breadcrumbsComponentsLvl, this.$scope.leftBarTabs];
+ };
+
+ private initMenuItems() {
+
+ let inCreateMode = this.$scope.isCreateMode();
+ this.$scope.leftBarTabs = new MenuItemGroup();
+ this.$scope.leftBarTabs.menuItems = this.updateMenuItemByRole(this.sdcMenu.component_workspace_menu_option[this.$scope.component.getComponentSubType()], this.role);
+
+ this.$scope.leftBarTabs.menuItems.forEach((item:MenuItem) => {
+ item.params = [item.state];
+ item.callback = this.$scope.onMenuItemPressed;
+ item.isDisabled = (inCreateMode && States.WORKSPACE_GENERAL != item.state) ||
+ (States.WORKSPACE_DEPLOYMENT === item.state && this.$scope.component.groups && this.$scope.component.groups.length === 0 && this.$scope.component.isResource());
+ });
+
+ if (this.cacheService.get('breadcrumbsComponents')) {
+ this.initBreadcrumbs();
+ } else {
+ let onSuccess = (components:Array<Component>) => {
+ this.cacheService.set('breadcrumbsComponents', components);
+ this.initBreadcrumbs();
+ };
+ this.EntityService.getCatalog().then(onSuccess); //getAllComponents() doesnt return components from catalog
+ }
+ }
+
+ private disableMenuItems() {
+ this.$scope.leftBarTabs.menuItems.forEach((item:MenuItem) => {
+ item.params = [item.state];
+ item.callback = this.$scope.onMenuItemPressed;
+ item.isDisabled = (States.WORKSPACE_GENERAL != item.state);
+ });
+ }
+
+ private enableMenuItems() {
+ this.$scope.leftBarTabs.menuItems.forEach((item:MenuItem) => {
+ item.params = [item.state];
+ item.callback = this.$scope.onMenuItemPressed;
+ item.isDisabled = false;
+ });
+ }
+
+ private showSuccessNotificationMessage = ():void => {
+ this.Notification.success({
+ message: this.$filter('translate')("IMPORT_VF_MESSAGE_CREATE_FINISHED_DESCRIPTION"),
+ title: this.$filter('translate')("IMPORT_VF_MESSAGE_CREATE_FINISHED_TITLE")
+ });
+ };
+
+}
+
+
diff --git a/catalog-ui/src/app/view-models/workspace/workspace-view.html b/catalog-ui/src/app/view-models/workspace/workspace-view.html
new file mode 100644
index 0000000000..dbb7fa6d63
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/workspace-view.html
@@ -0,0 +1,78 @@
+<div class="sdc-workspace-container">
+ <loader data-display="isLoading"></loader>
+ <div class="w-sdc-main-container">
+
+ <div class="w-sdc-left-sidebar" data-ng-if="!isComposition">
+ <div class="i-sdc-left-sidebar-item">
+ <expand-collapse-menu-box menu-items-group="leftBarTabs" menu-title="{{menuComponentTitle}}" parent-scope="this"> </expand-collapse-menu-box>
+ </div>
+ </div>
+
+ <div include-padding="true" class="w-sdc-main-right-container" data-ng-class="{'composition':isComposition}">
+ <loader data-display="isCreateProgress" data-ng-show="isCreateProgress" relative="false"></loader>
+
+ <div class="sdc-workspace-top-bar">
+ <div class="version-container">
+
+ <span data-ng-if="!isCreateMode() && !component.isLatestVersion()" class="not-latest"></span>
+ <select class="version-selector" data-ng-if="!isCreateMode()" data-tests-id="versionHeader" data-ng-model="changeVersion.selectedVersion"
+ ng-options="'V'+version.versionNumber for version in versionsList" data-ng-change="onVersionChanged(changeVersion.selectedVersion.versionId)">
+ </select>
+ </div>
+
+ <div class="lifecycle-state">
+ <div data-ng-show="showLifecycleIcon()" class="lifecycle-state-icon" data-ng-class="{'in-design-status-icon': isCreateMode(), '{{sdcMenu.LifeCycleStatuses[component.lifecycleState].icon}}': !isCreateMode()}"></div>
+ <span class="lifecycle-state-text" data-tests-id="formlifecyclestate">{{getStatus()}}</span>
+ </div>
+
+
+ <div class="progress-container" >
+ <top-progress class="general-view-top-progress" progress-value="progressService.getProgressValue(component.uniqueId)" progress-message="progressMessage"></top-progress>
+ </div>
+
+ <div class="sdc-workspace-top-bar-buttons">
+
+ <span ng-if="!isCreateMode() && !component.isLatestVersion() && !showChangeStateButton()">Switch to the&nbsp;<a ng-click="getLatestVersion()">latest version</a></span>
+
+ <button ng-if="isDesigner() && !isCreateMode()"
+ data-ng-class="{'disabled' :!isValidForm || isDisableMode() || isViewMode()}"
+ ng-click="save()"
+ class="tlv-btn blue"
+ data-tests-id="create/save"
+ data-ng-show="showFullIcons()"
+ sdc-smart-tooltip="">Update</button>
+
+ <button ng-repeat="(key,button) in changeLifecycleStateButtons"
+ ng-click="changeLifecycleState(key)"
+ ng-if="showChangeStateButton() && key != 'deleteVersion'"
+ data-ng-disabled="isCreateMode() || button.disabled || disabledButtons || !isValidForm"
+ class="change-lifecycle-state-btn tlv-btn"
+ ng-class="$first ? 'outline green' : 'grey'"
+ data-tests-id="{{button.text | testsId}}">
+ {{button.text}}
+ </button>
+
+ <button ng-if="!isViewMode() && isCreateMode()" data-ng-disabled="!isValidForm || isDisableMode() || isLoading" ng-click="save()" class="tlv-btn outline green" data-tests-id="create/save">Create</button>
+
+ <span data-ng-if="(isDesigner() || isProductManager()) && !isCreateMode() && component.lifecycleState === 'NOT_CERTIFIED_CHECKOUT'" sdc-smart-tooltip=""
+ data-ng-class="{'disabled' : !isValidForm || isDisableMode() || isViewMode()}" ng-click="changeLifecycleState('deleteVersion')"
+ class="sprite-new delete-btn" data-tests-id="delete_version" sdc-smart-tooltip="">Delete</span>
+
+ <span data-ng-if="isDesigner()" data-ng-class="{'disabled' :isDisableMode() || isViewMode()}" ng-click="revert()" class="sprite-new revert-btn" data-tests-id="revert"
+ data-ng-show="showFullIcons()" sdc-smart-tooltip="">Revert</span>
+
+ <span data-ng-if="isComposition && !component.isProduct()" class="sprite-new print-screen-btn" entity="component" print-graph-screen data-tests-id="printScreen"></span>
+ <span class="delimiter"></span>
+ <span class="sprite-new x-btn" data-ng-click="goToBreadcrumbHome()" sdc-smart-tooltip="">Close</span>
+
+ </div>
+ </div>
+
+ <div data-ng-if="component.creationDate && (!isComposition && !isDeployment)" class="sdc-asset-creation-info"><b>Created:</b>&nbsp;{{component.creationDate | date:'MM/dd/yyyy'}},&nbsp;{{component.creatorFullName}}&nbsp;|&nbsp;<b>Modifed:</b>&nbsp;{{component.lastUpdateDate | date:'MM/dd/yyyy'}}&nbsp;|&nbsp;<b>UUID:</b>&nbsp;{{component.uuid}}<b>&nbsp;Invariant UUID:</b>&nbsp;{{component.invariantUUID}}</div>
+
+ <div class="w-sdc-main-container-body-content" data-ng-class="{'third-party':thirdParty}" data-ui-view></div>
+ </div>
+ </div>
+ <top-nav search-bind="search.filterTerm" hide-search="true" menu-model="breadcrumbsModel" version="{{version}}"></top-nav>
+ <ecomp-footer></ecomp-footer>
+</div>
diff --git a/catalog-ui/src/app/view-models/workspace/workspace.less b/catalog-ui/src/app/view-models/workspace/workspace.less
new file mode 100644
index 0000000000..6cf024e335
--- /dev/null
+++ b/catalog-ui/src/app/view-models/workspace/workspace.less
@@ -0,0 +1,150 @@
+.sdc-workspace-container {
+ .bg_p;
+
+ .add-btn {
+ .f-color.a;
+ .f-type._12_m;
+ .hand;
+ float: right;
+ margin-bottom: 15px;
+
+ &:before {
+ .sprite-new;
+ .plus-icon;
+ margin-right: 5px;
+ content: "";
+
+ }
+ &:hover {
+ .f-color.b;
+ &:before {
+ .sprite-new;
+ .plus-icon-hover;
+ }
+ }
+
+ }
+ .w-sdc-left-sidebar {
+ padding: 3px 3px 0px 0px;
+ background-color: @main_color_p;
+ box-shadow: 7px -3px 6px -8px @main_color_n;
+ z-index: 2;
+ }
+
+ .sdc-asset-creation-info {
+ .n_12_r;
+ float: right;
+ margin: 8px 20px 0 0;
+ }
+
+ .w-sdc-main-right-container {
+
+ padding: 0px 0px 0px 0px;
+ background-color: @main_color_p;
+ z-index: 1;
+
+ .sdc-workspace-top-bar {
+ height: @action_nav_height;
+ padding: 12px 10px 0px 50px;
+ border-bottom: 1px solid @main_color_o;
+ display: flex;
+ justify-content: space-between;
+
+ .version-container {
+
+ }
+
+ .progress-container {
+ flex-grow: 4;
+ z-index: 10000000;
+
+ .general-view-top-progress {
+ width: 30%;
+ margin: 0 auto;
+ min-width: 200px;
+ }
+ }
+
+ .not-latest {
+ position: absolute;
+ left: 24px;
+ top: 20px;
+ .sprite-new;
+ .asdc-warning;
+ }
+
+ .sdc-workspace-top-bar-buttons {
+
+ > button, > span:not(.delimiter) {
+ margin-right: 10px;
+ vertical-align: middle;
+ .hand;
+
+ &.sprite-new {
+ text-indent: 100%;
+ }
+ &.disabled, &:hover.disabled {
+ pointer-events: none;
+ }
+ }
+ .delimiter {
+ height: 32px;
+ width: 1px;
+ background-color: #959595;
+ display: inline-block;
+ vertical-align: middle;
+ margin-right: 20px;
+ }
+
+ }
+
+ .lifecycle-state {
+ padding: 7px 0 0 10px;
+ margin: 2px 0 7px 10px;
+ border-left: 1px solid @main_color_o;
+ line-height: 15px;
+ font-family: @font-omnes-medium;
+ color: @main_color_m;
+
+ .lifecycle-state-icon {
+ .sprite-new;
+ }
+ .lifecycle-state-text {
+
+ font-weight: bold;
+ text-transform: uppercase;
+ vertical-align: top;
+ padding: 3px;
+ }
+ }
+
+ .version-selector {
+ // float:left;
+ background-color: transparent;
+ border: none;
+ margin-top: 6px;
+ }
+ }
+ .w-sdc-main-container-body-content {
+ height:calc(~'100% - @{action_nav_height}');
+
+ text-align: center;
+ align-items: center;
+ padding: 40px 14% 20px 14%;
+ &.third-party {
+ text-align: left;
+ padding: 0;
+ position: absolute;
+ top: @action_nav_height;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ }
+ }
+ }
+}
+
+
+.properties-assignment .sdc-workspace-container .w-sdc-main-right-container .w-sdc-main-container-body-content{
+ padding: 80px 2% 40px 2%;
+} \ No newline at end of file