diff options
author | ys9693 <ys9693@att.com> | 2020-01-19 13:50:02 +0200 |
---|---|---|
committer | Ofir Sonsino <ofir.sonsino@intl.att.com> | 2020-01-22 12:33:31 +0000 |
commit | 16a9fce0e104a38371a9e5a567ec611ae3fc7f33 (patch) | |
tree | 03a2aff3060ddb5bc26a90115805a04becbaffc9 /catalog-ui/src/app/ng2/pages | |
parent | aa83a2da4f911c3ac89318b8e9e8403b072942e1 (diff) |
Catalog alignment
Issue-ID: SDC-2724
Signed-off-by: ys9693 <ys9693@att.com>
Change-Id: I52b4aacb58cbd432ca0e1ff7ff1f7dd52099c6fe
Diffstat (limited to 'catalog-ui/src/app/ng2/pages')
309 files changed, 20838 insertions, 1643 deletions
diff --git a/catalog-ui/src/app/ng2/pages/automated-upgrade/automated-upgrade-models/ui-component-to-upgrade.ts b/catalog-ui/src/app/ng2/pages/automated-upgrade/automated-upgrade-models/ui-component-to-upgrade.ts index 97fb71e210..17e5ea7ef1 100644 --- a/catalog-ui/src/app/ng2/pages/automated-upgrade/automated-upgrade-models/ui-component-to-upgrade.ts +++ b/catalog-ui/src/app/ng2/pages/automated-upgrade/automated-upgrade-models/ui-component-to-upgrade.ts @@ -21,7 +21,7 @@ export class ServiceContainerToUpgradeUiObject extends UiBaseObject { this.icon = componentToUpgrade.icon; this.version = componentToUpgrade.version; this.isAlreadyUpgrade = true; - this.isLock = componentToUpgrade.state === ComponentState.CERTIFICATION_IN_PROGRESS || componentToUpgrade.state === ComponentState.NOT_CERTIFIED_CHECKOUT; + this.isLock = componentToUpgrade.state === ComponentState.NOT_CERTIFIED_CHECKOUT; this.vspInstances = []; } diff --git a/catalog-ui/src/app/ng2/pages/automated-upgrade/automated-upgrade-ui-components/upgrade-list-item-status/upgrade-list-status-item.component.html b/catalog-ui/src/app/ng2/pages/automated-upgrade/automated-upgrade-ui-components/upgrade-list-item-status/upgrade-list-status-item.component.html index f77c3410a6..c1e9529869 100644 --- a/catalog-ui/src/app/ng2/pages/automated-upgrade/automated-upgrade-ui-components/upgrade-list-item-status/upgrade-list-status-item.component.html +++ b/catalog-ui/src/app/ng2/pages/automated-upgrade/automated-upgrade-ui-components/upgrade-list-item-status/upgrade-list-status-item.component.html @@ -13,7 +13,6 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> - <div class="components-to-upgrade-list-item"> <div class="component-to-upgrade-data"> <div class="component-to-upgrade-icon small sprite-services-icons {{upgradedComponent.icon}}"></div> diff --git a/catalog-ui/src/app/ng2/pages/automated-upgrade/automated-upgrade-ui-components/upgrade-list-item/upgrade-list-item.component.html b/catalog-ui/src/app/ng2/pages/automated-upgrade/automated-upgrade-ui-components/upgrade-list-item/upgrade-list-item.component.html index b97e41444c..5c49735a81 100644 --- a/catalog-ui/src/app/ng2/pages/automated-upgrade/automated-upgrade-ui-components/upgrade-list-item/upgrade-list-item.component.html +++ b/catalog-ui/src/app/ng2/pages/automated-upgrade/automated-upgrade-ui-components/upgrade-list-item/upgrade-list-item.component.html @@ -13,8 +13,6 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> - - <div class="components-to-upgrade-list-item "> <div class="component-to-upgrade-data"> <sdc-checkbox class="component-to-upgrade-checkbox" diff --git a/catalog-ui/src/app/ng2/pages/automated-upgrade/automated-upgrade.component.ts b/catalog-ui/src/app/ng2/pages/automated-upgrade/automated-upgrade.component.ts index 9ae73497ef..613caa4b8d 100644 --- a/catalog-ui/src/app/ng2/pages/automated-upgrade/automated-upgrade.component.ts +++ b/catalog-ui/src/app/ng2/pages/automated-upgrade/automated-upgrade.component.ts @@ -29,8 +29,7 @@ import {AutomatedUpgradeService} from "./automated-upgrade.service"; @Component({ selector: 'upgrade-vsp', templateUrl: './automated-upgrade.component.html', - styleUrls: ['./automated-upgrade.component.less'], - providers: [TranslateService] + styleUrls: ['./automated-upgrade.component.less'] }) export class AutomatedUpgradeComponent { diff --git a/catalog-ui/src/app/ng2/pages/automated-upgrade/automated-upgrade.module.ts b/catalog-ui/src/app/ng2/pages/automated-upgrade/automated-upgrade.module.ts index 19f6412071..8a4e8fb660 100644 --- a/catalog-ui/src/app/ng2/pages/automated-upgrade/automated-upgrade.module.ts +++ b/catalog-ui/src/app/ng2/pages/automated-upgrade/automated-upgrade.module.ts @@ -2,16 +2,15 @@ * Created by ob0695 on 4/18/2018. */ import { NgModule } from "@angular/core"; -import {SdcUiComponentsModule} from "sdc-ui/lib/angular/index"; -import {CommonModule} from "@angular/common"; -import {AutomatedUpgradeStatusComponent} from "./automated-upgrade-status/automated-upgrade-status.component"; -import {AutomatedUpgradeComponent} from "./automated-upgrade.component"; -import {UpgradeListItemComponent} from "./automated-upgrade-ui-components/upgrade-list-item/upgrade-list-item.component"; -import {UpgradeListItemStatusComponent} from "./automated-upgrade-ui-components/upgrade-list-item-status/upgrade-list-status-item.component"; -import {TranslateService} from "../../shared/translator/translate.service"; -import {UpgradeListItemInnerContent} from "./automated-upgrade-ui-components/list-item-inner-content/list-item-inner-content.component"; -import {UpgradeLineItemComponent} from "./automated-upgrade-ui-components/upgrade-line-item/upgrade-line-item.component"; -import {UpgradeListItemOrderPipe} from "./automated-upgrade-ui-components/list-item-order-pipe/list-item-order-pipe"; +import { SdcUiComponentsModule } from "onap-ui-angular"; +import { CommonModule } from "@angular/common"; +import { AutomatedUpgradeStatusComponent } from "./automated-upgrade-status/automated-upgrade-status.component"; +import { AutomatedUpgradeComponent } from "./automated-upgrade.component"; +import { UpgradeListItemComponent } from "./automated-upgrade-ui-components/upgrade-list-item/upgrade-list-item.component"; +import { UpgradeListItemStatusComponent } from "./automated-upgrade-ui-components/upgrade-list-item-status/upgrade-list-status-item.component"; +import { UpgradeListItemInnerContent } from "./automated-upgrade-ui-components/list-item-inner-content/list-item-inner-content.component"; +import { UpgradeLineItemComponent } from "./automated-upgrade-ui-components/upgrade-line-item/upgrade-line-item.component"; +import { UpgradeListItemOrderPipe } from "./automated-upgrade-ui-components/list-item-order-pipe/list-item-order-pipe"; @NgModule({ declarations: [ @@ -27,8 +26,7 @@ import {UpgradeListItemOrderPipe} from "./automated-upgrade-ui-components/list-i exports: [], entryComponents: [ AutomatedUpgradeComponent, AutomatedUpgradeStatusComponent - ], - providers: [TranslateService] + ] }) export class AutomatedUpgradeModule { }
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/automated-upgrade/automated-upgrade.service.ts b/catalog-ui/src/app/ng2/pages/automated-upgrade/automated-upgrade.service.ts index 0acfececaa..14ca7f0947 100644 --- a/catalog-ui/src/app/ng2/pages/automated-upgrade/automated-upgrade.service.ts +++ b/catalog-ui/src/app/ng2/pages/automated-upgrade/automated-upgrade.service.ts @@ -1,19 +1,15 @@ -import {SdcUiComponents} from "sdc-ui/lib/angular"; -import {Injectable, Inject} from "@angular/core"; -import {IModalConfig} from "sdc-ui/lib/angular/modals/models/modal-config"; -import {AutomatedUpgradeComponent} from "./automated-upgrade.component"; -import {Component} from "../../../models/components/component"; -import {ComponentServiceNg2} from "../../services/component-services/component.service"; -import {GeneralStatus, ComponentType} from "../../../utils/constants"; -import {IDependenciesServerResponse} from "../../services/responses/dependencies-server-response"; -import {AutomatedUpgradeStatusComponent} from "./automated-upgrade-status/automated-upgrade-status.component"; -import {AutomatedUpgradeStatusResponse} from "../../services/responses/automated-upgrade-response"; +import { SdcUiComponents, SdcUiCommon, SdcUiServices } from "onap-ui-angular"; +import { Injectable, ComponentRef } from "@angular/core"; +import { AutomatedUpgradeComponent } from "./automated-upgrade.component"; +import { Component } from "../../../models/components/component"; +import { ComponentServiceNg2 } from "../../services/component-services/component.service"; +import { GeneralStatus, ComponentType } from "../../../utils/constants"; +import { IDependenciesServerResponse } from "../../services/responses/dependencies-server-response"; +import { AutomatedUpgradeStatusComponent } from "./automated-upgrade-status/automated-upgrade-status.component"; +import { AutomatedUpgradeStatusResponse } from "../../services/responses/automated-upgrade-response"; +import { TranslateService, ITranslateArgs } from "../../shared/translator/translate.service"; +import { ServiceContainerToUpgradeUiObject, AllottedResourceInstanceUiObject, VspInstanceUiObject } from "./automated-upgrade-models/ui-component-to-upgrade"; import Dictionary = _.Dictionary; -import {TranslateService, ITranslateArgs} from "../../shared/translator/translate.service"; -import { - ServiceContainerToUpgradeUiObject, - AllottedResourceInstanceUiObject, VspInstanceUiObject -} from "./automated-upgrade-models/ui-component-to-upgrade"; export interface IAutomatedUpgradeRequestObj { serviceId:string; @@ -30,8 +26,9 @@ export class AutomatedUpgradeService { private vspComponent:Component; private uiComponentsToUpgrade:Array<ServiceContainerToUpgradeUiObject>; private componentType:string; + private modalInstance: ComponentRef<SdcUiComponents.ModalComponent>; - constructor(private modalService:SdcUiComponents.ModalService, + constructor(private modalService:SdcUiServices.ModalService, private componentService:ComponentServiceNg2, private translateService:TranslateService) { } @@ -69,21 +66,21 @@ export class AutomatedUpgradeService { } private disabledAllModalButtons = ():void => { - this.modalService.getCurrentInstance().innerModalContent.instance.disabled = true; - this.modalService.getCurrentInstance().buttons[0].show_spinner = true; - this.modalService.getCurrentInstance().buttons[1].disabled = true; + this.modalInstance.instance.innerModalContent.instance.disabled = true; + this.modalInstance.instance.buttons[0].show_spinner = true; + this.modalInstance.instance.buttons[1].disabled = true; } public changeUpgradeButtonState = (isDisabled:boolean):void => { - if (this.modalService.getCurrentInstance().buttons[0].disabled !== isDisabled) { - this.modalService.getCurrentInstance().buttons[0].disabled = isDisabled; + if (this.modalInstance.instance.buttons[0].disabled !== isDisabled) { + this.modalInstance.instance.buttons[0].disabled = isDisabled; } } //TODO We will need to replace this function after sdc-ui modal new design, this is just a workaround public automatedUpgrade = ():void => { - let selectedServices = this.modalService.getCurrentInstance().innerModalContent.instance.selectedComponentsToUpgrade; + let selectedServices = this.modalInstance.instance.innerModalContent.instance.selectedComponentsToUpgrade; this.disabledAllModalButtons(); this.componentService.automatedUpgrade(this.vspComponent.componentType, this.vspComponent.uniqueId, this.convertToServerRequest(selectedServices)).subscribe((automatedUpgradeStatus:any) => { @@ -105,11 +102,11 @@ export class AutomatedUpgradeService { }); let statusModalTitle = this.getTextByComponentType("_UPGRADE_STATUS_TITLE"); - this.modalService.getCurrentInstance().setTitle(statusModalTitle); - this.modalService.getCurrentInstance().getButtons().splice(0, 1); // Remove the upgrade button - this.modalService.getCurrentInstance().buttons[0].disabled = false; // enable close again - this.modalService.getCurrentInstance().innerModalContent.destroy(); - this.modalService.createInnnerComponent(AutomatedUpgradeStatusComponent, { + this.modalInstance.instance.setTitle(statusModalTitle); + this.modalInstance.instance.getButtons().splice(0, 1); // Remove the upgrade button + this.modalInstance.instance.buttons[0].disabled = false; // enable close again + this.modalInstance.instance.innerModalContent.destroy(); + this.modalService.createInnnerComponent(this.modalInstance, AutomatedUpgradeStatusComponent, { upgradedComponentsList: upgradedComponent, upgradeStatusMap: statusMap, statusText: this.getStatusText(statusMap) @@ -250,10 +247,10 @@ export class AutomatedUpgradeService { let modalTitle = this.getTextByComponentType("_UPGRADE_TITLE"); let certificationText = isAfterCertification ? this.getTextByComponentType("_CERTIFICATION_STATUS_TEXT", {resourceName: this.vspComponent.name}) : undefined; - let upgradeVspModalConfig:IModalConfig = { + let upgradeVspModalConfig = { title: modalTitle, size: "md", - type: "custom", + type: SdcUiCommon.ModalType.custom, testId: "upgradeVspModal", buttons: [ { @@ -266,10 +263,11 @@ export class AutomatedUpgradeService { }, {text: 'CLOSE', size: 'sm', closeModal: true, type: 'secondary'} - ] - }; + ] as SdcUiCommon.IModalButtonComponent[] + } as SdcUiCommon.IModalConfig; - this.modalService.openCustomModal(upgradeVspModalConfig, AutomatedUpgradeComponent, { + this.modalInstance = this.modalService.openModal(upgradeVspModalConfig); + this.modalService.createInnnerComponent(this.modalInstance, AutomatedUpgradeComponent, { componentsToUpgrade: this.uiComponentsToUpgrade, informationText: informationalText, certificationStatusText: certificationText diff --git a/catalog-ui/src/app/ng2/pages/catalog/__snapshots__/catalog.component.spec.ts.snap b/catalog-ui/src/app/ng2/pages/catalog/__snapshots__/catalog.component.spec.ts.snap new file mode 100644 index 0000000000..d6091cd599 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/catalog/__snapshots__/catalog.component.spec.ts.snap @@ -0,0 +1,164 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`catalog component should match current snapshot of catalog component 1`] = ` +<catalog + $state={[Function Object]} + cacheService={[Function Object]} + catalogService={[Function Object]} + componentShouldReload={[Function Function]} + defaultFilterParams={[Function Object]} + getTestIdForCheckboxByText={[Function Function]} + initCatalogData={[Function Function]} + initLeftSwitch={[Function Function]} + initScopeMembers={[Function Function]} + isDefaultFilter={[Function Function]} + loaderService={[Function Object]} + resourceNamePipe={[Function Object]} + sdcConfig={[Function Object]} + sdcMenu={[Function Object]} + updateCatalogItems={[Function Function]} +> + <div + class="sdc-catalog-container" + > + <div + class="w-sdc-main-container" + > + <div + class="i-sdc-designer-leftbar-section-left-switch-header" + > + <div + class="i-sdc-designer-leftbar-section-left-switch-header-text" + > + + </div> + <div + class="i-sdc-designer-leftbar-section-left-switch-header-icon sprite-new arrow-up-small" + > + Â + </div> + + </div> + <div + class="sdc-catalog-body-container w-sdc-left-sidebar i-sdc-designer-left-sidebar" + perfectscrollbar="" + > + <div + class="sdc-catalog-leftbar-container" + > + <div + class="sdc-catalog-type-filter-container" + > + <div + class="i-sdc-designer-leftbar-section-title pointer" + > + <span + class="i-sdc-designer-leftbar-section-title-icon" + /> + <span + class="i-sdc-designer-leftbar-section-title-text" + data-tests-id="typeFilterTitle" + > + Type + </span> + </div> + <div + class="i-sdc-designer-leftbar-section-content" + > + <sdc-checklist /> + </div> + </div> + <div + class="sdc-catalog-categories-filter-container" + > + <div + class="i-sdc-designer-leftbar-section-title pointer" + > + <span + class="i-sdc-designer-leftbar-section-title-icon" + /> + <span + class="i-sdc-designer-leftbar-section-title-text" + data-tests-id="categoriesFilterTitle" + > + Categories + </span> + </div> + <div + class="i-sdc-designer-leftbar-section-content" + > + <sdc-checklist /> + </div> + </div> + <div + class="sdc-catalog-status-filter-container" + > + <div + class="i-sdc-designer-leftbar-section-title pointer" + > + <span + class="i-sdc-designer-leftbar-section-title-icon" + /> + <span + class="i-sdc-designer-leftbar-section-title-text" + data-tests-id="statusFilterTitle" + > + Status + </span> + </div> + <div + class="i-sdc-designer-leftbar-section-content" + > + <sdc-checklist /> + </div> + </div> + </div> + </div> + <div + class="w-sdc-main-right-container w-sdc-catalog-main" + infinitescroll="" + > + <div + class="catalog-top-bar" + > + <div + class="w-sdc-dashboard-catalog-items-header" + /> + <div + class="catalog-top-right-bar" + > + <span + class="w-sdc-dashboard-catalog-header-order1" + > + + </span> + Â Â + <a + class="w-sdc-dashboard-catalog-sort" + data-tests-id="sort-by-last-update" + > + + </a> + Â + + <a + class="w-sdc-dashboard-catalog-sort" + data-tests-id="sort-by-alphabetical" + > + + </a> + Â + + </div> + </div> + <div + class="catalog-elements-list" + > + + </div> + </div> + </div> + <top-nav /> + </div> +</catalog> +`; diff --git a/catalog-ui/src/app/ng2/pages/catalog/catalog.component.html b/catalog-ui/src/app/ng2/pages/catalog/catalog.component.html new file mode 100644 index 0000000000..4a13bee973 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/catalog/catalog.component.html @@ -0,0 +1,101 @@ +<div class="sdc-catalog-container"> + <div class="w-sdc-main-container"> + <div class="i-sdc-designer-leftbar-section-left-switch-header" + (click)="showCatalogSelector=!showCatalogSelector"> + <div class="i-sdc-designer-leftbar-section-left-switch-header-text"> + {{selectedCatalogItem.title}} + </div> + <div class="i-sdc-designer-leftbar-section-left-switch-header-icon sprite-new arrow-up-small"> </div> + + <div class="sdc-catalog-selector-wrapper" *ngIf="showCatalogSelector"> + <div class="sdc-catalog-selector-item" + *ngFor="let leftSwitchItem of catalogSelectorItems" + (click)="selectLeftSwitchItem(leftSwitchItem)"> + <span>{{leftSwitchItem.title}}</span> + </div> + </div> + </div> + + <!-- LEFT SIDE --> + <div perfectScrollbar class="sdc-catalog-body-container w-sdc-left-sidebar i-sdc-designer-left-sidebar"> + <div class="sdc-catalog-leftbar-container"> + <div class="sdc-catalog-type-filter-container"> + <div class="i-sdc-designer-leftbar-section-title pointer" + (click)="sectionClick('type')" + [ngClass]="{'expanded': expandedSection.indexOf('type') !== -1}"> + <span class="i-sdc-designer-leftbar-section-title-icon"></span> + <span class="i-sdc-designer-leftbar-section-title-text" + data-tests-id="typeFilterTitle">Type</span> + </div> + <div class="i-sdc-designer-leftbar-section-content"> + <sdc-checklist [checklistModel]="typesChecklistModel" [testId]="'checklist-type'" + (checkedChange)="gui.onComponentTypeClick()"></sdc-checklist> + </div> + </div> + + <div class="sdc-catalog-categories-filter-container"> + <div class="i-sdc-designer-leftbar-section-title pointer" (click)="sectionClick('category')" + [ngClass]="{'expanded': expandedSection.indexOf('category') !== -1}"> + <span class="i-sdc-designer-leftbar-section-title-icon"></span> + <span class="i-sdc-designer-leftbar-section-title-text" data-tests-id="categoriesFilterTitle">Categories</span> + </div> + <div class="i-sdc-designer-leftbar-section-content"> + <sdc-checklist [checklistModel]="categoriesChecklistModel" [testId]="'checklist-category'" + (checkedChange)="gui.onCategoryClick()"></sdc-checklist> + </div> + </div> + + <!-- STATUS --> + <div class="sdc-catalog-status-filter-container"> + <div class="i-sdc-designer-leftbar-section-title pointer" (click)="sectionClick('status')" + [ngClass]="{'expanded': expandedSection.indexOf('status') !== -1}"> + <span class="i-sdc-designer-leftbar-section-title-icon"></span> + <span class="i-sdc-designer-leftbar-section-title-text" + data-tests-id="statusFilterTitle">Status</span> + </div> + + <div class="i-sdc-designer-leftbar-section-content"> + <sdc-checklist [checklistModel]="statusChecklistModel" [testId]="'checklist-status'" + (checkedChange)="gui.onStatusClick()"></sdc-checklist> + </div> + </div> + + </div> + </div> + + <!-- RIGHT SIDE --> + <div class="w-sdc-main-right-container w-sdc-catalog-main" infiniteScroll + (infiniteScroll)="raiseNumberOfElementToDisplay()" [infiniteScrollDistance]="100"> + <!-- HEADER --> + <div class="catalog-top-bar"> + <div class="w-sdc-dashboard-catalog-items-header" + [innerHTML]="getNumOfElements(catalogFilteredItems.length)"> + + </div> + <div class="catalog-top-right-bar"> + <span class="w-sdc-dashboard-catalog-header-order1">{{'SORT_CAPTION'|translate}}</span> + <a class="w-sdc-dashboard-catalog-sort" data-tests-id="sort-by-last-update" + [ngClass]="{'blue' : sortBy==='lastUpdateDate'}" + (click)="order('lastUpdateDate')">{{'SORT_BY_UPDATE_DATE'|translate}}</a> + <span *ngIf="sortBy === 'lastUpdateDate'" class="w-sdc-catalog-sort-arrow" + [ngClass]="{'down': reverse, 'up':!reverse}"></span> + <a class="w-sdc-dashboard-catalog-sort" data-tests-id="sort-by-alphabetical" + [ngClass]="{'blue' : sortBy!=='lastUpdateDate'}" + (click)="order('resourceName')">{{'SORT_ALPHABETICAL'|translate}}</a> + <span *ngIf="sortBy !== 'lastUpdateDate'" class="w-sdc-catalog-sort-arrow" + [ngClass]="{'down': reverse, 'up':!reverse}"></span> + </div> + </div> + + <div class='catalog-elements-list'> + <!-- Tile new --> + <ui-tile *ngFor="let component of catalogFilteredSlicedItems" + [component]="component" (onTileClick)="goToComponent(component)"></ui-tile> + <!-- Tile new --> + </div> + </div> + </div> + + <top-nav [topLvlSelectedIndex]="1" [searchTerm]="search.filterTerm" + (searchTermChange)="gui.changeFilterTerm($event)" [version]="version"></top-nav> +</div> diff --git a/catalog-ui/src/app/ng2/pages/catalog/catalog.component.less b/catalog-ui/src/app/ng2/pages/catalog/catalog.component.less new file mode 100644 index 0000000000..036db8d94d --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/catalog/catalog.component.less @@ -0,0 +1,113 @@ +@import '../../../../assets/styles/variables'; +@import '../../../../assets/styles/mixins'; + +.w-sdc-catalog-main { + height: 100%; + overflow-y: scroll; + padding: 10px 50px; + + .catalog-top-bar { + display: flex; + justify-content:space-between; + padding: 0px 10px; + } + + .catalog-elements-list { + display: flex; + flex-wrap: wrap; + flex-direction: row; + } + +} + + +.i-sdc-designer-left-sidebar { + margin-top: 43px; +} + +.sdc-catalog-selector-item { + text-transform: none; + line-height: 40px; + font-family: OpenSans-Bold, sans-serif; + font-size: 14px; + color: #000000; + background-color: #ffffff; + padding-left: 20px; +} + +.sdc-catalog-selector-wrapper { + position: absolute; + left: 0px; + top: 42px; + width: 241px; + height: auto; + cursor: pointer; + opacity: 1; + z-index: 1000; + box-shadow: 1px 2px 3px #b1b1b1; +} + +.sdc-catalog-selector-item { + text-transform: none; + line-height: 40px; + font-family: OpenSans-Bold, sans-serif; + font-size: 14px; + color: @main_color_l; + background-color: @main_color_p; + padding-left: 20px; +} + +.sdc-catalog-selector-item:hover { + color: @main_color_a; + background-color: @tlv_color_v; +} + +.i-sdc-designer-leftbar-section-left-switch-header { + text-transform: uppercase; + .l_14_m; + line-height: 40px; + width: 243px; + + font-family: OpenSans-Bold, sans-serif; + font-size: 14px; + + color: @main_color_a; + background-color: @tlv_color_t; + border: solid 1px fade(@main_color_t, 40%); + cursor: pointer; + opacity: 1; + z-index: 9999; + position: relative; +} + +.i-sdc-designer-leftbar-section-left-switch-header-text { + display: inline-block; + width: 180px; + margin-left: 20px; +} + +.i-sdc-designer-leftbar-section-left-switch-header-icon { + display: inline-block; + vertical-align: middle; +} + +.w-sdc-dashboard-catalog-items-header { + color: @main_color_m; + font-family: OpenSans-Regular, sans-serif; + font-size: 12px; + display: inline-block; + font-style: normal; size: 12px; + margin-left: 11px; + font-weight: normal; + b { + font-family: OpenSans-Bold, sans-serif; + color: @main_color_l; + font-weight: bold; + } +} + +.w-sdc-dashboard-catalog-header-order1 { + font-style: normal; + font-size: 12px; + font-weight: 800; +} diff --git a/catalog-ui/src/app/ng2/pages/catalog/catalog.component.spec.ts b/catalog-ui/src/app/ng2/pages/catalog/catalog.component.spec.ts new file mode 100644 index 0000000000..ff27ec77fd --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/catalog/catalog.component.spec.ts @@ -0,0 +1,649 @@ + +import { async, ComponentFixture, TestBed } from "@angular/core/testing"; +import {ConfigureFn, configureTests} from "../../../../jest/test-config.helper"; +import {NO_ERRORS_SCHEMA} from "@angular/core"; +import { CacheService} from "../../../../app/services-ng2"; +import {CatalogComponent} from "./catalog.component"; +import { SdcUiServices } from "onap-ui-angular"; +import { SdcConfigToken } from "../../config/sdc-config.config"; +import { SdcMenuToken} from "../../config/sdc-menu.config"; +import { ResourceNamePipe } from "../../pipes/resource-name.pipe"; +import { CatalogService } from "../../services/catalog.service"; +import {TranslatePipe} from "../../shared/translator/translate.pipe"; +import {TranslateService} from "../../shared/translator/translate.service"; +import {Observable} from "rxjs"; +import {LoaderService} from "onap-ui-angular/dist/loader/loader.service"; +import {categoriesElements} from "../../../../jest/mocks/categories.mock"; +import {sdcMenu} from "../../../../jest/mocks/sdc-menu.mock"; +import {IEntityFilterObject} from "../../pipes/entity-filter.pipe"; + + + + + +describe('catalog component', () => { + + let fixture: ComponentFixture<CatalogComponent>; + + //Data variables + let catalogSelectorItemsMock; + let checkListModelMock; + let filterParamsMock; + let checkboxesFilterMock; + let checkboxesFilterKeysMock; + + + //Service variables + let stateServiceMock; + let cacheServiceMock: Partial<CacheService>; + let loaderServiceMock: Partial<LoaderService>; + let catalogServiceMock: Partial<CatalogService>; + + + beforeEach( + + async(() => { + console.info = jest.fn(); + catalogSelectorItemsMock = [ + { + value: 0, + title: 'Active Items', + header: 'Active' + }, + { + value: 1, + title: 'Archive', + header: 'Archived' + } + ]; + checkListModelMock = { + checkboxes: [ + {label: "VF", disabled: false, isChecked: false, testId: "checkbox-vf", value: "Resource.VF"}, + {label: "VFC", disabled: false, isChecked: false, testId: "checkbox-vfc", value: "Resource.VFC", + subLevelChecklist: {checkboxes:[{label: "VFD", disabled: false, isChecked: false, testId: "checkbox-vfd", value: "Resource.VFD"}], + selectedValues: ["Resource.VFD"]} + }, + {label: "CR", disabled: false, isChecked: false, testId: "checkbox-cr", value: "Resource.CR", + subLevelChecklist: { checkboxes:[{label: "VF", disabled: false, isChecked: false, testId: "checkbox-vf", value: "Resource.VF"}], + selectedValues: []} + }], + selectedValues: ["Resource.VF"] + } + filterParamsMock = { + active: true, + categories: ["resourceNewCategory.allotted resource.allotted resource", "resourceNewCategory.allotted resource.contrail route", "resourceNewCategory.application l4+.application server"], + components: ["Resource.VF", "Resource.VFC"], + order: ["lastUpdateDate", true], + statuses: ["inDesign"], + term: "Vf" + } + checkboxesFilterMock = { + selectedCategoriesModel: ["serviceNewCategory.network l4+", "resourceNewCategory.allotted resource.allotted resource"], + selectedComponentTypes: ["Resource.VF", "Resource.VFC"], + selectedResourceSubTypes: ["VF", "VFC"], + selectedStatuses: ["NOT_CERTIFIED_CHECKOUT", "NOT_CERTIFIED_CHECKIN"] + }; + checkboxesFilterKeysMock = { + categories:{_main: ["serviceNewCategory.network l4+"]}, + componentTypes: { Resource: ["Resource.VF", "Resource.VFC"], _main: ["Resource.VFC"]}, + statuses: {_main: ["inDesign"]} + } + + stateServiceMock = { + go: jest.fn(), + current: jest.fn() + }; + cacheServiceMock = { + get: jest.fn().mockImplementation(()=> categoriesElements), + set: jest.fn(), + contains: jest.fn().mockImplementation(()=> true) + }; + loaderServiceMock = { + activate: jest.fn(), + deactivate: jest.fn() + }; + catalogServiceMock = { + //TODO create mock function of archive + getCatalog: jest.fn().mockImplementation(()=> Observable.of(categoriesElements)), + getArchiveCatalog: jest.fn().mockImplementation(()=> Observable.of(categoriesElements)) + }; + const configure: ConfigureFn = testBed => { + testBed.configureTestingModule({ + declarations: [CatalogComponent, TranslatePipe], + imports: [], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + {provide: SdcConfigToken, useValue: {}}, + {provide: SdcMenuToken, useValue: sdcMenu}, + {provide: "$state", useValue: stateServiceMock }, + {provide: CacheService, useValue: cacheServiceMock }, + {provide: CatalogService, useValue: catalogServiceMock }, + {provide: ResourceNamePipe, useValue: {}}, + {provide: SdcUiServices.LoaderService, useValue: loaderServiceMock }, + {provide: TranslateService, useValue: {}} + ], + }); + }; + + configureTests(configure).then(testBed => { + fixture = testBed.createComponent(CatalogComponent); + }); + }) + ); + + + it('should match current snapshot of catalog component', () => { + expect(fixture).toMatchSnapshot(); + }); + + it ('should call on catalog component onInit' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.initGui = jest.fn(); + component.componentInstance.initLeftSwitch = jest.fn(); + component.componentInstance.initScopeMembers = jest.fn(); + component.componentInstance.loadFilterParams = jest.fn(); + component.componentInstance.initCatalogData = jest.fn(); + component.componentInstance.ngOnInit(); + expect(component.componentInstance.initGui).toHaveBeenCalled(); + expect(component.componentInstance.initLeftSwitch).toHaveBeenCalled(); + expect(component.componentInstance.initScopeMembers).toHaveBeenCalled(); + expect(component.componentInstance.loadFilterParams).toHaveBeenCalled(); + expect(component.componentInstance.initCatalogData).toHaveBeenCalled(); + }); + + it ('should call on catalog component initLeftSwitch' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.initLeftSwitch(); + expect(component.componentInstance.showCatalogSelector).toEqual(false); + expect(component.componentInstance.catalogSelectorItems).toEqual(catalogSelectorItemsMock); + expect(component.componentInstance.selectedCatalogItem).toEqual(catalogSelectorItemsMock[0]); + }); + + it ('should call on catalog component initCatalogData and selectedCatalogItem is archive ' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.getArchiveCatalogItems = jest.fn(); + component.componentInstance.selectedCatalogItem = catalogSelectorItemsMock[1]; + component.componentInstance.initCatalogData(); + expect(component.componentInstance.getArchiveCatalogItems).toHaveBeenCalled(); + }); + + it ('should call on catalog component initCatalogData and selectedCatalogItem is active ' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.getActiveCatalogItems = jest.fn(); + component.componentInstance.selectedCatalogItem = catalogSelectorItemsMock[0]; + component.componentInstance.initCatalogData(); + expect(component.componentInstance.getActiveCatalogItems).toHaveBeenCalled(); + }); + + it ('should call on catalog component initScopeMembers' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.makeSortedCategories = jest.fn().mockImplementation(()=> categoriesElements); + component.componentInstance.initCategoriesMap = jest.fn(); + component.componentInstance.initCheckboxesFilter = jest.fn(); + component.componentInstance.initCheckboxesFilterKeys = jest.fn(); + component.componentInstance.buildCheckboxLists = jest.fn(); + component.componentInstance.initScopeMembers(); + expect(component.componentInstance.numberOfItemToDisplay).toEqual(0); + expect(component.componentInstance.categories).toEqual(categoriesElements); + expect(component.componentInstance.confStatus).toEqual(component.componentInstance.sdcMenu.statuses); + expect(component.componentInstance.expandedSection).toEqual( ["type", "category", "status"]); + expect(component.componentInstance.catalogItems).toEqual([]); + expect(component.componentInstance.search).toEqual({FilterTerm: ""}); + expect(component.componentInstance.initCategoriesMap).toHaveBeenCalled(); + expect(component.componentInstance.initCheckboxesFilter).toHaveBeenCalled(); + expect(component.componentInstance.initCheckboxesFilterKeys).toHaveBeenCalled(); + expect(component.componentInstance.buildCheckboxLists).toHaveBeenCalled(); + expect(component.componentInstance.version).toEqual(categoriesElements); + expect(component.componentInstance.sortBy).toEqual('lastUpdateDate'); + expect(component.componentInstance.reverse).toEqual(true); + }); + + it ('should call on catalog component buildCheckboxLists ' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.buildChecklistModelForTypes = jest.fn(); + component.componentInstance.buildChecklistModelForCategories = jest.fn(); + component.componentInstance.buildChecklistModelForStatuses = jest.fn(); + component.componentInstance.buildCheckboxLists(); + expect(component.componentInstance.buildChecklistModelForTypes).toHaveBeenCalled(); + expect(component.componentInstance.buildChecklistModelForCategories).toHaveBeenCalled(); + expect(component.componentInstance.buildChecklistModelForStatuses).toHaveBeenCalled(); + }); + + it ('should call on catalog component getTestIdForCheckboxByText ' , () => { + const component = TestBed.createComponent(CatalogComponent); + let testId = component.componentInstance.getTestIdForCheckboxByText("catalog filter"); + expect(testId).toEqual("checkbox-catalogfilter"); + }); + + it ('should call on catalog component selectLeftSwitchItem with active catalog' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.selectedCatalogItem = catalogSelectorItemsMock[1]; + component.componentInstance.getActiveCatalogItems = jest.fn(); + component.componentInstance.changeFilterParams = jest.fn(); + component.componentInstance.selectLeftSwitchItem(catalogSelectorItemsMock[0]); + expect(component.componentInstance.getActiveCatalogItems).toBeCalledWith(true); + expect(component.componentInstance.changeFilterParams).toBeCalledWith({"active": true}); + }); + + it ('should call on catalog component selectLeftSwitchItem with archive catalog' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.selectedCatalogItem = catalogSelectorItemsMock[0]; + component.componentInstance.getArchiveCatalogItems = jest.fn(); + component.componentInstance.changeFilterParams = jest.fn(); + component.componentInstance.selectLeftSwitchItem(catalogSelectorItemsMock[1]); + expect(component.componentInstance.getArchiveCatalogItems).toBeCalledWith(true); + expect(component.componentInstance.changeFilterParams).toBeCalledWith({"active": false}); + }); + + it ('should call on catalog component buildChecklistModelForTypes' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.checkboxesFilterKeys = checkboxesFilterKeysMock; + component.componentInstance.buildChecklistModelForTypes(); + expect(component.componentInstance.componentTypes).toEqual({ Resource: ['VF', 'VFC', 'CR', 'PNF', 'CP', 'VL'], + Service: null}) + expect(component.componentInstance.typesChecklistModel.checkboxes.length).toEqual(2); + }); + + it ('should call on catalog component buildChecklistModelForCategories' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.checkboxesFilterKeys = checkboxesFilterKeysMock; + component.componentInstance.categories = categoriesElements; + component.componentInstance.buildChecklistModelForCategories(); + expect(component.componentInstance.categoriesChecklistModel.checkboxes).not.toEqual(null); + }); + + it ('should call on catalog component buildChecklistModelForStatuses' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.checkboxesFilterKeys = checkboxesFilterKeysMock; + component.componentInstance.categories = categoriesElements; + component.componentInstance.confStatus = sdcMenu.statuses; + component.componentInstance.buildChecklistModelForStatuses(); + expect(component.componentInstance.statusChecklistModel.checkboxes.length).toEqual(3); + }); + + it ('should call on catalog component initCheckboxesFilter' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.initCheckboxesFilter(); + expect(component.componentInstance.checkboxesFilter.selectedComponentTypes).toEqual([]); + expect(component.componentInstance.checkboxesFilter.selectedResourceSubTypes).toEqual([]); + expect(component.componentInstance.checkboxesFilter.selectedCategoriesModel).toEqual([]); + expect(component.componentInstance.checkboxesFilter.selectedStatuses).toEqual([]); + }); + + it ('should call on catalog component initCheckboxesFilterKeys' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.initCheckboxesFilterKeys(); + expect(component.componentInstance.checkboxesFilterKeys.componentTypes).toEqual({ _main: [] }); + expect(component.componentInstance.checkboxesFilterKeys.categories).toEqual({ _main: [] }); + expect(component.componentInstance.checkboxesFilterKeys.statuses).toEqual({ _main: [] }); + }); + + it ('should call on catalog component initCategoriesMap' , () => { + const component = TestBed.createComponent(CatalogComponent); + const categoriesMap = component.componentInstance.initCategoriesMap(categoriesElements); + expect(categoriesMap["resourceNewCategory.allotted resource.allotted resource"].parent.name).toEqual("Allotted Resource"); + expect(categoriesMap["resourceNewCategory.generic"].category.uniqueId).toEqual("resourceNewCategory.generic"); + expect(categoriesMap["serviceNewCategory.voip call control"].category.name).toEqual("VoIP Call Control"); + + }); + + + it ('should call on catalog component selectLeftSwitchItem with active and selectedCatalogItem equal to archived' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.getActiveCatalogItems = jest.fn(); + component.componentInstance.changeFilterParams = jest.fn(); + component.componentInstance.selectedCatalogItem = catalogSelectorItemsMock[1] + component.componentInstance.selectLeftSwitchItem(catalogSelectorItemsMock[0]); + expect(component.componentInstance.getActiveCatalogItems).toHaveBeenCalledWith(true); + expect(component.componentInstance.changeFilterParams).toHaveBeenCalledWith({active: true}) + }); + + it ('should call on catalog component selectLeftSwitchItem with archived and selectedCatalogItem equal to active' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.getArchiveCatalogItems = jest.fn(); + component.componentInstance.changeFilterParams = jest.fn(); + component.componentInstance.selectedCatalogItem = catalogSelectorItemsMock[0] + component.componentInstance.selectLeftSwitchItem(catalogSelectorItemsMock[1]); + expect(component.componentInstance.getArchiveCatalogItems).toBeCalledWith(true); + expect(component.componentInstance.changeFilterParams).toHaveBeenCalledWith({active: false}) + }); + + it ('should call on catalog component sectionClick with section contains in expandedSection' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.expandedSection = ["type", "category", "status"]; + component.componentInstance.sectionClick("type"); + expect(component.componentInstance.expandedSection).toEqual(["category", "status"]) + }); + + it ('should call on catalog component sectionClick with section not contains in expandedSection' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.expandedSection = ["type", "category", "status"]; + component.componentInstance.sectionClick("newItem"); + expect(component.componentInstance.expandedSection).toEqual(["type", "category", "status", "newItem"]) + }); + + it ('should call on catalog component makeFilterParamsFromCheckboxes with selected values' , () => { + const component = TestBed.createComponent(CatalogComponent); + expect(component.componentInstance.makeFilterParamsFromCheckboxes(checkListModelMock)).toEqual(["Resource.VF", "Resource.VFD"]) + }); + + it ('should call on catalog component order with resourceName value' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.changeFilterParams = jest.fn(); + component.componentInstance.filterParams = filterParamsMock + component.componentInstance.order("resourceName"); + expect(component.componentInstance.changeFilterParams).toHaveBeenCalledWith( {"order": ["resourceName", false]}) + }); + + it ('should call on catalog component goToComponent' , () => { + const component = TestBed.createComponent(CatalogComponent); + const componentMock = { uniqueId: "d3e80fed-12f6-4f29-aeb1-771050e5db72", componentType: "RESOURCE"} + component.componentInstance.goToComponent(componentMock); + expect(stateServiceMock.go).toHaveBeenCalledWith('workspace.general', {id: componentMock.uniqueId, type: componentMock.componentType.toLowerCase()}) + + }); + + it ('should call on catalog component getNumOfElements for active catalog' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.selectedCatalogItem = catalogSelectorItemsMock[0] + expect(component.componentInstance.getNumOfElements(3)).toEqual("3 <b>Active</b> Elements found") + + }); + + it ('should call on catalog component raiseNumberOfElementToDisplay with empty catalogFilteredItems' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.catalogFilteredItems = [] + component.componentInstance.raiseNumberOfElementToDisplay(true); + expect(component.componentInstance.numberOfItemToDisplay).toEqual(NaN); + expect(component.componentInstance.catalogFilteredSlicedItems).toEqual([]); + }); + + it ('should call on catalog component raiseNumberOfElementToDisplay with full catalogFilteredItems' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.catalogFilteredItems = [1 , 2 , 3, 4, 5, 6] + component.componentInstance.numberOfItemToDisplay = 2; + component.componentInstance.raiseNumberOfElementToDisplay(false); + expect(component.componentInstance.numberOfItemToDisplay).toEqual(6); + expect(component.componentInstance.catalogFilteredSlicedItems).toEqual([1 , 2 , 3, 4, 5, 6]); + }); + + it ('should call on catalog component componentShouldReload return false' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.isDefaultFilter = jest.fn().mockImplementation(() => false); + cacheServiceMock.get.mockImplementation(()=> "mockConstructor"); + let componentShouldReload = component.componentInstance.componentShouldReload(); + expect(component.componentInstance.cacheService.get()).toEqual(component.componentInstance.$state.current.name); + expect(component.componentInstance.cacheService.contains()).toEqual(true); + expect(component.componentInstance.isDefaultFilter).toHaveBeenCalled(); + expect(componentShouldReload).toEqual(false); + }); + + it ('should call on catalog component componentShouldReload return true' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.isDefaultFilter = jest.fn(); + let componentShouldReload = component.componentInstance.componentShouldReload(); + expect(component.componentInstance.cacheService.get()).not.toEqual(component.componentInstance.$state.current.name); + expect(component.componentInstance.cacheService.contains()).toEqual(true); + expect(componentShouldReload).toEqual(true); + }); + + it ('should call on catalog component getActiveCatalogItems with true' , () => { + const component = TestBed.createComponent(CatalogComponent); + let resp = component.componentInstance.cacheService.get(); + component.componentInstance.updateCatalogItems = jest.fn().mockImplementation((resp) => {}); + component.componentInstance.getActiveCatalogItems(true); + expect(component.componentInstance.loaderService.activate).toHaveBeenCalled(); + expect(component.componentInstance.updateCatalogItems).toHaveBeenCalledWith(resp); + expect(component.componentInstance.loaderService.deactivate).toHaveBeenCalled(); + expect(component.componentInstance.cacheService.set).toHaveBeenCalledWith('breadcrumbsComponentsState', "mockConstructor"); + expect(component.componentInstance.cacheService.set).toHaveBeenCalledWith('breadcrumbsComponents', categoriesElements); + expect(component.componentInstance.catalogService.getCatalog).toHaveBeenCalled(); + }); + + it ('should call on catalog component getActiveCatalogItems with false' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.componentShouldReload = jest.fn(); + component.componentInstance.updateCatalogItems = jest.fn().mockImplementation((resp) => {}); + component.componentInstance.getActiveCatalogItems(false); + expect(component.componentInstance.componentShouldReload).toHaveBeenCalled(); + let resp = component.componentInstance.cacheService.get(); + expect(component.componentInstance.updateCatalogItems).toHaveBeenCalledWith(resp); + }); + + it ('should call on catalog component getActiveCatalogItems with true observable return error' , () => { + const component = TestBed.createComponent(CatalogComponent); + catalogServiceMock.getCatalog.mockImplementation(()=> Observable.throwError('error')); + component.componentInstance.getActiveCatalogItems(true); + expect(component.componentInstance.loaderService.activate).toHaveBeenCalled(); + expect(console.info).toHaveBeenCalledWith('Failed to load catalog CatalogViewModel::getActiveCatalogItems'); + expect(component.componentInstance.loaderService.deactivate).toHaveBeenCalled(); + expect(component.componentInstance.catalogService.getCatalog).toHaveBeenCalled(); + }); + + it ('should call on catalog component getArchiveCatalogItems with true' , () => { + const component = TestBed.createComponent(CatalogComponent); + const resp = component.componentInstance.cacheService.get(); + component.componentInstance.updateCatalogItems = jest.fn().mockImplementation((resp) => {}); + component.componentInstance.getArchiveCatalogItems(true); + expect(component.componentInstance.loaderService.activate).toHaveBeenCalled(); + expect(component.componentInstance.catalogService.getArchiveCatalog).toHaveBeenCalled(); + expect(component.componentInstance.cacheService.set).toHaveBeenCalledWith('archiveComponents', categoriesElements); + expect(component.componentInstance.loaderService.deactivate).toHaveBeenCalled(); + expect(component.componentInstance.updateCatalogItems).toHaveBeenCalledWith(resp) + }); + + it ('should call on catalog component getArchiveCatalogItems with false' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.updateCatalogItems = jest.fn().mockImplementation((resp) => Observable.of()); + component.componentInstance.getArchiveCatalogItems(false); + expect(component.componentInstance.cacheService.contains).toHaveBeenCalled(); + expect(component.componentInstance.cacheService.get).toHaveBeenCalled(); + let resp = component.componentInstance.cacheService.get(); + expect(component.componentInstance.updateCatalogItems).toHaveBeenCalledWith(resp); + }); + + it ('should call on catalog component getArchiveCatalogItems with true observable return error' , () => { + const component = TestBed.createComponent(CatalogComponent); + catalogServiceMock.getArchiveCatalog.mockImplementation(()=> Observable.throwError('error')); + component.componentInstance.getArchiveCatalogItems(true); + expect(component.componentInstance.loaderService.activate).toHaveBeenCalled(); + expect(component.componentInstance.catalogService.getArchiveCatalog).toHaveBeenCalled(); + expect(component.componentInstance.loaderService.deactivate).toHaveBeenCalled(); + expect(console.info).toHaveBeenCalledWith('Failed to load catalog CatalogViewModel::getArchiveCatalogItems'); + }); + + it ('should call on catalog component updateCatalogItems' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.filterCatalogItems = jest.fn(); + component.componentInstance.addFilterTermToComponent = jest.fn(); + component.componentInstance.updateCatalogItems([1, 2, 3]); + expect(component.componentInstance.catalogItems).toEqual([1, 2, 3]); + expect(component.componentInstance.addFilterTermToComponent).toHaveBeenCalled(); + expect(component.componentInstance.filterCatalogItems).toHaveBeenCalled(); + }); + + it ('should call on catalog component applyFilterParamsToView' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.initCheckboxesFilter = jest.fn(); + component.componentInstance.filterCatalogCategories = jest.fn(); + component.componentInstance.applyFilterParamsComponents = jest.fn(); + component.componentInstance.applyFilterParamsCategories = jest.fn(); + component.componentInstance.applyFilterParamsStatuses = jest.fn(); + component.componentInstance.applyFilterParamsOrder = jest.fn(); + component.componentInstance.applyFilterParamsTerm = jest.fn(); + component.componentInstance.filterCatalogItems = jest.fn(); + component.componentInstance.applyFilterParamsToView(filterParamsMock); + expect(component.componentInstance.initCheckboxesFilter).toHaveBeenCalled(); + expect(component.componentInstance.filterCatalogCategories).toHaveBeenCalled(); + expect(component.componentInstance.applyFilterParamsComponents).toHaveBeenCalledWith(filterParamsMock); + expect(component.componentInstance.applyFilterParamsCategories).toHaveBeenCalledWith(filterParamsMock); + expect(component.componentInstance.applyFilterParamsStatuses).toHaveBeenCalledWith(filterParamsMock); + expect(component.componentInstance.applyFilterParamsOrder).toHaveBeenCalledWith(filterParamsMock); + expect(component.componentInstance.applyFilterParamsTerm).toHaveBeenCalledWith(filterParamsMock); + expect(component.componentInstance.filterCatalogItems).toHaveBeenCalled(); + }); + + it ('should call on catalog component filterCatalogCategories' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.makeFilteredCategories = jest.fn(); + component.componentInstance.buildChecklistModelForCategories = jest.fn(); + component.componentInstance.categories = categoriesElements; + component.componentInstance.checkboxesFilter = {selectedComponentTypes: ["firstType", "secondType"]}; + component.componentInstance.filterCatalogCategories(); + expect(component.componentInstance.makeFilteredCategories).toHaveBeenCalledWith(categoriesElements, ["firstType", "secondType"]); + expect(component.componentInstance.buildChecklistModelForCategories).toHaveBeenCalled(); + }); + + it ('should call on catalog component filterCatalogItems' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.makeFilteredItems = jest.fn().mockImplementation(() => [1,2,3]); + component.componentInstance.raiseNumberOfElementToDisplay = jest.fn(); + component.componentInstance.catalogItems = ["firstComponent", "secondComponent"]; + component.componentInstance.checkboxesFilter = {}; + component.componentInstance.search = {}; + component.componentInstance.sortBy = ""; + component.componentInstance.reverse = true; + component.componentInstance.numberOfItemToDisplay = 2; + // component.componentInstance.catalogFilteredItems = component.componentInstance.makeFilteredItems(); + component.componentInstance.filterCatalogItems(); + expect(component.componentInstance.makeFilteredItems).toHaveBeenCalledWith(["firstComponent", "secondComponent"], {}, {}, "",true); + expect(component.componentInstance.raiseNumberOfElementToDisplay).toHaveBeenCalledWith(true); + expect(component.componentInstance.catalogFilteredSlicedItems).toEqual([1,2]); + }); + + it ('should call on catalog component applyFilterParamsToCheckboxes' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.applyFilterParamsToCheckboxes(checkListModelMock, ["Resource.CR", "Resource.VFD", "Resource.VF"]); + expect(checkListModelMock.selectedValues).toEqual(["Resource.VF","Resource.CR"]); + expect(checkListModelMock.checkboxes[1].subLevelChecklist.selectedValues).toEqual(["Resource.VFD"]); + expect(checkListModelMock.checkboxes[2].subLevelChecklist.selectedValues).toEqual(["Resource.VF"]) + }); + + it ('should call on catalog component applyFilterParamsComponents and filterParams.active equal true' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.applyFilterParamsToCheckboxes = jest.fn(); + component.componentInstance.checkboxesFilterKeys = checkboxesFilterKeysMock; + component.componentInstance.checkboxesFilter = checkboxesFilterMock; + component.componentInstance.catalogSelectorItems = catalogSelectorItemsMock; + component.componentInstance.typesChecklistModel = checkListModelMock; + component.componentInstance.applyFilterParamsComponents(filterParamsMock); + expect(component.componentInstance.applyFilterParamsToCheckboxes).toHaveBeenCalledWith(checkListModelMock, filterParamsMock.components); + expect(component.componentInstance.checkboxesFilter.selectedComponentTypes).toEqual(["Resource.VFC"]); + expect(component.componentInstance.checkboxesFilter.selectedResourceSubTypes).toEqual(["VF", "VFC"]); + expect(component.componentInstance.selectedCatalogItem).toEqual(catalogSelectorItemsMock[0]); + }); + + it ('should call on catalog component applyFilterParamsComponents and filterParams.active equal false' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.applyFilterParamsToCheckboxes = jest.fn(); + filterParamsMock.active = false; + component.componentInstance.checkboxesFilterKeys = checkboxesFilterKeysMock; + component.componentInstance.checkboxesFilter = checkboxesFilterMock; + component.componentInstance.catalogSelectorItems = catalogSelectorItemsMock; + component.componentInstance.typesChecklistModel = checkListModelMock; + component.componentInstance.applyFilterParamsComponents(filterParamsMock); + expect(component.componentInstance.applyFilterParamsToCheckboxes).toHaveBeenCalledWith(checkListModelMock, filterParamsMock.components); + expect(component.componentInstance.checkboxesFilter.selectedComponentTypes).toEqual(["Resource.VFC"]); + expect(component.componentInstance.checkboxesFilter.selectedResourceSubTypes).toEqual(["VF", "VFC"]); + expect(component.componentInstance.selectedCatalogItem).toEqual(catalogSelectorItemsMock[1]); + }); + + it ('should call on catalog component applyFilterParamsCategories' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.applyFilterParamsToCheckboxes = jest.fn(); + component.componentInstance.categoriesChecklistModel = checkListModelMock; + component.componentInstance.checkboxesFilterKeys = checkboxesFilterKeysMock; + component.componentInstance.checkboxesFilter = checkboxesFilterMock; + component.componentInstance.applyFilterParamsCategories(filterParamsMock); + expect(component.componentInstance.applyFilterParamsToCheckboxes).toHaveBeenCalledWith(checkListModelMock, filterParamsMock.categories); + expect(component.componentInstance.checkboxesFilter.selectedCategoriesModel).toEqual(["serviceNewCategory.network l4+"]); + }); + + it ('should call on catalog component applyFilterParamsStatuses' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.applyFilterParamsToCheckboxes = jest.fn(); + + component.componentInstance.statusChecklistModel = checkListModelMock; + component.componentInstance.checkboxesFilterKeys = checkboxesFilterKeysMock; + component.componentInstance.checkboxesFilter = checkboxesFilterMock; + component.componentInstance.confStatus = sdcMenu.statuses; + component.componentInstance.applyFilterParamsStatuses(filterParamsMock); + expect(component.componentInstance.applyFilterParamsToCheckboxes).toHaveBeenCalledWith(checkListModelMock, filterParamsMock.statuses); + expect(component.componentInstance.checkboxesFilter.selectedStatuses).toEqual(["NOT_CERTIFIED_CHECKOUT", "NOT_CERTIFIED_CHECKIN"]); + }); + + it ('should call on catalog component applyFilterParamsOrder' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.applyFilterParamsOrder(filterParamsMock); + expect(component.componentInstance.sortBy).toEqual("lastUpdateDate"); + expect(component.componentInstance.reverse).toEqual( true); + }); + + it ('should call on catalog component applyFilterParamsTerm' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.applyFilterParamsTerm(filterParamsMock); + expect(component.componentInstance.search.filterTerm).toEqual("Vf"); + }); + + // it ('should call on catalog component loadFilterParams' , () => { + // const component = TestBed.createComponent(CatalogComponent); + // component.componentInstance.$state = {params: {}}; + // component.componentInstance.loadFilterParams(); + // }); + + it ('should call on catalog component changeFilterParams' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.applyFilterParamsToView = jest.fn(); + component.componentInstance.filterParams = { active: true, categories: [], components: [], order: ["lastUpdateDate", true], statuses: [], term: ""}; + component.componentInstance.$state.go = jest.fn().mockImplementation(() => Promise.resolve({ json: () => [] })); + const newParams = {"filter.active": true, "filter.categories": "resourceNewCategory.allotted resource.allotted resource,resourceNewCategory.allotted resource.contrail route,resourceNewCategory.application l4+.application server", "filter.components": "Resource.VF,Resource.VFC", "filter.order": "-lastUpdateDate", "filter.statuses": "inDesign", "filter.term": "Vf"} + component.componentInstance.changeFilterParams(filterParamsMock); + expect(component.componentInstance.filterParams).toEqual(filterParamsMock); + expect(component.componentInstance.$state.go).toHaveBeenCalledWith('.',newParams, {location: 'replace', notify: false}); + expect(component.componentInstance.applyFilterParamsToView).toHaveBeenCalledWith(filterParamsMock); + }); + + it ('should call on catalog component changeFilterParams and rebuild equal true' , () => { + const component = TestBed.createComponent(CatalogComponent); + component.componentInstance.applyFilterParamsToView = jest.fn(); + component.componentInstance.makeFilterParamsFromCheckboxes = jest.fn(); + component.componentInstance.buildCheckboxLists = jest.fn(); + component.componentInstance.filterParams = { active: true, categories: [], components: [], order: ["lastUpdateDate", true], statuses: [], term: ""}; + component.componentInstance.$state.go = jest.fn().mockImplementation(() => Promise.resolve({ json: () => [] })); + const newParams = {"filter.active": true, "filter.categories": "resourceNewCategory.allotted resource.allotted resource,resourceNewCategory.allotted resource.contrail route,resourceNewCategory.application l4+.application server", "filter.components": "Resource.VF,Resource.VFC", "filter.order": "-lastUpdateDate", "filter.statuses": "inDesign", "filter.term": "Vf"} + component.componentInstance.typesChecklistModel = checkListModelMock; + component.componentInstance.categoriesChecklistModel = checkListModelMock; + component.componentInstance.statusChecklistModel = checkListModelMock; + component.componentInstance.changeFilterParams(filterParamsMock, true); + expect(component.componentInstance.filterParams).toEqual(filterParamsMock); + expect(component.componentInstance.$state.go).toHaveBeenCalledWith('.',newParams, {location: 'replace', notify: false}); + //expect(component.componentInstance.makeFilterParamsFromCheckboxes).toHaveBeenCalledWith(component.componentInstance.typesChecklistModel); + //expect(component.componentInstance.buildCheckboxLists).toHaveBeenCalled(); + expect(component.componentInstance.applyFilterParamsToView).toHaveBeenCalledWith(filterParamsMock); + }); + + it ('should call on catalog component makeFilteredCategories' , () => { + const component = TestBed.createComponent(CatalogComponent); + const categoryMock = [{"name":"Network L1-3","normalizedName":"network l1-3","uniqueId":"serviceNewCategory.network l1-3","icons":["network_l_1-3"],"subcategories":null,"version":null,"ownerId":null,"empty":false,"type":null}]; + cacheServiceMock.get.mockImplementation(()=> categoryMock); + const resp = component.componentInstance.makeFilteredCategories(categoriesElements, checkboxesFilterMock.selectedComponentTypes); + expect(component.componentInstance.cacheService.get).toHaveBeenCalledWith("resourceCategories"); + expect(resp).toEqual(categoryMock); + }); + + it ('should call on catalog component makeFilteredCategories return unique elements' , () => { + const component = TestBed.createComponent(CatalogComponent); + const categoryMock = [{"name":"Network L1-3","normalizedName":"network l1-3","uniqueId":"serviceNewCategory.network l1-3","icons":["network_l_1-3"],"subcategories":null,"version":null,"ownerId":null,"empty":false,"type":null}, + {"name":"Network L1-3","normalizedName":"network l1-3","uniqueId":"serviceNewCategory.network l1-3","icons":["network_l_1-3"],"subcategories":null,"version":null,"ownerId":null,"empty":false,"type":null}, + {"name":"Network Service","normalizedName":"network service","uniqueId":"serviceNewCategory.network service","icons":["network_l_1-3"],"subcategories":null,"version":null,"ownerId":null,"empty":false,"type":null}]; + const categoryUniqueMock = [{"name":"Network L1-3","normalizedName":"network l1-3","uniqueId":"serviceNewCategory.network l1-3","icons":["network_l_1-3"],"subcategories":null,"version":null,"ownerId":null,"empty":false,"type":null}, + {"name":"Network Service","normalizedName":"network service","uniqueId":"serviceNewCategory.network service","icons":["network_l_1-3"],"subcategories":null,"version":null,"ownerId":null,"empty":false,"type":null}]; + cacheServiceMock.get.mockImplementation(()=> categoryMock); + checkboxesFilterMock.selectedComponentTypes = ["SERVICE", "Resource.VF"]; + const resp = component.componentInstance.makeFilteredCategories(categoriesElements, checkboxesFilterMock.selectedComponentTypes); + expect(component.componentInstance.cacheService.get).toHaveBeenCalledWith("resourceCategories"); + expect(resp).toEqual(categoryUniqueMock); + }); + + +}); diff --git a/catalog-ui/src/app/ng2/pages/catalog/catalog.component.ts b/catalog-ui/src/app/ng2/pages/catalog/catalog.component.ts new file mode 100644 index 0000000000..527764862a --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/catalog/catalog.component.ts @@ -0,0 +1,666 @@ +/*- + * ============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========================================================= + */ + +import * as _ from "lodash"; +import { Component as NgComponent, Inject } from '@angular/core'; +import { SdcUiCommon, SdcUiServices } from "onap-ui-angular"; +import { CacheService, CatalogService } from "app/services-ng2"; +import { SdcConfigToken, ISdcConfig } from "../../config/sdc-config.config"; +import { SdcMenuToken, IAppMenu } from "../../config/sdc-menu.config"; +import { Component, ICategoryBase, IMainCategory, ISubCategory, IConfigStatuses, ICatalogSelector, CatalogSelectorTypes } from "app/models"; +import { ResourceNamePipe } from "../../pipes/resource-name.pipe"; +import { EntityFilterPipe, IEntityFilterObject, ISearchFilter} from "../../pipes/entity-filter.pipe"; + +interface Gui { + onComponentSubTypesClick:Function; + onComponentTypeClick:Function; + onCategoryClick:Function; + onStatusClick:Function; + changeFilterTerm:Function; +} + +interface IFilterParams { + components: string[]; + categories: string[]; + statuses: (string)[]; + order: [string, boolean]; + term: string; + active: boolean; +} + +interface ICheckboxesFilterMap { + [key: string]: Array<string>; + _main: Array<string>; +} + +interface ICheckboxesFilterKeys { + componentTypes: ICheckboxesFilterMap; + categories: ICheckboxesFilterMap; + statuses: ICheckboxesFilterMap; +} + +interface ICategoriesMap { + [key: string]: { + category: ICategoryBase, + parent: ICategoryBase + } +} + +@NgComponent({ + selector: 'catalog', + templateUrl: './catalog.component.html', + styleUrls:['./catalog.component.less'] +}) +export class CatalogComponent { + public checkboxesFilter:IEntityFilterObject; + public checkboxesFilterKeys:ICheckboxesFilterKeys; + public gui:Gui; + public categories:Array<IMainCategory>; + public filteredCategories:Array<IMainCategory>; + public confStatus:IConfigStatuses; + public componentTypes:{[key:string]: Array<string>}; + public catalogItems:Array<Component>; + public catalogFilteredItems:Array<Component>; + public catalogFilteredSlicedItems:Array<Component>; + public expandedSection:Array<string>; + public version:string; + public sortBy:string; + public reverse:boolean; + public filterParams:IFilterParams; + public search:ISearchFilter; + + //this is for UI paging + public numberOfItemToDisplay:number; + + public selectedCatalogItem: ICatalogSelector; + public catalogSelectorItems: Array<ICatalogSelector>; + public showCatalogSelector: boolean; + + public typesChecklistModel: SdcUiCommon.ChecklistModel; + public categoriesChecklistModel: SdcUiCommon.ChecklistModel; + public statusChecklistModel: SdcUiCommon.ChecklistModel; + + private defaultFilterParams:IFilterParams = { + components: [], + categories: [], + statuses: [], + order: ['lastUpdateDate', true], + term: '', + active: true + }; + private categoriesMap:ICategoriesMap; + + constructor( + @Inject(SdcConfigToken) private sdcConfig:ISdcConfig, + @Inject(SdcMenuToken) public sdcMenu:IAppMenu, + @Inject("$state") private $state:ng.ui.IStateService, + private cacheService:CacheService, + private catalogService:CatalogService, + private resourceNamePipe:ResourceNamePipe, + private loaderService: SdcUiServices.LoaderService + ) {} + + ngOnInit(): void { + this.initGui(); + this.initLeftSwitch(); + this.initScopeMembers(); + this.loadFilterParams(); + this.initCatalogData(); // Async task to get catalog from server. + } + + private initLeftSwitch = ():void => { + this.showCatalogSelector = false; + + this.catalogSelectorItems = [ + {value: CatalogSelectorTypes.Active, title: "Active Items", header: "Active"}, + {value: CatalogSelectorTypes.Archive, title: "Archive", header: "Archived"} + ]; + // set active items is default + this.selectedCatalogItem = this.catalogSelectorItems[0]; + }; + + private initCatalogData = ():void => { + if(this.selectedCatalogItem.value === CatalogSelectorTypes.Archive){ + this.getArchiveCatalogItems(); + } else { + this.getActiveCatalogItems(); + } + }; + + + private initScopeMembers = ():void => { + this.numberOfItemToDisplay = 0; + this.categories = this.makeSortedCategories(this.cacheService.get('serviceCategories').concat(this.cacheService.get('resourceCategories'))) + .map((cat) => <IMainCategory>cat); + this.confStatus = this.sdcMenu.statuses; + this.expandedSection = ["type", "category", "status"]; + this.catalogItems = []; + this.search = {FilterTerm: ""}; + this.categoriesMap = this.initCategoriesMap(); + this.initCheckboxesFilter(); + this.initCheckboxesFilterKeys(); + this.buildCheckboxLists(); + + this.version = this.cacheService.get('version'); + this.sortBy = 'lastUpdateDate'; + this.reverse = true; + }; + + private buildCheckboxLists() { + this.buildChecklistModelForTypes(); + this.buildChecklistModelForCategories(); + this.buildChecklistModelForStatuses(); + } + + private getTestIdForCheckboxByText = ( text: string ):string => { + return 'checkbox-' + text.toLowerCase().replace(/ /g, ''); + } + + private buildChecklistModelForTypes() { + this.componentTypes = { + Resource: ['VF', 'VFC', 'CR', 'PNF', 'CP', 'VL'], + Service: null + }; + this.typesChecklistModel = new SdcUiCommon.ChecklistModel(this.checkboxesFilterKeys.componentTypes._main, + Object.keys(this.componentTypes).map((ct) => { + let subChecklist = null; + if (this.componentTypes[ct]) { + this.checkboxesFilterKeys.componentTypes[ct] = this.checkboxesFilterKeys.componentTypes[ct] || []; + subChecklist = new SdcUiCommon.ChecklistModel(this.checkboxesFilterKeys.componentTypes[ct], + this.componentTypes[ct].map((st) => { + const stKey = [ct, st].join('.'); + const testId = this.getTestIdForCheckboxByText(st); + return new SdcUiCommon.ChecklistItemModel(st, false, this.checkboxesFilterKeys.componentTypes[ct].indexOf(stKey) !== -1, null, testId, stKey); + }) + ); + } + const testId = this.getTestIdForCheckboxByText(ct); + return new SdcUiCommon.ChecklistItemModel(ct, false, this.checkboxesFilterKeys.componentTypes._main.indexOf(ct) !== -1, subChecklist, testId, ct); + }) + ); + } + + private buildChecklistModelForCategories() { + this.categoriesChecklistModel = new SdcUiCommon.ChecklistModel(this.checkboxesFilterKeys.categories._main, + (this.filteredCategories || this.categories).map((cat) => { + this.checkboxesFilterKeys.categories[cat.uniqueId] = this.checkboxesFilterKeys.categories[cat.uniqueId] || []; + const subCategoriesChecklistModel = new SdcUiCommon.ChecklistModel(this.checkboxesFilterKeys.categories[cat.uniqueId], + (cat.subcategories || []).map((scat) => { + this.checkboxesFilterKeys.categories[scat.uniqueId] = this.checkboxesFilterKeys.categories[scat.uniqueId] || []; + const groupingsChecklistModel = new SdcUiCommon.ChecklistModel(this.checkboxesFilterKeys.categories[scat.uniqueId], + (scat.groupings || []).map(gcat => + new SdcUiCommon.ChecklistItemModel(gcat.name, false, this.checkboxesFilterKeys.categories[scat.uniqueId].indexOf(gcat.uniqueId) !== -1, null, this.getTestIdForCheckboxByText(gcat.uniqueId), gcat.uniqueId)) + ); + return new SdcUiCommon.ChecklistItemModel(scat.name, false, this.checkboxesFilterKeys.categories[cat.uniqueId].indexOf(scat.uniqueId) !== -1, groupingsChecklistModel, this.getTestIdForCheckboxByText(scat.uniqueId), scat.uniqueId); + }) + ); + return new SdcUiCommon.ChecklistItemModel(cat.name, false, this.checkboxesFilterKeys.categories._main.indexOf(cat.uniqueId) !== -1, subCategoriesChecklistModel, this.getTestIdForCheckboxByText(cat.uniqueId), cat.uniqueId); + }) + ); + } + + private buildChecklistModelForStatuses() { + // For statuses checklist model, use the statuses keys as values. On applying filtering map the statuses keys to statuses values. + this.statusChecklistModel = new SdcUiCommon.ChecklistModel(this.checkboxesFilterKeys.statuses._main, + Object.keys(this.confStatus).map((sKey) => new SdcUiCommon.ChecklistItemModel( + this.confStatus[sKey].name, + false, + this.checkboxesFilterKeys.statuses._main.indexOf(sKey) !== -1, + null, + this.getTestIdForCheckboxByText(sKey), + sKey)) + ); + } + + private initCheckboxesFilter() { + // Checkboxes filter init + this.checkboxesFilter = <IEntityFilterObject>{}; + this.checkboxesFilter.selectedComponentTypes = []; + this.checkboxesFilter.selectedResourceSubTypes = []; + this.checkboxesFilter.selectedCategoriesModel = []; + this.checkboxesFilter.selectedStatuses = []; + } + + private initCheckboxesFilterKeys() { + // init checkboxes filter keys (for checklists values): + this.checkboxesFilterKeys = <ICheckboxesFilterKeys>{}; + this.checkboxesFilterKeys.componentTypes = { _main: [] }; + this.checkboxesFilterKeys.categories = { _main: [] }; + this.checkboxesFilterKeys.statuses = { _main: [] }; + } + + private initCategoriesMap(categoriesList?:(ICategoryBase)[], parentCategory:ICategoryBase=null): ICategoriesMap { + categoriesList = (categoriesList) ? categoriesList : this.categories; + + // Init categories map + return categoriesList.reduce((acc, cat) => { + acc[cat.uniqueId] = { + category: cat, + parent: parentCategory + }; + const catChildren = ((<IMainCategory>cat).subcategories) + ? (<IMainCategory>cat).subcategories + : (((<ISubCategory>cat).groupings) + ? (<ISubCategory>cat).groupings + : null); + if (catChildren) { + Object.assign(acc, this.initCategoriesMap(catChildren, cat)); + } + return acc; + }, <ICategoriesMap>{}); + } + + public selectLeftSwitchItem(item: ICatalogSelector): void { + if (this.selectedCatalogItem.value !== item.value) { + this.selectedCatalogItem = item; + switch (item.value) { + case CatalogSelectorTypes.Active: + this.getActiveCatalogItems(true); + break; + + case CatalogSelectorTypes.Archive: + this.getArchiveCatalogItems(true); + break; + } + this.changeFilterParams({active: (item.value === CatalogSelectorTypes.Active)}); + } + } + + public sectionClick(section: string): void { + let index: number = this.expandedSection.indexOf(section); + if (index !== -1) { + this.expandedSection.splice(index, 1); + } else { + this.expandedSection.push(section); + } + } + + + private makeFilterParamsFromCheckboxes(checklistModel:SdcUiCommon.ChecklistModel): Array<string> { + return checklistModel.checkboxes.reduce((acc, chbox) => { + if (checklistModel.selectedValues.indexOf(chbox.value) !== -1) { + acc.push(chbox.value); + } else if (chbox.subLevelChecklist) { // else, if checkbox is not checked, then try to get values from sub checklists + acc.push(...this.makeFilterParamsFromCheckboxes(chbox.subLevelChecklist)); + } + return acc; + }, []); + } + + //default sort by descending last update. default for alphabetical = ascending + public order(sortBy: string): void { + this.changeFilterParams({ + order: (this.filterParams.order[0] === sortBy) + ? [sortBy, !this.filterParams.order[1]] + : [sortBy, sortBy === 'lastUpdateDate'] + }); + } + + + public goToComponent(component: Component): void { + this.$state.go('workspace.general', {id: component.uniqueId, type: component.componentType.toLowerCase()}); + } + + + // Will print the number of elements found in catalog + public getNumOfElements(num:number):string { + if (!num || num === 0) { + return `No <b>${this.selectedCatalogItem.header}</b> Elements found`; + } else if (num === 1) { + return `1 <b>${this.selectedCatalogItem.header}</b> Element found`; + } else { + return num + ` <b>${this.selectedCatalogItem.header}</b> Elements found`; + } + } + + public initGui(): void { + this.gui = <Gui>{}; + + /** + * Select | unselect sub resource when resource is clicked | unclicked. + * @param type + */ + this.gui.onComponentTypeClick = (): void => { + this.changeFilterParams({ + components: this.makeFilterParamsFromCheckboxes(this.typesChecklistModel) + }); + }; + + this.gui.onCategoryClick = (): void => { + this.changeFilterParams({ + categories: this.makeFilterParamsFromCheckboxes(this.categoriesChecklistModel) + }); + }; + + this.gui.onStatusClick = (statusChecklistItem: SdcUiCommon.ChecklistItemModel) => { + this.changeFilterParams({ + statuses: this.makeFilterParamsFromCheckboxes(this.statusChecklistModel) + }); + }; + + this.gui.changeFilterTerm = (filterTerm: string) => { + this.changeFilterParams({ + term: filterTerm + }); + }; + } + + public raiseNumberOfElementToDisplay(recalculate:boolean = false): void { + const scrollPageAmount = 35; + if (!this.catalogFilteredItems) { + this.numberOfItemToDisplay = 0; + } else if (this.catalogFilteredItems.length > this.numberOfItemToDisplay || recalculate) { + let fullPagesAmount = Math.ceil(this.numberOfItemToDisplay / scrollPageAmount) * scrollPageAmount; + if (!recalculate || fullPagesAmount === 0) { //TODO trigger infiniteScroll to check bottom and fire onBottomHit by itself (sdc-ui) + fullPagesAmount += scrollPageAmount; + } + this.numberOfItemToDisplay = Math.min(this.catalogFilteredItems.length, fullPagesAmount); + this.catalogFilteredSlicedItems = this.catalogFilteredItems.slice(0, this.numberOfItemToDisplay); + } + } + + private isDefaultFilter = (): boolean => { + return angular.equals(this.defaultFilterParams, this.filterParams); + } + + private componentShouldReload = ():boolean => { + let breadcrumbsValid: boolean = (this.$state.current.name === this.cacheService.get('breadcrumbsComponentsState') && this.cacheService.contains('breadcrumbsComponents')); + return !breadcrumbsValid || this.isDefaultFilter(); + } + + private getActiveCatalogItems(forceReload?: boolean): void { + if (forceReload || this.componentShouldReload()) { + this.loaderService.activate(); + + let onSuccess = (followedResponse:Array<Component>):void => { + this.updateCatalogItems(followedResponse); + this.loaderService.deactivate(); + this.cacheService.set('breadcrumbsComponentsState', this.$state.current.name); //catalog + this.cacheService.set('breadcrumbsComponents', followedResponse); + + }; + + let onError = ():void => { + console.info('Failed to load catalog CatalogViewModel::getActiveCatalogItems'); + this.loaderService.deactivate(); + }; + this.catalogService.getCatalog().subscribe(onSuccess, onError); + } else { + let cachedComponents = this.cacheService.get('breadcrumbsComponents'); + this.updateCatalogItems(cachedComponents); + } + } + + private getArchiveCatalogItems(forceReload?: boolean): void { + if(forceReload || !this.cacheService.contains("archiveComponents")) { + this.loaderService.activate(); + let onSuccess = (followedResponse:Array<Component>):void => { + this.cacheService.set("archiveComponents", followedResponse); + this.loaderService.deactivate(); + this.updateCatalogItems(followedResponse); + }; + + let onError = ():void => { + console.info('Failed to load catalog CatalogViewModel::getArchiveCatalogItems'); + this.loaderService.deactivate(); + }; + + this.catalogService.getArchiveCatalog().subscribe(onSuccess, onError); + } else { + let archiveCache = this.cacheService.get("archiveComponents"); + this.updateCatalogItems(archiveCache); + } + } + + private updateCatalogItems = (items:Array<Component>):void => { + this.catalogItems = items; + this.catalogItems.forEach(this.addFilterTermToComponent); + this.filterCatalogItems(); + } + + private applyFilterParamsToView(filterParams:IFilterParams) { + // reset checkboxes filter + this.initCheckboxesFilter(); + + this.filterCatalogCategories(); + + this.applyFilterParamsComponents(filterParams); + this.applyFilterParamsCategories(filterParams); + this.applyFilterParamsStatuses(filterParams); + this.applyFilterParamsOrder(filterParams); + this.applyFilterParamsTerm(filterParams); + + // do filters when filter params are changed: + this.filterCatalogItems(); + } + + private filterCatalogCategories() { + this.filteredCategories = this.makeFilteredCategories(this.categories, this.checkboxesFilter.selectedComponentTypes); + this.buildChecklistModelForCategories(); + } + + private filterCatalogItems() { + this.catalogFilteredItems = this.makeFilteredItems(this.catalogItems, this.checkboxesFilter, this.search, this.sortBy, this.reverse); + this.raiseNumberOfElementToDisplay(true); + this.catalogFilteredSlicedItems = this.catalogFilteredItems.slice(0, this.numberOfItemToDisplay); + } + + private applyFilterParamsToCheckboxes(checklistModel:SdcUiCommon.ChecklistModel, filterParamsList:Array<string>) { + checklistModel.checkboxes.forEach((chbox) => { + // if checkbox is checked, then add it to selected values if not there, and select all sub checkboxes + if (filterParamsList.indexOf(chbox.value) !== -1 && checklistModel.selectedValues.indexOf(chbox.value) === -1) { + checklistModel.selectedValues.push(chbox.value); + if (chbox.subLevelChecklist) { + this.applyFilterParamsToCheckboxes(chbox.subLevelChecklist, chbox.subLevelChecklist.checkboxes.map((subchbox) => subchbox.value)); + } + } else if ( chbox.subLevelChecklist ) { + this.applyFilterParamsToCheckboxes(chbox.subLevelChecklist, filterParamsList); + } + }); + } + + private applyFilterParamsComponents(filterParams:IFilterParams) { + this.applyFilterParamsToCheckboxes(this.typesChecklistModel, filterParams.components); + this.checkboxesFilter.selectedComponentTypes = this.checkboxesFilterKeys.componentTypes._main; + Object.keys(this.checkboxesFilterKeys.componentTypes).forEach((chKey) => { + if (chKey !== '_main') { + this.checkboxesFilter['selected' + chKey + 'SubTypes'] = this.checkboxesFilterKeys.componentTypes[chKey].map((st) => st.substr(chKey.length + 1)); + } + }); + + let selectedCatalogIndex = filterParams.active ? CatalogSelectorTypes.Active : CatalogSelectorTypes.Archive; + this.selectedCatalogItem = this.catalogSelectorItems[selectedCatalogIndex]; + } + + private applyFilterParamsCategories(filterParams:IFilterParams) { + this.applyFilterParamsToCheckboxes(this.categoriesChecklistModel, filterParams.categories); + this.checkboxesFilter.selectedCategoriesModel = <Array<string>>_.flatMap(this.checkboxesFilterKeys.categories); + } + + private applyFilterParamsStatuses(filterParams: IFilterParams) { + this.applyFilterParamsToCheckboxes(this.statusChecklistModel, filterParams.statuses); + this.checkboxesFilter.selectedStatuses = _.reduce(_.flatMap(this.checkboxesFilterKeys.statuses), (stats, st:string) => [...stats, ...this.confStatus[st].values], []); + } + + private applyFilterParamsOrder(filterParams: IFilterParams) { + this.sortBy = filterParams.order[0]; + this.reverse = filterParams.order[1]; + } + + private applyFilterParamsTerm(filterParams: IFilterParams) { + this.search = { + filterTerm: filterParams.term + }; + } + + private loadFilterParams() { + const params = this.$state.params; + this.filterParams = angular.copy(this.defaultFilterParams); + Object.keys(params).forEach((k) => { + if (!angular.isUndefined(params[k])) { + let newVal; + let paramsChecklist: SdcUiCommon.ChecklistModel = null; + let filterKey = k.substr('filter.'.length); + switch (k) { + case 'filter.components': + paramsChecklist = paramsChecklist || this.typesChecklistModel; + case 'filter.categories': + paramsChecklist = paramsChecklist || this.categoriesChecklistModel; + case 'filter.statuses': + paramsChecklist = paramsChecklist || this.statusChecklistModel; + + // for those cases above - split param by comma and make reduced checklist values for filter params (url) + newVal = _.uniq(params[k].split(',')); + break; + case 'filter.order': + newVal = params[k].startsWith('-') ? [params[k].substr(1), true] : [params[k], false]; + break; + case 'filter.term': + newVal = params[k]; + break; + case 'filter.active': + newVal = (params[k] === "true" || params[k] === true)? true : false; + break; + default: + // unknown filter key + filterKey = null; + } + if (filterKey) { + this.filterParams[filterKey] = newVal; + } + } + }); + // re-set filter params with valid values, and then re-build checklists + this.changeFilterParams(this.filterParams, true); + } + + private changeFilterParams(changedFilterParams, rebuild:boolean = false) { + const newParams = {}; + Object.keys(changedFilterParams).forEach((k) => { + let newVal; + switch (k) { + case 'components': + case 'categories': + case 'statuses': + newVal = changedFilterParams[k] && changedFilterParams[k].length ? changedFilterParams[k].join(',') : null; + break; + case 'order': + newVal = (changedFilterParams[k][1] ? '-' : '') + changedFilterParams[k][0]; + break; + case 'term': + newVal = changedFilterParams[k] ? changedFilterParams[k] : null; + break; + case 'active': + newVal = (changedFilterParams[k] === "true" || changedFilterParams[k] === true); + break; + default: + return; + } + this.filterParams[k] = changedFilterParams[k]; + newParams['filter.' + k] = newVal; + }); + this.$state.go('.', newParams, {location: 'replace', notify: false}).then(() => { + if (rebuild) { + // fix the filter params to only valid values for checkboxes + this.changeFilterParams({ + components: this.makeFilterParamsFromCheckboxes(this.typesChecklistModel), + categories: this.makeFilterParamsFromCheckboxes(this.categoriesChecklistModel), + statuses: this.makeFilterParamsFromCheckboxes(this.statusChecklistModel) + }); + // rebuild the checkboxes to show selected + this.buildCheckboxLists(); + } + }); + this.applyFilterParamsToView(this.filterParams); + } + + private makeFilteredCategories(categories:Array<IMainCategory>, selectedTypes:Array<string>=[]): Array<IMainCategory> { + let filteredCategories = categories.slice(); + + const filteredMainTypes = selectedTypes.reduce((acc, st) => { + const mainType = st.split('.')[0]; + if (acc.indexOf(mainType) === -1) { + acc.push(mainType); + } + return acc; + }, []); + + // filter by selected types + if (filteredMainTypes.length) { + const filteredTypesCategories = filteredMainTypes.reduce((acc, mainType: string) => { + acc.push(...this.cacheService.get(mainType.toLowerCase() + 'Categories')); + return acc; + }, []); + + filteredCategories = _.intersectionBy(filteredCategories, filteredTypesCategories, c => c.uniqueId); + } + + return filteredCategories; + } + + private makeSortedCategories(categories:Array<IMainCategory|ISubCategory|ICategoryBase>, sortBy?:any): Array<IMainCategory|ISubCategory|ICategoryBase> { + sortBy = (sortBy !== undefined) ? sortBy : ['name']; + let sortedCategories = categories.map(cat => Object.assign({}, cat)); // copy each object in the array + sortedCategories = _.sortBy(sortedCategories, sortBy); + + // inner sort of subcategories and groupings + sortedCategories.forEach((cat) => { + if ('subcategories' in cat && cat['subcategories'] && cat['subcategories'].length > 0) { + cat['subcategories'] = this.makeSortedCategories(cat['subcategories'], sortBy); + } + if ('groupings' in cat && cat['groupings'] && cat['groupings'].length > 0) { + cat['groupings'] = this.makeSortedCategories(cat['groupings'], sortBy); + } + }); + + return sortedCategories; + } + + private addFilterTermToComponent(component:Component) { + component.filterTerm = component.name + ' ' + component.description + ' ' + component.tags.toString() + ' ' + component.version; + component.filterTerm = component.filterTerm.toLowerCase(); + } + + private makeFilteredItems(catalogItems:Array<Component>, filter:IEntityFilterObject, search:ISearchFilter, sortBy:string, reverse:boolean) { + let filteredComponents:Array<Component> = catalogItems; + + // common entity filter + // -------------------------------------------------------------------------- + filter = Object.assign({ search }, filter); // add search to entity filter object + filteredComponents = EntityFilterPipe.transform(filteredComponents, filter); + + // sort + // -------------------------------------------------------------------------- + if (sortBy) { + switch (sortBy) { + case 'resourceName': + filteredComponents = _.sortBy(filteredComponents, cat => this.resourceNamePipe.transform(cat.name)); + break; + default: + filteredComponents = _.sortBy(filteredComponents, [sortBy]); + } + if (reverse) { + _.reverse(filteredComponents); + } + } + + return filteredComponents; + } +} diff --git a/catalog-ui/src/app/ng2/pages/catalog/catalog.module.ts b/catalog-ui/src/app/ng2/pages/catalog/catalog.module.ts new file mode 100644 index 0000000000..5ef8de01e3 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/catalog/catalog.module.ts @@ -0,0 +1,33 @@ +import { NgModule } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { CatalogComponent } from "./catalog.component"; +import { LayoutModule } from "../../components/layout/layout.module"; +import { UiElementsModule } from "../../components/ui/ui-elements.module"; +import { GlobalPipesModule } from "../../pipes/global-pipes.module"; +import { TranslateModule } from "../../shared/translator/translate.module"; +import { SdcUiComponentsModule } from "onap-ui-angular"; +import {SdcTileModule} from "../../components/ui/tile/sdc-tile.module"; + +@NgModule({ + declarations: [ + CatalogComponent + ], + imports: [ + CommonModule, + SdcUiComponentsModule, + LayoutModule, + UiElementsModule, + GlobalPipesModule, + TranslateModule, + SdcTileModule + ], + exports: [ + CatalogComponent + ], + entryComponents: [ + CatalogComponent + ], + providers: [] +}) +export class CatalogModule { +} diff --git a/catalog-ui/src/app/ng2/pages/composition/common/common-graph-data.service.ts b/catalog-ui/src/app/ng2/pages/composition/common/common-graph-data.service.ts new file mode 100644 index 0000000000..d4caa5e9ed --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/common/common-graph-data.service.ts @@ -0,0 +1,64 @@ +import {Injectable} from "@angular/core"; +import 'rxjs/add/observable/forkJoin'; +import {ComponentInstance} from "../../../../models/componentsInstances/componentInstance"; +import {SelectedComponentType} from "./store/graph.actions"; +import {RelationshipModel} from "../../../../models/graph/relationship"; + +@Injectable() +export class CommonGraphDataService { + + public componentInstances: Array<ComponentInstance>; + public componentInstancesRelations: RelationshipModel[]; + public selectedComponentType: SelectedComponentType; + + constructor() { + } + + //------------------------ RELATIONS ---------------------------------// + public setRelations = (componentInstancesRelations: RelationshipModel[]) => { + this.componentInstancesRelations = this.componentInstancesRelations; + } + + public getRelations = (): RelationshipModel[] => { + return this.componentInstancesRelations; + } + + public addRelation = (componentInstancesRelations: RelationshipModel) => { + this.componentInstancesRelations.push(componentInstancesRelations); + } + + public deleteRelation(relationToDelete: RelationshipModel) { + this.componentInstancesRelations = _.filter(this.componentInstancesRelations, (relationship: RelationshipModel) => { + return relationship.relationships[0].relation.id !== relationToDelete.relationships[0].relation.id; + }); + } + + //---------------------------- COMPONENT INSTANCES ------------------------------------// + public getComponentInstances = (): Array<ComponentInstance> => { + return this.componentInstances; + } + + public addComponentInstance = (instance: ComponentInstance) => { + return this.componentInstances.push(instance); + } + + public updateComponentInstances = (componentInstances: ComponentInstance[]) => { + _.unionBy(this.componentInstances, componentInstances, 'uniqueId'); + } + + public updateInstance = (instance: ComponentInstance) => { + this.componentInstances = this.componentInstances.map(componentInstance => instance.uniqueId === componentInstance.uniqueId? instance : componentInstance); + } + + public deleteComponentInstance(instanceToDelete: string) { + this.componentInstances = _.filter(this.componentInstances, (instance: ComponentInstance) => { + return instance.uniqueId !== instanceToDelete; + }); + } + + //----------------------------SELECTED COMPONENT -----------------------// + + public setSelectedComponentType = (selectedType: SelectedComponentType) => { + this.selectedComponentType = selectedType; + } +} diff --git a/catalog-ui/src/app/ng2/pages/composition/common/store/graph.actions.ts b/catalog-ui/src/app/ng2/pages/composition/common/store/graph.actions.ts new file mode 100644 index 0000000000..9bd5d0db62 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/common/store/graph.actions.ts @@ -0,0 +1,33 @@ +export enum SelectedComponentType { + COMPONENT_INSTANCE = "COMPONENT_INSTANCE", + GROUP = "GROUP", + POLICY = "POLICY", + TOPOLOGY_TEMPLATE = "TOPOLOGY_TEMPLATE" +} + +export class UpdateSelectedComponentAction { + static readonly type = '[COMPOSITION] UpdateSelectedComponent'; + + constructor(public payload: {uniqueId?: string, type?: string}) { + } +} + +export class SetSelectedComponentAction { + static readonly type = '[COMPOSITION] SetSelectedComponent'; + + constructor(public payload: {component?: any, type?: SelectedComponentType}) { + } +} + +export class OnSidebarOpenOrCloseAction { + static readonly type = '[COMPOSITION] OnSidebarOpenOrCloseAction'; + + constructor() { + } +} + +export class TogglePanelLoadingAction { + static readonly type = '[COMPOSITION] TogglePanelLoading'; + constructor(public payload: { isLoading: boolean}) { + } +} diff --git a/catalog-ui/src/app/ng2/pages/composition/common/store/graph.state.ts b/catalog-ui/src/app/ng2/pages/composition/common/store/graph.state.ts new file mode 100644 index 0000000000..d58bb446df --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/common/store/graph.state.ts @@ -0,0 +1,170 @@ +import { Action, Selector, State, StateContext} from '@ngxs/store'; +import { + OnSidebarOpenOrCloseAction, + SelectedComponentType, + SetSelectedComponentAction, + TogglePanelLoadingAction +} from "./graph.actions"; +import { PolicyInstance, GroupInstance, Component as TopologyTemplate, ComponentInstance, LeftPaletteComponent, FullComponentInstance} from "app/models"; +import { TopologyTemplateService } from "app/ng2/services/component-services/topology-template.service"; +import { tap } from "rxjs/operators"; +import { CompositionService } from "app/ng2/pages/composition/composition.service"; +import {GroupsService} from "../../../../services/groups.service"; +import {PoliciesService} from "../../../../services/policies.service"; +import {WorkspaceService} from "../../../workspace/workspace.service"; + +export class CompositionStateModel { + + isViewOnly?: boolean; + panelLoading?: boolean; + selectedComponentType?: SelectedComponentType; + selectedComponent?: PolicyInstance | GroupInstance | TopologyTemplate | ComponentInstance; + withSidebar?: boolean; +} + +@State<CompositionStateModel>({ + name: 'composition', + defaults: { + withSidebar: true + } +}) +export class GraphState { + + constructor(private topologyTemplateService: TopologyTemplateService, + private compositionService: CompositionService, + private policiesService:PoliciesService, private groupsService:GroupsService, + private workspaceService: WorkspaceService) {} + + @Action(SetSelectedComponentAction) + setSelectedComponent({dispatch, getState, patchState}:StateContext<CompositionStateModel>, action: SetSelectedComponentAction) { + + const state:CompositionStateModel = getState(); + + patchState({ panelLoading: true }); + + if(action.payload.component instanceof ComponentInstance){ + let originComponent = this.compositionService.getOriginComponentById(action.payload.component.getComponentUid()); + if(!originComponent) { + return this.topologyTemplateService.getFullComponent(action.payload.component.originType, action.payload.component.getComponentUid()) + .pipe(tap(resp => { + this.compositionService.addOriginComponent(resp); + this.compositionService.setSelectedComponentType(SelectedComponentType.COMPONENT_INSTANCE); + patchState({ + selectedComponent: new FullComponentInstance(action.payload.component, resp), + selectedComponentType: action.payload.type, + panelLoading: false + }); + }, err => { + patchState({ + panelLoading: false + }) + } + )); + } else { + patchState({ + selectedComponent: new FullComponentInstance(action.payload.component, originComponent), + selectedComponentType: action.payload.type, + panelLoading: false + }); + } + } else if (action.payload.component instanceof PolicyInstance) { + let topologyTemplate = this.workspaceService.metadata; + return this.policiesService.getSpecificPolicy(topologyTemplate.componentType, topologyTemplate.uniqueId, action.payload.component.uniqueId).pipe(tap(resp => + { + this.compositionService.updatePolicy(resp); + patchState({ + selectedComponent: resp, + selectedComponentType: action.payload.type, + panelLoading: false + }) + }, err => { + patchState({ + panelLoading: false + }) + } + )); + + } else if (action.payload.component instanceof GroupInstance) { + let topologyTemplate = this.workspaceService.metadata; + return this.groupsService.getSpecificGroup(topologyTemplate.componentType, topologyTemplate.uniqueId, action.payload.component.uniqueId).pipe(tap(resp => { + this.compositionService.updateGroup(resp); + patchState({ + selectedComponent: resp, + selectedComponentType: action.payload.type, + panelLoading: false + }); + }, err => { + patchState({ + panelLoading: false + }) + } + )); + } else { //TopologyTemplate + patchState({ + selectedComponent: action.payload.component, + selectedComponentType: action.payload.type, + panelLoading: false + }) + } + } + + + // @Action(UpdateSelectedComponentNameAction) + // UpdateSelectedComponentNameAction({patchState}:StateContext<CompositionStateModel>, action: UpdateSelectedComponentNameAction) { + + // switch(action.payload.type){ + // case SelectedComponentType.COMPONENT_INSTANCE: + // this.store.dispatch(new UpdateComponentInstancesAction([action.payload.component])); + // break; + // case SelectedComponentType.POLICY: + // this.store.dispatch(new UpdatePolicyNameAction(action.payload.uniqueId, action.payload.newName)); + // break; + // case SelectedComponentType.GROUP: + // this.store.dispatch(new UpdateGroupInstancesAction) + + // } + // if(action.payload.type === SelectedComponentType.COMPONENT_INSTANCE){ + + // } + + // } + + @Selector() + static getSelectedComponent(state:CompositionStateModel) { + return state.selectedComponent; + } + + @Selector() + static getSelectedComponentId(state:CompositionStateModel) { + return state.selectedComponent.uniqueId; + } + + @Selector() + static getSelectedComponentType(state:CompositionStateModel) { + return state.selectedComponentType; + } + + + @Action(OnSidebarOpenOrCloseAction) + onSidebarOpenOrCloseAction({getState, setState}:StateContext<CompositionStateModel>) { + const state:CompositionStateModel = getState(); + + setState({ + ...state, + withSidebar: !state.withSidebar + }); + } + + @Action(TogglePanelLoadingAction) + TogglePanelLoading({patchState}:StateContext<CompositionStateModel>, action: TogglePanelLoadingAction) { + + patchState({ + panelLoading: action.payload.isLoading + }); + } + + @Selector() static withSidebar(state):boolean { + return state.withSidebar; + } + +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/composition/composition-page.component.html b/catalog-ui/src/app/ng2/pages/composition/composition-page.component.html new file mode 100644 index 0000000000..e1851d5c0c --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/composition-page.component.html @@ -0,0 +1,8 @@ +<div class="workspace-composition-page"> + <div class="composition-graph"> + <composition-palette></composition-palette> + <app-palette-popup-panel></app-palette-popup-panel> + <composition-graph dndDropzone [dndAllowExternal]=true [topologyTemplate]="topologyTemplate" [testId]="'canvas'"></composition-graph> + <ng2-composition-panel [topologyTemplate]="topologyTemplate"></ng2-composition-panel> + </div> +</div> diff --git a/catalog-ui/src/app/ng2/pages/composition/composition-page.component.less b/catalog-ui/src/app/ng2/pages/composition/composition-page.component.less new file mode 100644 index 0000000000..a80333e2be --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/composition-page.component.less @@ -0,0 +1,26 @@ +@import "./../../../../assets/styles/override"; +.workspace-composition-page { + height:100%; + display: block; + text-align: left; + align-items: left; + padding: 0; + + .composition-graph { + height:100%; + background-color: @sdcui_color_white; + bottom: 0; + display:flex; + flex-direction: row; + + .view-mode{ + background-color: #f8f8f8; + border:0; + } + } +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} diff --git a/catalog-ui/src/app/ng2/pages/composition/composition-page.component.ts b/catalog-ui/src/app/ng2/pages/composition/composition-page.component.ts new file mode 100644 index 0000000000..ed1b82e1df --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/composition-page.component.ts @@ -0,0 +1,47 @@ +/*- + * ============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========================================================= + */ +import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; +import { Component as TopologyTemplate } from 'app/models'; +import * as Constants from 'constants'; +import { EventListenerService } from '../../../services/event-listener-service'; +import { EVENTS } from '../../../utils'; + +@Component({ + templateUrl: './composition-page.component.html', + styleUrls: ['composition-page.component.less'] +}) +export class CompositionPageComponent implements OnInit, OnDestroy { + + private topologyTemplate: TopologyTemplate; + + constructor(@Inject('$stateParams') private stateParams, private eventListenerService: EventListenerService) { + this.topologyTemplate = stateParams.component; + } + + ngOnInit(): void { + this.eventListenerService.registerObserverCallback(EVENTS.ON_CHECKOUT, (comp) => { + this.topologyTemplate = comp; + }); + } + + ngOnDestroy(): void { + this.eventListenerService.unRegisterObserver(EVENTS.ON_CHECKOUT); + } +} diff --git a/catalog-ui/src/app/ng2/pages/composition/composition-page.module.ts b/catalog-ui/src/app/ng2/pages/composition/composition-page.module.ts new file mode 100644 index 0000000000..d0ca05b2be --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/composition-page.module.ts @@ -0,0 +1,30 @@ +/** + * Created by ob0695 on 6/4/2018. + */ +import {NgModule} from "@angular/core"; +import {CommonModule} from "@angular/common"; +import {CompositionGraphModule} from "./graph/composition-graph.module"; +import {CompositionPageComponent} from "./composition-page.component"; +import {NgxsModule} from "@ngxs/store"; +import {PaletteModule} from "./palette/palette.module"; +import {PalettePopupPanelComponent} from "./palette/palette-popup-panel/palette-popup-panel.component"; +import { CompositionPanelModule } from "app/ng2/pages/composition/panel/composition-panel.module"; +import {CompositionService} from "./composition.service"; +import {DndModule} from "ngx-drag-drop"; +import {GraphState} from "./common/store/graph.state"; + +@NgModule({ + declarations: [CompositionPageComponent, PalettePopupPanelComponent], + imports: [CommonModule, + CompositionGraphModule, + CompositionPanelModule, + PaletteModule, + DndModule, + NgxsModule.forFeature([ + GraphState])], + exports: [CompositionPageComponent], + entryComponents: [CompositionPageComponent], + providers: [CompositionService] +}) +export class CompositionPageModule { +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/composition/composition.service.ts b/catalog-ui/src/app/ng2/pages/composition/composition.service.ts new file mode 100644 index 0000000000..e5e9d2dca8 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/composition.service.ts @@ -0,0 +1,59 @@ +import {Injectable} from "@angular/core"; +import 'rxjs/add/observable/forkJoin'; +import {Component, PropertiesGroup, AttributesGroup, PolicyInstance} from "app/models"; +import {GroupInstance} from "app/models/graph/zones/group-instance"; +import {CommonGraphDataService} from "./common/common-graph-data.service"; +import {ForwardingPath} from "../../../models/forwarding-path"; +import {SelectedComponentType} from "./common/store/graph.actions"; + +@Injectable() +export class CompositionService extends CommonGraphDataService{ + + public originComponents: Array<Component>; //This contains the full data set after specifically requesting it. The uniqueId matches the 'componentUid' in the componentInstances array + public componentInstancesProperties:PropertiesGroup; + public componentInstancesAttributes:AttributesGroup; + public groupInstances: GroupInstance[]; + public policies: PolicyInstance[]; + public forwardingPaths: { [key:string]:ForwardingPath }; + public selectedComponentType: SelectedComponentType; + + //---------------------------- COMPONENT INSTANCES ------------------------------------// + + public getOriginComponentById = (uniqueId:string):Component => { + return this.originComponents && this.originComponents.find(instance => instance.uniqueId === uniqueId); + } + + public addOriginComponent = (originComponent:Component) => { + if(!this.originComponents) this.originComponents = []; + if(!this.getOriginComponentById(originComponent.uniqueId)){ + this.originComponents.push(originComponent); + } + } + + + public updateGroup = (instance: GroupInstance) => { + this.groupInstances = this.groupInstances.map(group => instance.uniqueId === group.uniqueId? instance : group); + } + + public updatePolicy = (instance: PolicyInstance) => { + this.policies = this.policies.map(policy => instance.uniqueId === policy.uniqueId? instance : policy); + } + + //---------------------------- POLICIES---------------------------------// + public addPolicyInstance = (instance: PolicyInstance) => { + return this.policies.push(instance); + } + + + //---------------------------- POLICIES---------------------------------// + public addGroupInstance = (instance: GroupInstance) => { + return this.groupInstances.push(instance); + } + + + //----------------------------SELECTED COMPONENT -----------------------// + + public setSelectedComponentType = (selectedType: SelectedComponentType) => { + this.selectedComponentType = selectedType; + } +} diff --git a/catalog-ui/src/app/ng2/pages/composition/deployment/deployment-graph.component.html b/catalog-ui/src/app/ng2/pages/composition/deployment/deployment-graph.component.html new file mode 100644 index 0000000000..4a163ee24b --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/deployment/deployment-graph.component.html @@ -0,0 +1 @@ +<div class="sdc-deployment-graph-wrapper"></div> diff --git a/catalog-ui/src/app/ng2/pages/composition/deployment/deployment-graph.component.less b/catalog-ui/src/app/ng2/pages/composition/deployment/deployment-graph.component.less new file mode 100644 index 0000000000..9b80fcd651 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/deployment/deployment-graph.component.less @@ -0,0 +1,13 @@ +.sdc-deployment-graph-wrapper { + height: 100%; + width: 100%; + + ::ng-deep canvas { + /*canvas z-index is initialized to 999 while top-nav z-Index is 10, which makes top-nav disappear, so z-Index must be overwritten here*/ + z-index: 10 !important; + } + } + +::ng-deep .sdc-workspace-container .w-sdc-main-right-container .w-sdc-main-container-body-content.deploy-body-content{ + padding: 0px; +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/composition/deployment/deployment-graph.component.spec.ts b/catalog-ui/src/app/ng2/pages/composition/deployment/deployment-graph.component.spec.ts new file mode 100644 index 0000000000..823086fbbf --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/deployment/deployment-graph.component.spec.ts @@ -0,0 +1,92 @@ +import {async, ComponentFixture} from '@angular/core/testing'; +import 'jest-dom/extend-expect'; +import {DeploymentGraphComponent} from "./deployment-graph.component"; +import {DeploymentGraphService} from "./deployment-graph.service"; +import {NO_ERRORS_SCHEMA} from "@angular/core"; +import * as cytoscape from "cytoscape/dist/cytoscape" +import {AngularJSBridge} from "../../../../services/angular-js-bridge-service"; +import {NodesFactory} from "../../../../models/graph/nodes/nodes-factory"; +import {CommonGraphUtils} from "../graph/common/common-graph-utils"; +import {groupsMock} from "../../../../../jest/mocks/groups.mock"; +import {Module} from "../../../../models/modules/base-module"; +import {ComponentInstance} from "../../../../models/componentsInstances/componentInstance"; +import {componentInstancesMock} from "../../../../../jest/mocks/component-instance.mock"; +import {ConfigureFn, configureTests} from "../../../../../jest/test-config.helper"; +import {TopologyTemplateService} from "../../../services/component-services/topology-template.service"; +import {WorkspaceService} from "../../workspace/workspace.service"; +import {SdcConfigToken} from "../../../config/sdc-config.config"; +import {CompositionGraphLinkUtils} from "../graph/utils"; + +describe('DeploymentGraphComponent', () => { + + let fixture: ComponentFixture<DeploymentGraphComponent>; + let deploymentGraphServiceMock: Partial<DeploymentGraphService>; + let nodeFactoryServiceMock: Partial<NodesFactory>; + let commonGraphUtilsServiceMock: Partial<CommonGraphUtils>; + let angularJsBridgeServiceMock: Partial<AngularJSBridge>; + let sdcConfigTokenMock: Partial<AngularJSBridge>; + + beforeEach( + async(() => { + + deploymentGraphServiceMock = { + modules: <Array<Module>>groupsMock, + componentInstances: <Array<ComponentInstance>>componentInstancesMock + } + + nodeFactoryServiceMock = { + createModuleNode: jest.fn().mockResolvedValue(() => { + }), + createNode: jest.fn().mockResolvedValue(() => { + }) + } + + commonGraphUtilsServiceMock = { + addNodeToGraph: jest.fn(), + addComponentInstanceNodeToGraph: jest.fn() + } + + sdcConfigTokenMock = { + imagePath: '' + } + + const configure: ConfigureFn = testBed => { + testBed.configureTestingModule({ + declarations: [DeploymentGraphComponent], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + {provide: DeploymentGraphService, useValue: deploymentGraphServiceMock}, + {provide: NodesFactory, useValue: nodeFactoryServiceMock}, + {provide: TopologyTemplateService, useValue: {}}, + {provide: WorkspaceService, useValue: {}}, + {provide: CommonGraphUtils, useValue: commonGraphUtilsServiceMock}, + {provide: CompositionGraphLinkUtils, useValue: {}}, + {provide: AngularJSBridge, useValue: angularJsBridgeServiceMock}, + {provide: SdcConfigToken, useValue: SdcConfigToken} + ] + }); + }; + + configureTests(configure).then(testBed => { + fixture = testBed.createComponent(DeploymentGraphComponent); + }); + }) + ); + + it('expected deployment graph component to be defined', () => { + expect(fixture).toBeDefined(); + }); + + + it('expected to addNodeToGraph to haveBeenCalled 6 times out of 7 cause one of the instances have no parent module', () => { + fixture.componentInstance._cy = cytoscape({ + zoomingEnabled: false, + selectionType: 'single', + }); + jest.spyOn(fixture.componentInstance, 'findInstanceModule'); + fixture.componentInstance.initGraphComponentInstances(); + expect(fixture.componentInstance.findInstanceModule).toHaveBeenCalledTimes(7); + expect(commonGraphUtilsServiceMock.addComponentInstanceNodeToGraph).toHaveBeenCalledTimes(6); + }); + +});
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/composition/deployment/deployment-graph.component.ts b/catalog-ui/src/app/ng2/pages/composition/deployment/deployment-graph.component.ts new file mode 100644 index 0000000000..143a759960 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/deployment/deployment-graph.component.ts @@ -0,0 +1,127 @@ +import {Component, ElementRef, Inject, OnInit} from "@angular/core"; +import {DeploymentGraphService} from "./deployment-graph.service"; +import '@bardit/cytoscape-expand-collapse'; +import * as _ from "lodash"; +import {TopologyTemplateService} from "../../../services/component-services/topology-template.service"; +import {WorkspaceService} from "../../workspace/workspace.service"; +import {NodesFactory} from "../../../../models/graph/nodes/nodes-factory"; +import {CommonGraphUtils} from "../graph/common/common-graph-utils"; +import {ISdcConfig, SdcConfigToken} from "../../../config/sdc-config.config"; +import {Module} from "../../../../models/modules/base-module"; +import {ComponentInstance} from "../../../../models/componentsInstances/componentInstance"; +import {ComponentGenericResponse} from "../../../services/responses/component-generic-response"; +import {ComponentInstanceFactory} from "../../../../utils/component-instance-factory"; +import {ModulesNodesStyle} from "../graph/common/style/module-node-style"; +import {ComponentInstanceNodesStyle} from "../graph/common/style/component-instances-nodes-style"; +import {CompositionGraphLinkUtils} from "../graph/utils/composition-graph-links-utils"; + +@Component({ + selector: 'deployment-graph', + templateUrl: './deployment-graph.component.html', + styleUrls: ['./deployment-graph.component.less'] +}) + +export class DeploymentGraphComponent implements OnInit { + constructor(private elRef: ElementRef, + private topologyTemplateService: TopologyTemplateService, + private workspaceService: WorkspaceService, + private deploymentService: DeploymentGraphService, + private commonGraphUtils: CommonGraphUtils, + private nodeFactory: NodesFactory, + private commonGraphLinkUtils: CompositionGraphLinkUtils, + @Inject(SdcConfigToken) private sdcConfig: ISdcConfig) { + + } + + public _cy: Cy.Instance; + + ngOnInit(): void { + this.topologyTemplateService.getDeploymentGraphData(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId).subscribe((response: ComponentGenericResponse) => { + this.deploymentService.componentInstances = response.componentInstances; + this.deploymentService.componentInstancesRelations = response.componentInstancesRelations; + this.deploymentService.modules = response.modules; + this.loadGraph(); + }); + } + + public findInstanceModule = (groupsArray: Array<Module>, componentInstanceId: string): string => { + let parentGroup: Module = _.find(groupsArray, (group: Module) => { + return _.find(_.values(group.members), (member: string) => { + return member === componentInstanceId; + }); + }); + return parentGroup ? parentGroup.uniqueId : ""; + }; + + public initGraphModules = () => { + if (this.deploymentService.modules) { // Init module nodes + _.each(this.deploymentService.modules, (groupModule: Module) => { + let moduleNode = this.nodeFactory.createModuleNode(groupModule); + this.commonGraphUtils.addNodeToGraph(this._cy, moduleNode); + }); + } + } + + public initGraphComponentInstances = () => { + _.each(this.deploymentService.componentInstances, (instance: ComponentInstance) => { // Init component instance nodes + let componentInstanceNode = this.nodeFactory.createNode(instance); + componentInstanceNode.parent = this.findInstanceModule(this.deploymentService.modules, instance.uniqueId); + if (componentInstanceNode.parent) { // we are not drawing instances that are not a part of a module + this.commonGraphUtils.addComponentInstanceNodeToGraph(this._cy, componentInstanceNode); + } + }); + } + + public handleEmptyModule = () => { + // This is a special functionality to pass the cytoscape default behavior - we can't create Parent module node without children's + // so we must add an empty dummy child node + _.each(this._cy.nodes('[?isGroup]'), (moduleNode: Cy.CollectionFirstNode) => { + if (!moduleNode.isParent()) { + let dummyInstance = ComponentInstanceFactory.createEmptyComponentInstance(); + let componentInstanceNode = this.nodeFactory.createNode(dummyInstance); + componentInstanceNode.parent = moduleNode.id(); + let dummyNode = this.commonGraphUtils.addNodeToGraph(this._cy, componentInstanceNode, moduleNode.position()); + dummyNode.addClass('dummy-node'); + } + }) + } + + public initGraphNodes = (): void => { + this.initGraphModules(); + this.initGraphComponentInstances(); + this.handleEmptyModule(); + }; + + private loadGraph = () => { + + let graphEl = this.elRef.nativeElement.querySelector('.sdc-deployment-graph-wrapper'); + this._cy = cytoscape({ + container: graphEl, + style: ComponentInstanceNodesStyle.getCompositionGraphStyle().concat(ModulesNodesStyle.getModuleGraphStyle()), + zoomingEnabled: false, + selectionType: 'single', + + }); + + //adding expand collapse extension + this._cy.expandCollapse({ + layoutBy: { + name: "grid", + animate: true, + randomize: false, + fit: true + }, + fisheye: false, + undoable: false, + expandCollapseCueSize: 18, + expandCueImage: this.sdcConfig.imagesPath + '/assets/styles/images/resource-icons/' + 'closeModule.png', + collapseCueImage: this.sdcConfig.imagesPath + '/assets/styles/images/resource-icons/' + 'openModule.png', + expandCollapseCueSensitivity: 2, + cueOffset: -20 + }); + + this.initGraphNodes(); //creating instances nodes + this.commonGraphLinkUtils.initGraphLinks(this._cy, this.deploymentService.componentInstancesRelations); + this._cy.collapseAll(); + }; +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/composition/deployment/deployment-graph.module.ts b/catalog-ui/src/app/ng2/pages/composition/deployment/deployment-graph.module.ts new file mode 100644 index 0000000000..91f97db8c3 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/deployment/deployment-graph.module.ts @@ -0,0 +1,15 @@ +import {NgModule} from "@angular/core"; +import {CommonModule} from "@angular/common"; +import {DeploymentGraphComponent} from "./deployment-graph.component"; + +@NgModule({ + declarations: [DeploymentGraphComponent], + imports: [CommonModule], + exports: [DeploymentGraphComponent], + entryComponents: [DeploymentGraphComponent], + providers: [ + + ] +}) +export class DeploymentGraphModule { +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/composition/deployment/deployment-graph.service.ts b/catalog-ui/src/app/ng2/pages/composition/deployment/deployment-graph.service.ts new file mode 100644 index 0000000000..7ec346c20b --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/deployment/deployment-graph.service.ts @@ -0,0 +1,8 @@ +import {Injectable} from "@angular/core"; +import 'rxjs/add/observable/forkJoin'; +import {CommonGraphDataService} from "../common/common-graph-data.service"; +import {Module} from "../../../../models/modules/base-module"; +@Injectable() +export class DeploymentGraphService extends CommonGraphDataService { + public modules:Array<Module>; +} diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/canvas-search/canvas-search.component.html b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-search/canvas-search.component.html new file mode 100644 index 0000000000..a8645dc5f0 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-search/canvas-search.component.html @@ -0,0 +1,23 @@ +<div class="canvas-search-component" [ngClass]="{'results-shown': autoCompleteResults.length}" + [class.canvas-search-visible]="autoCompleteValues && autoCompleteValues.length" [attr.data-tests-id]="testId"> + <div class="canvas-search-bar-container" [attr.data-tests-id]="testId" + [class.active]="searchQuery && searchQuery.length"> + <sdc-search-bar class="canvas-search-bar" + [placeHolder]="placeholder" + (onSearchClicked)="onSearchClicked($event)" + [size]="'medium'" + [value]="searchQuery" + (valueChange)="onSearchQueryChanged($event)"> + </sdc-search-bar> + <svg-icon class="canvas-clear-search" + [name]="'close'" + [clickable]="true" + [mode]="'secondary'" + [size]="'small'" + (click)="onClearSearch()"> + </svg-icon> + </div> + <dropdown-results *ngIf="autoCompleteResults && autoCompleteResults.length" [options]="autoCompleteResults" + (onItemSelected)="onItemSelected($event)"></dropdown-results> +</div> + diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/canvas-search/canvas-search.component.less b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-search/canvas-search.component.less new file mode 100644 index 0000000000..247f2a3913 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-search/canvas-search.component.less @@ -0,0 +1,42 @@ +.canvas-search-component { + + .canvas-search-bar-container { + display:flex; + border-radius: 4px; + align-items: center; + box-shadow: 0px 2px 3.88px 0.12px rgba(0, 0, 0, 0.29); + + /deep/.sdc-search-bar .search-bar-container .search-button { + border: solid 1px #d2d2d2; + } + + /deep/.sdc-input__input { + width: 250px; + transition: all 0.4s; + } + + .canvas-clear-search { + position: absolute; + right: 45px; + } + } + + &:not(:hover):not(.canvas-search-visible):not(.active) { + border-radius: 0; + box-shadow: none; + + /deep/.sdc-input__input:not(:focus) { + border: none; + padding: 0px; + width: 0px; + } + .canvas-clear-search { + display: none; + } + } +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/canvas-search/canvas-search.component.ts b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-search/canvas-search.component.ts new file mode 100644 index 0000000000..c1a45a9a4b --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-search/canvas-search.component.ts @@ -0,0 +1,25 @@ +import {Component, EventEmitter, Input, Output} from '@angular/core'; +import {AutoCompleteComponent} from "onap-ui-angular/dist/autocomplete/autocomplete.component"; + +@Component({ + selector: 'canvas-search', + templateUrl: './canvas-search.component.html', + styleUrls: ['./canvas-search.component.less'] +}) +export class CanvasSearchComponent extends AutoCompleteComponent { + + @Output() public searchButtonClicked: EventEmitter<string> = new EventEmitter<string>(); + @Output() public onSelectedItem: EventEmitter<string> = new EventEmitter<string>(); + + public onSearchClicked = (searchText:string)=> { + this.searchButtonClicked.emit(searchText); + } + + public onItemSelected = (selectedItem) => { + this.searchQuery = selectedItem.value; + this.autoCompleteResults = []; + this.searchButtonClicked.emit(this.searchQuery); + this.onSelectedItem.emit(selectedItem); + } + +} diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/canvas-search/canvas-search.module.ts b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-search/canvas-search.module.ts new file mode 100644 index 0000000000..6df06067a6 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-search/canvas-search.module.ts @@ -0,0 +1,30 @@ +import {SdcUiComponentsModule} from "onap-ui-angular"; +import { NgModule } from "@angular/core"; +import {CanvasSearchComponent} from "./canvas-search.component"; +import {CommonModule} from "@angular/common"; +import {BrowserAnimationsModule} from "@angular/platform-browser/animations"; +import {HttpClientModule} from "@angular/common/http"; +import {BrowserModule} from "@angular/platform-browser"; +import {AutocompletePipe} from "onap-ui-angular/dist/autocomplete/autocomplete.pipe"; + +@NgModule({ + declarations: [ + CanvasSearchComponent + ], + imports: [ + CommonModule, + BrowserModule, + HttpClientModule, + BrowserAnimationsModule, + SdcUiComponentsModule, + ], + exports: [ + CanvasSearchComponent + ], + entryComponents: [ + CanvasSearchComponent + ], + providers: [AutocompletePipe] +}) +export class CanvasSearchModule { +} diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/__snapshots__/zone-container.component.spec.ts.snap b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/__snapshots__/zone-container.component.spec.ts.snap new file mode 100644 index 0000000000..d4e2a7a359 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/__snapshots__/zone-container.component.spec.ts.snap @@ -0,0 +1,35 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ZoneContainerComponent should match current snapshot of palette element component 1`] = ` +<zone-container + backgroundClick={[Function EventEmitter]} + backgroundClicked={[Function Function]} + minimize={[Function EventEmitter]} + unminifyZone={[Function Function]} +> + <div> + <div + class="sdc-canvas-zone__header" + > + <div + class="sdc-canvas-zone__title" + > + + <span + class="sdc-canvas-zone__counter" + > + + </span> + </div> + <span + class="sdc-canvas-zone__state-button" + > + – + </span> + </div> + <div + class="sdc-canvas-zone__container" + /> + </div> +</zone-container> +`; diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-container.component.html b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-container.component.html new file mode 100644 index 0000000000..d6343a4a4f --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-container.component.html @@ -0,0 +1,30 @@ +<!-- + ~ Copyright (C) 2018 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. + --> + + +<div class="sdc-canvas-zone {{class}}-zone" [class.minimized]="minimized" [class.hidden]="!visible" + (click)="backgroundClicked()"> + <div class="sdc-canvas-zone__header" (click)="unminifyZone(); $event.stopPropagation();"> + <div class="sdc-canvas-zone__title">{{title}} + <span class="sdc-canvas-zone__counter">{{count}}</span> + </div> + <span class="sdc-canvas-zone__state-button">–</span> + </div> + <div class="sdc-canvas-zone__container" #scrollDiv> + <ng-content></ng-content> + </div> +</div> + diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-container.component.less b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-container.component.less new file mode 100644 index 0000000000..827786cc49 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-container.component.less @@ -0,0 +1,62 @@ +.sdc-canvas-zone { + width: 285px; + max-height:186px; + display:flex; + flex-direction:column; + color:white; + font-family:OpenSans-Regular, sans-serif; + transition: width .2s ease-in-out, max-height .2s ease-in-out .1s; + position:relative; + bottom:0px; + margin-right: 5px; + + .sdc-canvas-zone__header { + background: #5A5A5A; + border-radius: 2px 2px 0 0; + padding: 5px 10px; + display:flex; + justify-content: space-between; + font-size: 14px; + text-transform:uppercase; + .sdc-canvas-zone__state-button { + font-weight:bold; + cursor:pointer; + } + } + + .sdc-canvas-zone__container { + padding:5px; + background-color: #5A5A5A; + opacity:0.9; + flex: 1; + display:flex; + flex-direction: row; + align-items: flex-start; + flex-wrap:wrap; + overflow-y:auto; + min-height: 80px; + max-height: 170px; + } + + + &.minimized { + max-height:30px; + width:120px; + cursor:pointer; + + .sdc-canvas-zone__state-button { + display:none; + } + .sdc-canvas-zone__container { + flex: 0 0 0; + min-height: 0; + padding: 0; + overflow-y:hidden; + transition: min-height .2s ease-in-out .2s; + transition: padding .1s ease-in-out 0s; + } + } + &.hidden { + display:none; + } +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-container.component.spec.ts b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-container.component.spec.ts new file mode 100644 index 0000000000..c432054492 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-container.component.spec.ts @@ -0,0 +1,46 @@ +import {async, ComponentFixture} from '@angular/core/testing'; +import {By} from '@angular/platform-browser'; +import {ConfigureFn, configureTests} from '../../../../../../jest/test-config.helper'; +import 'jest-dom/extend-expect'; +import {ZoneInstanceType} from '../../../../../../app/models/graph/zones/zone-instance'; +import {ZoneContainerComponent} from './zone-container.component'; + + +describe('ZoneContainerComponent', () => { + let fixture: ComponentFixture<ZoneContainerComponent>; + + beforeEach( + async(() => { + const configure: ConfigureFn = testBed => { + testBed.configureTestingModule({ + declarations: [ZoneContainerComponent] + }); + }; + + configureTests(configure).then(testBed => { + fixture = testBed.createComponent(ZoneContainerComponent); + }); + }) + ); + + + it('should match current snapshot of palette element component', () => { + expect(fixture).toMatchSnapshot(); + }); + + it('should have a group-zone class when the ZoneInstanceType is GROUP', + () => { + fixture.componentInstance.type = ZoneInstanceType.GROUP; + fixture.detectChanges(); + const compiled = fixture.debugElement.query(By.css('.sdc-canvas-zone')); + expect(compiled.nativeElement).toHaveClass('group-zone'); + }); + + it('should have a policy-zone class when the ZoneInstanceType is POLICY', + () => { + fixture.componentInstance.type = ZoneInstanceType.POLICY; + fixture.detectChanges(); + const compiled = fixture.debugElement.query(By.css('.sdc-canvas-zone')); + expect(compiled.nativeElement).toHaveClass('policy-zone'); + }); +});
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-container.component.ts b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-container.component.ts new file mode 100644 index 0000000000..4757c1f36d --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-container.component.ts @@ -0,0 +1,35 @@ +import { Component, Input, Output, ViewEncapsulation, EventEmitter, OnInit } from '@angular/core'; +import { ZoneInstanceType } from 'app/models/graph/zones/zone-instance'; + +@Component({ + selector: 'zone-container', + templateUrl: './zone-container.component.html', + styleUrls: ['./zone-container.component.less'], + encapsulation: ViewEncapsulation.None +}) + +export class ZoneContainerComponent implements OnInit { + @Input() title:string; + @Input() type:ZoneInstanceType; + @Input() count:number; + @Input() visible:boolean; + @Input() minimized:boolean; + @Output() minimize: EventEmitter<any> = new EventEmitter<any>(); + @Output() backgroundClick: EventEmitter<void> = new EventEmitter<void>(); + private class:string; + + constructor() {} + + ngOnInit() { + this.class = ZoneInstanceType[this.type].toLowerCase(); + } + + private unminifyZone = () => { + this.minimize.emit(); + } + + private backgroundClicked = () => { + this.backgroundClick.emit(); + } + +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-instance/zone-instance.component.html b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-instance/zone-instance.component.html new file mode 100644 index 0000000000..d97be69e34 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-instance/zone-instance.component.html @@ -0,0 +1,27 @@ +<!-- + ~ Copyright (C) 2018 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. + --> + + +<div #currentComponent class="zone-instance mode-{{zoneInstance.mode}}" [class.locked]="activeInstanceMode > MODE.HOVER" + [class.hiding]="hidden" + (mouseenter)="setMode(MODE.HOVER)" (mouseleave)="setMode(MODE.NONE)" (click)="setMode(MODE.SELECTED, $event)"> + <div class="zone-instance__body" sdc-tooltip tooltip-text="{{zoneInstance.instanceData.name}}" [attr.data-tests-id]="zoneInstance.instanceData.name"> + <div *ngIf="zoneInstance.handle" class="target-handle {{zoneInstance.handle}}" + (click)="tagHandleClicked($event)"></div> + <div *ngIf="!isViewOnly" class="zone-instance__handle" (click)="setMode(MODE.TAG, $event)">+</div> + <div class="zone-instance__body-content">{{zoneInstance.assignments.length || defaultIconText}}</div> + </div> + <div class="zone-instance__name">{{zoneInstance.instanceData.name}}</div> +</div> diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-instance/zone-instance.component.less b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-instance/zone-instance.component.less new file mode 100644 index 0000000000..c34b8e149a --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-instance/zone-instance.component.less @@ -0,0 +1,135 @@ +@import '../../../../../../../assets/styles/variables'; + +.zone-instance { + + width:76px; + margin:5px; + opacity:1; + + .zone-instance__handle { + display:none; + position:absolute; + left: 31px; + top: 8px; + width:22px; + height:22px; + cursor:pointer; + border: solid @main_color_p 1px; + border-radius: 2px; + text-align: center; + font-weight:bold; + } + + .zone-instance__body { + position:relative; + margin:0 auto; + width:43px; + height:43px; + display:flex; + padding:3px; + } + + .zone-instance__body-content { + border-radius: 2px; + flex:1; + color:@main_color_p; + font-size:16px; + text-align:center; + display:flex; + align-items: center; + justify-content: center; + box-shadow:none; + transition:box-shadow 5s; + } + + .zone-instance__name { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + text-align:center; + } + /* Dynamic classes below */ + + .target-handle { + position:absolute; + width:18px; + height:18px; + display:block; + top: -4px; + right: -6px; + background-size: 100% 100%; + cursor: url("../../../../../../../assets/styles/images/canvas-tagging-icons/policy_2.svg"), pointer; + + &.tagged-policy { + background-image: url('../../../../../../../assets/styles/images/canvas-tagging-icons/policy_added.svg'); + } + + &.tag-available { + background-image: url('../../../../../../../assets/styles/images/canvas-tagging-icons/indication.svg'); + } + } + + + &.mode-1, &.mode-2, &.mode-3 { //hover, selected, tag + .zone-instance__body { + border:solid 2px; + border-radius: 2px; + padding:2px; + cursor:pointer; + } + } + + &.mode-1, &.mode-2:hover{ + .zone-instance__handle{ + display:block; + } + } + + &.locked { + cursor: inherit; + } + + &.hiding { + opacity:0; + .zone-instance__body-content { + box-shadow: #CCC 0px 0px 15px; + } + } + + + &.mode-3 .zone-instance__handle { + width:24px; + height:24px; + right:-6px; + top:7px; + display:block; + background-image: linear-gradient(-140deg, #009E98 0%, #97D648 100%); + border: 2px solid @main_color_p; + border-radius: 2px; + box-shadow: inset 2px -2px 3px 0 #007A3E; + } + +} +.sdc-canvas-zone.group-zone { + .zone-instance__handle { + background-color:@main_color_a; + } + .zone-instance__body { + border-color:@main_color_a; + .zone-instance__body-content { + background: @main_color_a; + } + } +} + +.sdc-canvas-zone.policy-zone { + .zone-instance__handle { + background-color:@main_color_r; + } + .zone-instance__body { + border-color:@main_color_r; + .zone-instance__body-content { + background: @main_color_r; + } + } +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-instance/zone-instance.component.spec.ts b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-instance/zone-instance.component.spec.ts new file mode 100644 index 0000000000..f5a5f6f546 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-instance/zone-instance.component.spec.ts @@ -0,0 +1,132 @@ +import { ComponentFixture, TestBed, async } from '@angular/core/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { SimpleChanges } from '@angular/core'; +import { PoliciesService } from 'app/ng2/services/policies.service'; +import { GroupsService } from 'app/ng2/services/groups.service'; +import { EventListenerService } from 'app/services'; +import { Store } from '@ngxs/store'; +import { CompositionService } from 'app/ng2/pages/composition/composition.service'; +import { ZoneInstanceComponent } from './zone-instance.component'; +import { ZoneInstanceType, ZoneInstance, ZoneInstanceMode, ZoneInstanceAssignmentType, IZoneInstanceAssignment } from "app/models"; +import { PolicyInstance } from "app/models/graph/zones/policy-instance"; +import { Subject, of } from 'rxjs'; +import { _throw } from 'rxjs/observable/throw'; + +describe('ZoneInstanceComponent', () => { + let component: ZoneInstanceComponent; + let fixture: ComponentFixture<ZoneInstanceComponent>; + + let createPolicyInstance = () => { + let policy = new PolicyInstance(); + policy.targets = {COMPONENT_INSTANCES: [], GROUPS: []}; + return new ZoneInstance(policy, '', ''); + } + + beforeEach(() => { + const policiesServiceStub = {updateZoneInstanceAssignments : jest.fn()}; + const groupsServiceStub = {}; + const eventListenerServiceStub = {}; + const storeStub = {}; + const compositionServiceStub = {}; + TestBed.configureTestingModule({ + schemas: [NO_ERRORS_SCHEMA], + declarations: [ZoneInstanceComponent], + providers: [ + { provide: PoliciesService, useValue: policiesServiceStub }, + { provide: GroupsService, useValue: groupsServiceStub }, + { provide: EventListenerService, useValue: eventListenerServiceStub }, + { provide: Store, useValue: storeStub }, + { provide: CompositionService, useValue: compositionServiceStub } + ] + }).compileComponents().then(() => { + fixture = TestBed.createComponent(ZoneInstanceComponent); + component = fixture.componentInstance; + }); + }); + + it('can load instance', async((done) => { + component.zoneInstance = <ZoneInstance>{type : ZoneInstanceType.POLICY, instanceData: {name: 'test policy'}, assignments: []}; + component.forceSave = new Subject<Function>(); + fixture.detectChanges(); + expect(component).toBeTruthy(); + })); + + + it('if another instance is already tagging, i cannot change my mode', ()=> { + component.zoneInstance = <ZoneInstance>{ mode: ZoneInstanceMode.NONE }; + component.isActive = false; + component.activeInstanceMode = ZoneInstanceMode.TAG; + component.setMode(ZoneInstanceMode.SELECTED); + expect(component.zoneInstance.mode).toBe(ZoneInstanceMode.NONE); + }); + + it('if i am active(selected) and NOT in tag mode, I can set another mode', ()=> { + component.isActive = true; + component.zoneInstance = <ZoneInstance>{ mode: ZoneInstanceMode.SELECTED }; + jest.spyOn(component.modeChange, 'emit'); + component.setMode(ZoneInstanceMode.NONE); + expect(component.modeChange.emit).toHaveBeenCalledWith({instance: component.zoneInstance, newMode: ZoneInstanceMode.NONE }); + }); + + it('if i am active and in tag mode and i try to set mode other than tag, I am not allowed', ()=> { + component.isActive = true; + component.zoneInstance = <ZoneInstance>{ mode: ZoneInstanceMode.TAG }; + component.setMode(ZoneInstanceMode.SELECTED); + expect(component.zoneInstance.mode).toBe(ZoneInstanceMode.TAG); + }); + + it('if i am active and in tag mode and click tag again and no changes, does NOT call save, but DOES turn tagging off', ()=> { + component.isActive = true; + component.zoneInstance = createPolicyInstance(); + component.zoneService = component.policiesService; + component.zoneInstance.mode = ZoneInstanceMode.TAG; + jest.spyOn(component.zoneService, 'updateZoneInstanceAssignments'); + jest.spyOn(component.modeChange, 'emit'); + + component.setMode(ZoneInstanceMode.TAG); + + expect(component.zoneService.updateZoneInstanceAssignments).not.toHaveBeenCalled(); + expect(component.modeChange.emit).toHaveBeenCalledWith({instance: component.zoneInstance, newMode: ZoneInstanceMode.NONE }); + + }); + it('if i am active and in tag mode and click tag again and HAVE changes, calls save AND turns tagging off', ()=> { + component.isActive = true; + component.zoneInstance = createPolicyInstance(); + component.zoneService = component.policiesService; + component.zoneInstance.mode = ZoneInstanceMode.TAG; + component.zoneInstance.assignments.push(<IZoneInstanceAssignment>{uniqueId: '123', type: ZoneInstanceAssignmentType.COMPONENT_INSTANCES}); + jest.spyOn(component.zoneService, 'updateZoneInstanceAssignments').mockReturnValue(of(true)); + jest.spyOn(component.modeChange, 'emit'); + + component.setMode(ZoneInstanceMode.TAG); + + expect(component.zoneService.updateZoneInstanceAssignments).toHaveBeenCalled(); + expect(component.modeChange.emit).toHaveBeenCalledWith({instance: component.zoneInstance, newMode: ZoneInstanceMode.NONE }); + }); + + it('on save error, temporary assignment list is reverted to saved assignments', ()=> { + component.isActive = true; + component.zoneInstance = createPolicyInstance(); + component.zoneService = component.policiesService; + component.zoneInstance.mode = ZoneInstanceMode.TAG; + component.zoneInstance.assignments.push(<IZoneInstanceAssignment>{uniqueId: '123', type: ZoneInstanceAssignmentType.COMPONENT_INSTANCES}); + jest.spyOn(component.zoneService, 'updateZoneInstanceAssignments').mockReturnValue(_throw({status: 404})); + + component.setMode(ZoneInstanceMode.TAG); + + expect(component.zoneInstance.assignments.length).toEqual(0); + }); + + it('on save success, all changes are saved to zoneInstance.savedAssignments', ()=> { + component.isActive = true; + component.zoneInstance = createPolicyInstance(); + component.zoneService = component.policiesService; + component.zoneInstance.mode = ZoneInstanceMode.TAG; + component.zoneInstance.assignments.push(<IZoneInstanceAssignment>{uniqueId: '123', type: ZoneInstanceAssignmentType.COMPONENT_INSTANCES}); + jest.spyOn(component.zoneService, 'updateZoneInstanceAssignments').mockReturnValue(of(true)); + + component.setMode(ZoneInstanceMode.TAG); + + expect(component.zoneInstance.instanceData.getSavedAssignments().length).toEqual(1); + }); +}); diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-instance/zone-instance.component.ts b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-instance/zone-instance.component.ts new file mode 100644 index 0000000000..1b1363e576 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zone-instance/zone-instance.component.ts @@ -0,0 +1,128 @@ +import { Component, Input, Output, EventEmitter, ViewEncapsulation, OnInit, SimpleChange, ElementRef, ViewChild, SimpleChanges } from '@angular/core'; +import { + ZoneInstance, ZoneInstanceMode, ZoneInstanceType, + IZoneInstanceAssignment +} from 'app/models/graph/zones/zone-instance'; +import { PoliciesService } from 'app/ng2/services/policies.service'; +import { GroupsService } from 'app/ng2/services/groups.service'; +import { IZoneService } from "app/models/graph/zones/zone"; +import { EventListenerService } from 'app/services'; +import { GRAPH_EVENTS } from 'app/utils'; +import { Subject } from 'rxjs'; +import { Store } from "@ngxs/store"; +import { CompositionService } from "app/ng2/pages/composition/composition.service"; +import { PolicyInstance } from "app/models"; +import {SelectedComponentType, SetSelectedComponentAction} from "../../../common/store/graph.actions"; + + +@Component({ + selector: 'zone-instance', + templateUrl: './zone-instance.component.html', + styleUrls: ['./zone-instance.component.less'], + encapsulation: ViewEncapsulation.None +}) +export class ZoneInstanceComponent implements OnInit { + + @Input() zoneInstance:ZoneInstance; + @Input() defaultIconText:string; + @Input() isActive:boolean; + @Input() isViewOnly:boolean; + @Input() activeInstanceMode: ZoneInstanceMode; + @Input() hidden:boolean; + @Input() forceSave:Subject<Function>; + @Output() modeChange: EventEmitter<any> = new EventEmitter<any>(); + @Output() assignmentSaveStart: EventEmitter<void> = new EventEmitter<void>(); + @Output() assignmentSaveComplete: EventEmitter<boolean> = new EventEmitter<boolean>(); + @Output() tagHandleClick: EventEmitter<ZoneInstance> = new EventEmitter<ZoneInstance>(); + @ViewChild('currentComponent') currentComponent: ElementRef; + private MODE = ZoneInstanceMode; + private zoneService:IZoneService; + + constructor(private policiesService:PoliciesService, private groupsService:GroupsService, private eventListenerService:EventListenerService, private compositionService:CompositionService, private store:Store){} + + ngOnInit(){ + if(this.zoneInstance.type == ZoneInstanceType.POLICY){ + this.zoneService = this.policiesService; + } else { + this.zoneService = this.groupsService; + } + if(this.forceSave) { + this.forceSave.subscribe((afterSaveFunction:Function) => { + this.setMode(ZoneInstanceMode.TAG, null, afterSaveFunction); + }) + } + } + + ngOnChanges(changes:SimpleChanges) { + if(changes.hidden){ + this.currentComponent.nativeElement.scrollIntoView({behavior: "smooth", block: "nearest", inline:"end"}); + } + } + + ngOnDestroy() { + if(this.forceSave) { + this.forceSave.unsubscribe(); + } + } + + private setMode = (mode:ZoneInstanceMode, event?:any, afterSaveCallback?:Function):void => { + + if(event){ //prevent event from handle and then repeat event from zone instance + event.stopPropagation(); + } + + if(!this.isActive && this.activeInstanceMode === ZoneInstanceMode.TAG) { + return; //someone else is tagging. No events allowed + } + + if(this.isActive && this.zoneInstance.mode === ZoneInstanceMode.TAG){ + if(mode !== ZoneInstanceMode.TAG) { + return; //ignore all other events. The only valid option is saving changes. + } + + let oldAssignments:Array<IZoneInstanceAssignment> = this.zoneInstance.instanceData.getSavedAssignments(); + if(this.zoneInstance.isZoneAssignmentChanged(oldAssignments, this.zoneInstance.assignments)) { + + this.assignmentSaveStart.emit(); + + this.zoneService.updateZoneInstanceAssignments(this.zoneInstance.parentComponentType, this.zoneInstance.parentComponentID, this.zoneInstance.instanceData.uniqueId, this.zoneInstance.assignments).subscribe( + (success) => { + this.zoneInstance.instanceData.setSavedAssignments(this.zoneInstance.assignments); + + if(this.zoneInstance.instanceData instanceof PolicyInstance){ + this.compositionService.updatePolicy(this.zoneInstance.instanceData); + this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_POLICY_INSTANCE_UPDATE, this.zoneInstance.instanceData); + this.store.dispatch(new SetSelectedComponentAction({component: this.zoneInstance.instanceData, type: SelectedComponentType.POLICY})); + } else { + this.compositionService.updateGroup(this.zoneInstance.instanceData); + this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_GROUP_INSTANCE_UPDATE, this.zoneInstance.instanceData); + this.store.dispatch(new SetSelectedComponentAction({component: this.zoneInstance.instanceData, type: SelectedComponentType.GROUP})); + } + + this.assignmentSaveComplete.emit(true); + if(afterSaveCallback) afterSaveCallback(); + }, (error) => { + this.zoneInstance.assignments = oldAssignments; + this.assignmentSaveComplete.emit(false); + }); + } else { + if(afterSaveCallback) afterSaveCallback(); + } + this.modeChange.emit({newMode: ZoneInstanceMode.NONE, instance: this.zoneInstance}); + // this.store.dispatch(new unsavedChangesActions.RemoveUnsavedChange(this.zoneInstance.instanceData.uniqueId)); + + + } else { + this.modeChange.emit({newMode: mode, instance: this.zoneInstance}); + if(mode == ZoneInstanceMode.TAG){ + // this.store.dispatch(new unsavedChangesActions.AddUnsavedChange(this.zoneInstance.instanceData.uniqueId)); + } + } + } + + private tagHandleClicked = (event:Event) => { + this.tagHandleClick.emit(this.zoneInstance); + event.stopPropagation(); + }; + +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zones-module.ts b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zones-module.ts new file mode 100644 index 0000000000..3287c01f5a --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/canvas-zone/zones-module.ts @@ -0,0 +1,15 @@ +import { NgModule } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { ZoneContainerComponent } from "./zone-container.component"; +import { ZoneInstanceComponent } from "./zone-instance/zone-instance.component"; +import { SdcUiComponentsModule } from "onap-ui-angular"; + +@NgModule({ + declarations: [ZoneContainerComponent, ZoneInstanceComponent], + imports: [CommonModule, SdcUiComponentsModule], + entryComponents: [ZoneContainerComponent, ZoneInstanceComponent], + exports: [ZoneContainerComponent, ZoneInstanceComponent], + providers: [] +}) +export class ZoneModules { +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/common/common-graph-utils.ts b/catalog-ui/src/app/ng2/pages/composition/graph/common/common-graph-utils.ts new file mode 100644 index 0000000000..bfc540e97e --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/common/common-graph-utils.ts @@ -0,0 +1,304 @@ +/*- + * ============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========================================================= + */ + +import * as _ from "lodash"; +import { + CommonNodeBase, + Relationship, + CompositionCiNodeBase +} from "app/models"; +import {CompositionCiServicePathLink} from "app/models/graph/graph-links/composition-graph-links/composition-ci-service-path-link"; +import {Requirement, Capability} from "app/models"; +import {Injectable} from "@angular/core"; + + + +@Injectable() +export class CommonGraphUtils { + + constructor() { + + } + + public safeApply = (scope:ng.IScope, fn:any) => { //todo remove to general utils + let phase = scope.$root.$$phase; + if (phase == '$apply' || phase == '$digest') { + if (fn && (typeof(fn) === 'function')) { + fn(); + } + } else { + scope.$apply(fn); + } + }; + + /** + * Draw node on the graph + * @param cy + * @param compositionGraphNode + * @param position + * @returns {CollectionElements} + */ + public addNodeToGraph(cy:Cy.Instance, compositionGraphNode:CommonNodeBase, position?:Cy.Position):Cy.CollectionElements { + + let node = cy.add(<Cy.ElementDefinition> { + group: 'nodes', + position: position, + data: compositionGraphNode, + classes: compositionGraphNode.classes + }); + + this.initNodeTooltip(node); + return node; + }; + + /** + * The function will create a component instance node by the componentInstance position. + * If the node is UCPE the function will create all cp lan&wan for the ucpe + * @param cy + * @param compositionGraphNode + * @returns {Cy.CollectionElements} + */ + public addComponentInstanceNodeToGraph(cy:Cy.Instance, compositionGraphNode:CompositionCiNodeBase):Cy.CollectionElements { + + let nodePosition = { + x: +compositionGraphNode.componentInstance.posX, + y: +compositionGraphNode.componentInstance.posY + }; + + let node = this.addNodeToGraph(cy, compositionGraphNode, nodePosition); + return node; + }; + + /** + * Add service path link to graph - only draw the link + * @param cy + * @param link + */ + public insertServicePathLinkToGraph = (cy:Cy.Instance, link:CompositionCiServicePathLink) => { + let linkElement = cy.add({ + group: 'edges', + data: link, + classes: link.classes + }); + this.initServicePathTooltip(linkElement, link); + }; + + /** + * Returns function for the link tooltip content + * @param {Relationship} linkRelation + * @param {Requirement} requirement + * @param {Capability} capability + * @returns {() => string} + * @private + */ + private _getLinkTooltipContent(linkRelation:Relationship, requirement?:Requirement, capability?:Capability):string { + return '<div class="line">' + + '<span class="req-cap-label">R: </span>' + + '<span>' + (requirement ? requirement.getTitle() : linkRelation.relation.requirement) + '</span>' + + '</div>' + + '<div class="line">' + + '<div class="sprite-new link-tooltip-arrow"></div>' + + '<span class="req-cap-label">C: </span>' + + '<span>' + (capability ? capability.getTitle() : linkRelation.relation.capability) + '</span>' + + '</div>'; + } + + /** + * This function will init qtip tooltip on the link + * @param linkElement - the link we want the tooltip to apply on, + * @param link + * @param getLinkRequirementCapability + * link - the link obj + */ + public initLinkTooltip(linkElement:Cy.CollectionElements, link:Relationship, getLinkRequirementCapability:Function) { + const content = () => this._getLinkTooltipContent(link); // base tooltip content without owner names + const render = (event, api) => { + // on render (called once at first show), get the link requirement and capability and change to full tooltip content (with owner names) + getLinkRequirementCapability().then((linkReqCap) => { + const fullContent = () => this._getLinkTooltipContent(link, linkReqCap.requirement, linkReqCap.capability); + api.set('content.text', fullContent); + }); + }; + linkElement.qtip(this.prepareInitTooltipData({content, events: {render}})); + }; + + /** + * + * @param linkElement + * @param link + */ + public initServicePathTooltip(linkElement:Cy.CollectionElements, link:CompositionCiServicePathLink) { + let content = function () { + return '<div class="line">' + + '<div>' + link.pathName + '</div>' + + '</div>'; + }; + linkElement.qtip(this.prepareInitTooltipData({content})); + }; + + private prepareInitTooltipData(options?:Object) { + return _.merge({ + position: { + my: 'top center', + at: 'bottom center', + adjust: {x: 0, y: 0}, + effect: false + }, + style: { + classes: 'qtip-dark qtip-rounded qtip-custom link-qtip', + tip: { + width: 16, + height: 8 + } + }, + show: { + event: 'mouseover', + delay: 1000 + }, + hide: {event: 'mouseout mousedown'}, + includeLabels: true, + events: {} + }, options); + } + + public HTMLCoordsToCytoscapeCoords(cytoscapeBoundingBox:Cy.Extent, mousePos:Cy.Position):Cy.Position { + return {x: mousePos.x + cytoscapeBoundingBox.x1, y: mousePos.y + cytoscapeBoundingBox.y1} + }; + + + public getCytoscapeNodePosition = (cy:Cy.Instance, event:DragEvent | MouseEvent):Cy.Position => { + let targetOffset = $(event.target).offset(); + if(event instanceof DragEvent) { + targetOffset = $('canvas').offset(); + } + + let x = (event.pageX - targetOffset.left) / cy.zoom(); + let y = (event.pageY - targetOffset.top) / cy.zoom(); + + return this.HTMLCoordsToCytoscapeCoords(cy.extent(), { + x: x, + y: y + }); + }; + + + public getNodePosition(node:Cy.CollectionFirstNode):Cy.Position { + let nodePosition = node.relativePoint(); + if (node.data().isUcpe) { //UCPEs use bounding box and not relative point. + nodePosition = {x: node.boundingbox().x1, y: node.boundingbox().y1}; + } + + return nodePosition; + } + + /** + * Generic function that can be used for any html elements overlaid on canvas + * Returns the html position of a node on canvas, including left palette and header offsets. Option to pass in additional offset to add to return position. + * @param node + * @param additionalOffset + * @returns {Cy.Position} + + public getNodePositionWithOffset = (node:Cy.CollectionFirstNode, additionalOffset?:Cy.Position): Cy.Position => { + if(!additionalOffset) additionalOffset = {x: 0, y:0}; + + let nodePosition = node.renderedPosition(); + let posWithOffset:Cy.Position = { + x: nodePosition.x + GraphUIObjects.DIAGRAM_PALETTE_WIDTH_OFFSET + additionalOffset.x, + y: nodePosition.y + GraphUIObjects.COMPOSITION_HEADER_OFFSET + additionalOffset.y + }; + return posWithOffset; + };*/ + + /** + * return true/false if first node contains in second - this used in order to verify is node is entirely inside ucpe + * @param firstBox + * @param secondBox + * @returns {boolean} + */ + public isFirstBoxContainsInSecondBox(firstBox:Cy.BoundingBox, secondBox:Cy.BoundingBox) { + + return firstBox.x1 > secondBox.x1 && firstBox.x2 < secondBox.x2 && firstBox.y1 > secondBox.y1 && firstBox.y2 < secondBox.y2; + + }; + + /** + * + * @param cy + * @param node + * @returns {Array} + */ + public getLinkableNodes(cy:Cy.Instance, node:Cy.CollectionFirstNode):Array<CompositionCiNodeBase> { + let compatibleNodes = []; + _.each(cy.nodes(), (tempNode)=> { + if (this.nodeLocationsCompatible(node, tempNode)) { + compatibleNodes.push(tempNode.data()); + } + }); + return compatibleNodes; + } + + /** + * Checks whether node locations are compatible in reference to UCPEs. + * Returns true if both nodes are in UCPE or both nodes out, or one node is UCPEpart. + * @param node1 + * @param node2 + */ + public nodeLocationsCompatible(node1:Cy.CollectionFirstNode, node2:Cy.CollectionFirstNode) { + return (this.isFirstBoxContainsInSecondBox(node1.boundingbox(), node2.boundingbox())); + } + + /** + * This function will init qtip tooltip on the node + * @param node - the node we want the tooltip to apply on + */ + public initNodeTooltip(node:Cy.CollectionNodes) { + + let opts = { + content: function () { + return this.data('name'); + }, + position: { + my: 'top center', + at: 'bottom center', + adjust: {x: 0, y: -5} + }, + style: { + classes: 'qtip-dark qtip-rounded qtip-custom', + tip: { + width: 16, + height: 8 + } + }, + show: { + event: 'mouseover', + delay: 1000 + }, + hide: {event: 'mouseout mousedown'}, + includeLabels: true + }; + + if (node.data().isUcpePart) { //fix tooltip positioning for UCPE-cps + opts.position.adjust = {x: 0, y: 20}; + } + + node.qtip(opts); + }; +} + diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/common/image-creator.service.ts b/catalog-ui/src/app/ng2/pages/composition/graph/common/image-creator.service.ts new file mode 100644 index 0000000000..2be92c782b --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/common/image-creator.service.ts @@ -0,0 +1,92 @@ +/*- + * ============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 {Injectable} from "@angular/core"; + +export interface ICanvasImage { + src: string; + width: number + height: number; + x: number; + y: number; +} + +@Injectable() +export class ImageCreatorService { + + private _canvas:HTMLCanvasElement; + + constructor() { + this._canvas = <HTMLCanvasElement>$('<canvas>')[0]; + this._canvas.setAttribute('style', 'display:none'); + + let body = document.getElementsByTagName('body')[0]; + body.appendChild(this._canvas); + } + + /** + * Create an image composed of different image layers + * @param canvasImages + * @param canvasWidth + * @param canvasHeight + * returns a PROMISE + */ + getMultiLayerBase64Image(canvasImages: ICanvasImage[], canvasWidth?:number, canvasHeight?:number):Promise<string> { + + var promise = new Promise<string>((resolve, reject) => { + if(canvasImages && canvasImages.length === 0){ + return null; + } + + //If only width was set, use it for height, otherwise use first canvasImage height + canvasHeight = canvasHeight || canvasImages[0].height; + canvasWidth = canvasWidth || canvasImages[0].width; + + const images = []; + let imagesLoaded = 0; + const onImageLoaded = () => { + imagesLoaded++; + if(imagesLoaded < canvasImages.length){ + return; + } + this._canvas.setAttribute('width', (canvasWidth * 4).toString()); + this._canvas.setAttribute('height', (canvasHeight * 4).toString()); + const canvasCtx = this._canvas.getContext('2d'); + canvasCtx.scale(4,4); + canvasCtx.clearRect(0, 0, this._canvas.width, this._canvas.height); + images.forEach((image, index) => { + const canvasImage = canvasImages[index]; + canvasCtx.drawImage(image, canvasImage.x, canvasImage.y, canvasImage.width, canvasImage.height); + }); + + let base64Image = this._canvas.toDataURL(); + resolve(base64Image) + }; + canvasImages.forEach(canvasImage => { + let image = new Image(); + image.onload = onImageLoaded; + image.src = canvasImage.src; + images.push(image); + }); + }); + + return promise; + } +} diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/common/style/component-instances-nodes-style.spec.ts b/catalog-ui/src/app/ng2/pages/composition/graph/common/style/component-instances-nodes-style.spec.ts new file mode 100644 index 0000000000..54b3dbed24 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/common/style/component-instances-nodes-style.spec.ts @@ -0,0 +1,37 @@ +import {async} from "@angular/core/testing"; +import {ComponentInstanceNodesStyle} from "./component-instances-nodes-style"; + + +describe('component instance nodes style component', () => { + + beforeEach( + async(() => { + const createElement = document.createElement.bind(document); + document.createElement = (tagName) => { + if (tagName === 'canvas') { + return { + getContext: () => ({ + font: "", + measureText: (x) => ({width: x.length}) + }), + }; + } + return createElement(tagName); + }; + }) + ); + + it('verify getGraphDisplayName for String.length smaller than 67 chars', () => { + let inputString = 'SomeText'; + let expectedRes = inputString; + let res = ComponentInstanceNodesStyle.getGraphDisplayName(inputString); + expect(res).toBe(expectedRes); + }); + + it('verify getGraphDisplayName for String.length greater than 67 chars', () => { + let inputString = 'AAAAAAAAAABBBBBBBBBBCCCCCCCCCCDDDDDDDDDDEEEEEEEEEEFFFFFFFFFFGGGGGGGGGG12345678'; + let expectedRes = 'AAAAAAAAAABBBBBBBBBBCCCCCCCCCCDDDDDDDDDDEEEEEEEEEEFFFFFFFFF...'; + let res = ComponentInstanceNodesStyle.getGraphDisplayName(inputString); + expect(res).toBe(expectedRes); + }); +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/common/style/component-instances-nodes-style.ts b/catalog-ui/src/app/ng2/pages/composition/graph/common/style/component-instances-nodes-style.ts new file mode 100644 index 0000000000..cc9cac16e6 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/common/style/component-instances-nodes-style.ts @@ -0,0 +1,362 @@ +/*- + * ============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========================================================= + */ + +import { GraphColors, GraphUIObjects} from "app/utils/constants"; +import constant = require("lodash/constant"); +import {ImagesUrl} from "app/utils/constants"; +import {AngularJSBridge} from "app/services/angular-js-bridge-service"; +import { CanvasHandleTypes } from "app/utils"; +/** + * Created by obarda on 12/18/2016. + */ +export class ComponentInstanceNodesStyle { + + public static getCompositionGraphStyle = ():Array<Cy.Stylesheet> => { + return [ + { + selector: 'core', + css: { + 'shape': 'rectangle', + 'active-bg-size': 0, + 'selection-box-color': 'rgb(0, 159, 219)', + 'selection-box-opacity': 0.2, + 'selection-box-border-color': '#009fdb', + 'selection-box-border-width': 1 + + } + }, + { + selector: 'node', + css: { + 'font-family': 'OpenSans-Regular,sans-serif', + + 'font-size': 14, + 'events': 'yes', + 'text-events': 'yes', + 'text-border-width': 15, + 'text-border-color': GraphColors.NODE_UCPE, + 'text-margin-y': 5 + } + }, + { + selector: '.vf-node', + css: { + 'background-color': 'transparent', + 'shape': 'rectangle', + 'label': 'data(displayName)', + 'background-image': 'data(img)', + 'width': GraphUIObjects.DEFAULT_RESOURCE_WIDTH, + 'height': GraphUIObjects.DEFAULT_RESOURCE_WIDTH, + 'background-opacity': 0, + "background-width": GraphUIObjects.DEFAULT_RESOURCE_WIDTH, + "background-height": GraphUIObjects.DEFAULT_RESOURCE_WIDTH, + 'text-valign': 'bottom', + 'text-halign': 'center', + 'background-fit': 'cover', + 'background-clip': 'node', + 'overlay-color': GraphColors.NODE_BACKGROUND_COLOR, + 'overlay-opacity': 0 + } + }, + + { + selector: '.service-node', + css: { + 'background-color': 'transparent', + 'label': 'data(displayName)', + 'events': 'yes', + 'text-events': 'yes', + 'background-image': 'data(img)', + 'width': 64, + 'height': 64, + "border-width": 0, + 'text-valign': 'bottom', + 'text-halign': 'center', + 'background-opacity': 0, + 'overlay-color': GraphColors.NODE_BACKGROUND_COLOR, + 'overlay-opacity': 0 + } + }, + { + selector: '.cp-node', + css: { + 'background-color': 'rgb(255,255,255)', + 'shape': 'rectangle', + 'label': 'data(displayName)', + 'background-image': 'data(img)', + 'background-width': GraphUIObjects.SMALL_RESOURCE_WIDTH, + 'background-height': GraphUIObjects.SMALL_RESOURCE_WIDTH, + 'width': GraphUIObjects.SMALL_RESOURCE_WIDTH + GraphUIObjects.HANDLE_SIZE, + 'height': GraphUIObjects.SMALL_RESOURCE_WIDTH + GraphUIObjects.HANDLE_SIZE/2, + 'background-position-x': GraphUIObjects.HANDLE_SIZE / 2, + 'background-position-y': GraphUIObjects.HANDLE_SIZE / 2, + 'text-valign': 'bottom', + 'text-halign': 'center', + 'background-opacity': 0, + 'overlay-color': GraphColors.NODE_BACKGROUND_COLOR, + 'overlay-opacity': 0 + } + }, + { + selector: '.vl-node', + css: { + 'background-color': 'rgb(255,255,255)', + 'shape': 'rectangle', + 'label': 'data(displayName)', + 'background-image': 'data(img)', + 'background-width': GraphUIObjects.SMALL_RESOURCE_WIDTH, + 'background-height': GraphUIObjects.SMALL_RESOURCE_WIDTH, + 'background-position-x': GraphUIObjects.HANDLE_SIZE / 2, + 'background-position-y': GraphUIObjects.HANDLE_SIZE / 2, + 'width': GraphUIObjects.SMALL_RESOURCE_WIDTH + GraphUIObjects.HANDLE_SIZE, + 'height': GraphUIObjects.SMALL_RESOURCE_WIDTH + GraphUIObjects.HANDLE_SIZE / 2, + 'text-valign': 'bottom', + 'text-halign': 'center', + 'background-opacity': 0, + 'overlay-color': GraphColors.NODE_BACKGROUND_COLOR, + 'overlay-opacity': 0 + } + }, + { + selector: '.ucpe-cp', + css: { + 'background-color': GraphColors.NODE_UCPE_CP, + 'background-width': 15, + 'background-height': 15, + 'width': 15, + 'height': 15, + 'text-halign': 'center', + 'overlay-opacity': 0, + 'label': 'data(displayName)', + 'text-valign': 'data(textPosition)', + 'text-margin-y': (ele:Cy.Collection) => { + return (ele.data('textPosition') == 'top') ? -5 : 5; + }, + 'font-size': 12 + } + }, + { + selector: '.ucpe-node', + css: { + 'background-fit': 'cover', + 'padding-bottom': 0, + 'padding-top': 0 + } + }, + { + selector: '.simple-link', + css: { + 'width': 1, + 'line-color': GraphColors.BASE_LINK, + 'target-arrow-color': '#3b7b9b', + 'target-arrow-shape': 'triangle', + 'curve-style': 'bezier', + 'control-point-step-size': 30 + } + }, + { + selector: '.vl-link', + css: { + 'width': 3, + 'line-color': GraphColors.VL_LINK, + 'curve-style': 'bezier', + 'control-point-step-size': 30 + } + }, + { + selector: '.vl-link-1', + css: { + 'width': 3, + 'line-color': GraphColors.ACTIVE_LINK, + 'curve-style': 'unbundled-bezier', + 'target-arrow-color': '#3b7b9b', + 'target-arrow-shape': 'triangle', + 'control-point-step-size': 30 + } + }, + { + selector: '.ucpe-host-link', + css: { + 'width': 0 + } + }, + { + selector: '.not-certified-link', + css: { + 'width': 1, + 'line-color': GraphColors.NOT_CERTIFIED_LINK, + 'curve-style': 'bezier', + 'control-point-step-size': 30, + 'line-style': 'dashed', + 'target-arrow-color': '#3b7b9b', + 'target-arrow-shape': 'triangle' + + } + }, + + { + selector: '.service-path-link', + css: { + 'width': 2, + 'line-color': GraphColors.SERVICE_PATH_LINK, + 'target-arrow-color': GraphColors.SERVICE_PATH_LINK, + 'target-arrow-shape': 'triangle', + 'curve-style': 'bezier', + 'control-point-step-size': 30 + } + }, + { + selector: '.not-certified', + css: { + 'shape': 'rectangle', + 'background-image': (ele:Cy.Collection) => { + // return ele.data().setUncertifiedImageBgStyle(ele, GraphUIObjects.NODE_OVERLAP_MIN_SIZE);//Change name to setUncertifiedImageBgStyle?? + return ele.data().initUncertifiedImage(ele, GraphUIObjects.NODE_OVERLAP_MIN_SIZE); + }, + 'border-width': 0 + } + }, + { + selector: '.dependent', + css: { + 'shape': 'rectangle', + 'background-image': (ele:Cy.Collection) => { + return ele.data().initDependentImage(ele, GraphUIObjects.NODE_OVERLAP_MIN_SIZE) + }, + 'border-width': 0 + } + }, + { + selector: '.dependent.not-certified', + css: { + 'shape': 'rectangle', + 'background-image': (ele:Cy.Collection) => { + return ele.data().initUncertifiedDependentImage(ele, GraphUIObjects.NODE_OVERLAP_MIN_SIZE) + }, + 'border-width': 0 + } + }, + { + selector: 'node:selected', + css: { + "border-width": 2, + "border-color": GraphColors.NODE_SELECTED_BORDER_COLOR, + 'shape': 'rectangle' + } + }, + { + selector: 'edge:selected', + css: { + 'line-color': GraphColors.ACTIVE_LINK + + } + }, + { + selector: 'edge:active', + css: { + 'overlay-opacity': 0 + } + }, { + selector: '.configuration-node', + css: { + 'background-color': 'rgb(255,255,255)', + 'shape': 'rectangle', + 'label': 'data(displayName)', + 'background-image': 'data(img)', + 'background-width': GraphUIObjects.SMALL_RESOURCE_WIDTH, + 'background-height': GraphUIObjects.SMALL_RESOURCE_WIDTH, + 'background-position-x': GraphUIObjects.HANDLE_SIZE / 2, + 'background-position-y': GraphUIObjects.HANDLE_SIZE / 2, + 'width': GraphUIObjects.SMALL_RESOURCE_WIDTH + GraphUIObjects.HANDLE_SIZE, + 'height': GraphUIObjects.SMALL_RESOURCE_WIDTH + GraphUIObjects.HANDLE_SIZE/2, + 'text-valign': 'bottom', + 'text-halign': 'center', + 'background-opacity': 0, + 'overlay-color': GraphColors.NODE_BACKGROUND_COLOR, + 'overlay-opacity': 0 + } + }, + { + selector: '.archived', + css: { + 'shape': 'rectangle', + 'background-image': (ele:Cy.Collection) => { + return ele.data().setArchivedImageBgStyle(ele, GraphUIObjects.NODE_OVERLAP_MIN_SIZE); //Change name to setArchivedImageBgStyle ?? + }, + "border-width": 0 + } + } + ] + } + + public static getAddEdgeHandle = () => { + return { + + single: false, + type: CanvasHandleTypes.ADD_EDGE, + imageUrl: AngularJSBridge.getAngularConfig().imagesPath + ImagesUrl.CANVAS_PLUS_ICON, + lineColor: '#27a337', + lineWidth: 2, + lineStyle: 'dashed' + + } + } + + public static getTagHandle = () => { + return { + single: false, + type: CanvasHandleTypes.TAG_AVAILABLE, + imageUrl: AngularJSBridge.getAngularConfig().imagesPath + ImagesUrl.CANVAS_TAG_ICON, + } + } + + public static getTaggedPolicyHandle = () => { + return { + single: false, + type: CanvasHandleTypes.TAGGED_POLICY, + imageUrl: AngularJSBridge.getAngularConfig().imagesPath + ImagesUrl.CANVAS_POLICY_TAGGED_ICON, + } + } + + public static getTaggedGroupHandle = () => { + return { + single: false, + type: CanvasHandleTypes.TAGGED_GROUP, + imageUrl: AngularJSBridge.getAngularConfig().imagesPath + ImagesUrl.CANVAS_GROUP_TAGGED_ICON, + } + } + + public static getGraphDisplayName(name:string):string { + let context = document.createElement("canvas").getContext("2d"); + context.font = "13px Arial"; + + if (67 < context.measureText(name).width) { + let newLen = name.length - 3; + let newName = name.substring(0, newLen); + + while (59 < (context.measureText(newName).width)) { + newName = newName.substring(0, (--newLen)); + } + return newName + '...'; + } + return name; + } + +} diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/common/style/module-node-style.ts b/catalog-ui/src/app/ng2/pages/composition/graph/common/style/module-node-style.ts new file mode 100644 index 0000000000..bf71e1c868 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/common/style/module-node-style.ts @@ -0,0 +1,103 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +import {GraphColors} from "app/utils"; +export class ModulesNodesStyle { + + public static getModuleGraphStyle = ():Array<Cy.Stylesheet> => { + + return [ + { + selector: '.cy-expand-collapse-collapsed-node', + css: { + 'background-image': 'data(img)', + 'width': 34, + 'height': 32, + 'background-opacity': 0, + 'shape': 'rectangle', + 'label': 'data(displayName)', + 'events': 'yes', + 'text-events': 'yes', + 'text-valign': 'bottom', + 'text-halign': 'center', + 'text-margin-y': 5, + 'border-opacity': 0 + } + }, + { + selector: '.module-node', + css: { + 'background-color': 'transparent', + 'background-opacity': 0, + "border-width": 2, + "border-color": GraphColors.NODE_SELECTED_BORDER_COLOR, + 'border-style': 'dashed', + 'label': 'data(displayName)', + 'events': 'yes', + 'text-events': 'yes', + 'text-valign': 'bottom', + 'text-halign': 'center', + 'text-margin-y': 8 + } + }, + { + selector: 'node:selected', + css: { + "border-opacity": 0 + } + }, + { + selector: '.simple-link:selected', + css: { + 'line-color': GraphColors.BASE_LINK, + } + }, + { + selector: '.vl-link:selected', + css: { + 'line-color': GraphColors.VL_LINK, + } + }, + { + selector: '.cy-expand-collapse-collapsed-node:selected', + css: { + "border-color": GraphColors.NODE_SELECTED_BORDER_COLOR, + 'border-opacity': 1, + 'border-style': 'solid', + 'border-width': 2 + } + }, + { + selector: '.module-node:selected', + css: { + "border-color": GraphColors.NODE_SELECTED_BORDER_COLOR, + 'border-opacity': 1 + } + }, + { + selector: '.dummy-node', + css: { + 'width': 20, + 'height': 20 + } + }, + ] + } +} diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.component.html b/catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.component.html new file mode 100644 index 0000000000..5a0ca3e43f --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.component.html @@ -0,0 +1,57 @@ +<div class="sdc-composition-graph-wrapper {{zoneTagMode}}" + [ngClass]="{'with-sidebar': withSidebar$ | async, 'view-only':isViewOnly$ | async}"> +</div> + +<div class="sdc-composition-menu" [ngClass]="{'with-sidebar': withSidebar$ | async}"> + + <service-path-selector + *ngIf="topologyTemplate.isService() && compositionService.forwardingPaths" + [drawPath]="drawPathOnCy" + [deletePaths]="deletePathsOnCy" + [selectedPathId]="selectedPathId"> + </service-path-selector> + + <canvas-search *ngIf="componentInstanceNames" class="composition-search" + [placeholder]="'Type to search'" + [data]="componentInstanceNames" + (searchChanged)="getAutoCompleteValues($event)" + (searchButtonClicked)="highlightSearchMatches($event)"> + </canvas-search> + + <!--<service-path class="zoom-icons"--> + <!--*ngIf="!(isViewOnly$ | async) && topologyTemplate.isService()"--> + <!--[service]="topologyTemplate"--> + <!--[onCreate]="createOrUpdateServicePath">--> + <!--</service-path>--> + + <svg-icon *ngIf="!(isViewOnly$ | async) && topologyTemplate.isService()" class="zoom-icons" [mode]="'primary2'" [size]="'medium'" [backgroundShape]="'rectangle'" + [backgroundColor]="'silver'" [name]="'browse'" [clickable]="true" [testId]="'pathsMenuBtn'" + (click)="openServicePathMenu($event)"></svg-icon> + <svg-icon class="zoom-icons" [mode]="'primary2'" [size]="'medium'" [backgroundShape]="'rectangle'" + [backgroundColor]="'silver'" [name]="'expand-o'" [clickable]="true" + (click)="zoomAllWithoutSidebar()"></svg-icon> + <svg-icon class="zoom-icons" [mode]="'primary2'" [size]="'medium'" [backgroundShape]="'rectangle'" + [backgroundColor]="'silver'" [name]="'plus'" [clickable]="true" + (click)="zoom(true)"></svg-icon> + <svg-icon class="zoom-icons" [mode]="'primary2'" [size]="'medium'" [backgroundShape]="'rectangle'" + [backgroundColor]="'silver'" [name]="'minus'" [clickable]="true" + (click)="zoom(false)"></svg-icon> +</div> + +<div class="sdc-canvas-zones__wrapper {{zoneTagMode}}" [ngClass]="{'with-sidebar': withSidebar$ | async}"> + <zone-container *ngFor="let zone of zones" [title]="zone.title" [type]="zone.type" [count]="zone.instances.length" + [visible]="zone.visible" [minimized]="zone.minimized" (minimize)="zoneMinimizeToggle(zone.type)" + (backgroundClick)="zoneBackgroundClicked()"> + <zone-instance *ngFor="let instance of zone.instances" [hidden]="instance.hidden" + [zoneInstance]="instance" [defaultIconText]="zone.defaultIconText" + [isActive]="activeZoneInstance == instance" + [activeInstanceMode]="activeZoneInstance && activeZoneInstance.mode" + [isViewOnly]="isViewOnly$ | async" + [forceSave]="instance.forceSave" + (modeChange)="zoneInstanceModeChanged($event.newMode, $event.instance, zone.type)" + (tagHandleClick)="zoneInstanceTagged($event)" + (assignmentSaveStart)="zoneAssignmentSaveStart()" + (assignmentSaveComplete)="zoneAssignmentSaveComplete($event)"> + </zone-instance> + </zone-container> +</div>
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.component.less b/catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.component.less new file mode 100644 index 0000000000..b3e5ef3a0c --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.component.less @@ -0,0 +1,93 @@ +:host(composition-graph) { + flex: 1; + padding-top: 53px; +} + +.composition { + .custom-modal { + /* Hack solution to hide canvas tooltips under modals */ + z-index: 20000 !important; + } +} + +.sdc-composition-graph-wrapper { + height: 100%; + width: 100%; + + &.with-sidebar { + width: calc(~'100% - 300px'); + } +} + +.view-only { + background-color: rgb(248, 248, 248); +} + +.sdc-canvas-zones__wrapper { + position: absolute; + bottom: 10px; + right: 12px; + display: flex; + transition: right 0.2s; + + &.with-sidebar { + right: 310px; + } + + ng2-zone-container { + display: flex; + margin-left: 10px; + } +} + +.group-tagging { + cursor: url("../../../../../assets/styles/images/canvas-tagging-icons/group_1.svg"), pointer; +} + +.group-tagging-hover { + cursor: url("../../../../../assets/styles/images/canvas-tagging-icons/group_2.svg"), pointer; +} + +.policy-tagging { + cursor: url("../../../../../assets/styles/images/canvas-tagging-icons/policy_1.svg"), pointer; +} + +.policy-tagging-hover { + cursor: url("../../../../../assets/styles/images/canvas-tagging-icons/policy_2.svg"), pointer; +} + +//Canvas menu +.sdc-composition-menu { + position: absolute; + right: 18px; + top: 53px; + transition: right 0.2s; + display: flex; + flex-direction: column; + align-items: flex-end; + margin-right: 10px; + pointer-events: none; + + & > * { + pointer-events: all; + } + + &.with-sidebar { + right: 320px; + } + + .composition-search { + margin-top: 12px; + } + + .zoom-icons { + border: solid 1px #d2d2d2; + border-radius: 2px; + box-shadow: 0px 2px 3.88px 0.12px rgba(0, 0, 0, 0.29); + margin-top: 10px; + + /deep/ .svg-icon { + box-sizing: content-box; + } + } +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.component.spec.ts b/catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.component.spec.ts new file mode 100644 index 0000000000..9a15ecba69 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.component.spec.ts @@ -0,0 +1,354 @@ +import {NO_ERRORS_SCHEMA} from '@angular/core'; +import {async, ComponentFixture} from '@angular/core/testing'; +import {SdcUiServices} from 'onap-ui-angular'; +import 'rxjs/add/observable/of'; +import {ConfigureFn, configureTests} from '../../../../../jest/test-config.helper'; +import {CompositionGraphComponent} from "./composition-graph.component"; +import {WorkspaceService} from "../../workspace/workspace.service"; +import {ComponentInstance, GroupInstance, NodesFactory, ZoneInstance, ZoneInstanceMode} from "../../../../models"; +import {EventListenerService} from "../../../../services"; +import { + CompositionGraphGeneralUtils, + CompositionGraphNodesUtils, + CompositionGraphZoneUtils, + MatchCapabilitiesRequirementsUtils, ServicePathGraphUtils +} from "./utils"; +import {CompositionGraphLinkUtils} from "./utils/composition-graph-links-utils"; +import {ConnectionWizardService} from "./connection-wizard/connection-wizard.service"; +import {CommonGraphUtils} from "./common/common-graph-utils"; +import {CompositionGraphPaletteUtils} from "./utils/composition-graph-palette-utils"; +import {TopologyTemplateService} from "../../../services/component-services/topology-template.service"; +import {ComponentInstanceServiceNg2} from "../../../services/component-instance-services/component-instance.service"; +import {CompositionService} from "../composition.service"; +import {ModalService} from '../../../services/modal.service'; +import {Store} from '@ngxs/store'; +import {PoliciesService} from '../../../services/policies.service'; +import {GroupsService} from '../../../services/groups.service'; +import {PolicyInstance} from "../../../../models/graph/zones/policy-instance"; +import {ZoneInstanceType} from "../../../../models/graph/zones/zone-instance"; +import {GRAPH_EVENTS} from "../../../../utils/constants"; +import * as cytoscape from "cytoscape"; +import {ComponentMetadata} from "../../../../models/component-metadata"; +import {Zone} from "../../../../models/graph/zones/zone"; +import {SelectedComponentType, SetSelectedComponentAction} from "../common/store/graph.actions"; + +describe('composition graph component', () => { + + let fixture: ComponentFixture<CompositionGraphComponent>; + let instance: CompositionGraphComponent; + let eventServiceMock: Partial<EventListenerService>; + let compositionGraphZoneUtils: Partial<CompositionGraphZoneUtils>; + let generalGraphUtils: Partial<CompositionGraphGeneralUtils>; + let workspaceServiceMock: Partial<WorkspaceService>; + let policyService: Partial<PoliciesService>; + let storeStub; + let compositionGraphLinkUtils: Partial<CompositionGraphLinkUtils>; + let nodesGraphUtils: Partial<CompositionGraphNodesUtils>; + + let createPolicyInstance = () => { + let policy = new PolicyInstance(); + policy.targets = {COMPONENT_INSTANCES: [], GROUPS: []}; + return new ZoneInstance(policy, '', ''); + } + + beforeEach( + async(() => { + + eventServiceMock = { + notifyObservers: jest.fn(), + unRegisterObserver: jest.fn() + } + + compositionGraphZoneUtils = { + endCyTagMode: jest.fn(), + showZoneTagIndications: jest.fn(), + hideZoneTagIndications: jest.fn(), + hideGroupZoneIndications: jest.fn(), + showGroupZoneIndications: jest.fn(), + startCyTagMode: jest.fn() + } + + workspaceServiceMock = { + metadata: <ComponentMetadata>{ + uniqueId: 'service_unique_id', + componentType: 'SERVICE' + } + } + + compositionGraphLinkUtils = { + handleLinkClick: jest.fn(), + getModifyLinkMenu: jest.fn() + } + + storeStub = { + dispatch: jest.fn() + } + policyService = { + getSpecificPolicy: jest.fn() + } + + generalGraphUtils = { + zoomGraphTo: jest.fn() + } + + nodesGraphUtils = { + onNodesPositionChanged: jest.fn() + } + + const configure: ConfigureFn = (testBed) => { + testBed.configureTestingModule({ + declarations: [CompositionGraphComponent], + imports: [], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + {provide: NodesFactory, useValue: {}}, + {provide: EventListenerService, useValue: eventServiceMock}, + {provide: CompositionGraphZoneUtils, useValue: compositionGraphZoneUtils}, + {provide: CompositionGraphGeneralUtils, useValue: generalGraphUtils}, + {provide: CompositionGraphLinkUtils, useValue: compositionGraphLinkUtils}, + {provide: CompositionGraphNodesUtils, useValue: nodesGraphUtils}, + {provide: ConnectionWizardService, useValue: {}}, + {provide: CommonGraphUtils, useValue: {}}, + {provide: CompositionGraphPaletteUtils, useValue: {}}, + {provide: TopologyTemplateService, useValue: {}}, + {provide: ComponentInstanceServiceNg2, useValue: {}}, + {provide: MatchCapabilitiesRequirementsUtils, useValue: {}}, + {provide: CompositionService, useValue: {}}, + {provide: SdcUiServices.LoaderService, useValue: {}}, + {provide: WorkspaceService, useValue: workspaceServiceMock}, + {provide: SdcUiServices.NotificationsService, useValue: {}}, + {provide: SdcUiServices.simplePopupMenuService, useValue: {}}, + {provide: ServicePathGraphUtils, useValue: {}}, + {provide: ModalService, useValue: {}}, + {provide: PoliciesService, useValue: policyService}, + {provide: GroupsService, useValue: {}}, + {provide: Store, useValue: storeStub}, + ], + }); + }; + + configureTests(configure).then((testBed) => { + fixture = testBed.createComponent(CompositionGraphComponent); + instance = fixture.componentInstance; + instance._cy = cytoscape({}); + }); + }) + ); + + it('composition graph component should be defined', () => { + expect(fixture).toBeDefined(); + }); + + describe('on zone instance mode changed', () => { + let newZoneInstance: ZoneInstance; + + beforeEach( + async(() => { + newZoneInstance = createPolicyInstance(); + instance.zoneTagMode = null; + instance.zones = []; + instance.zones[ZoneInstanceType.POLICY] = new Zone('Policies', 'P', ZoneInstanceType.POLICY); + instance.zones[ZoneInstanceType.GROUP] = new Zone('Groups', 'G', ZoneInstanceType.GROUP); + instance.activeZoneInstance = createPolicyInstance(); + })) + + it('zone instance in tag mode and we want to turn tag mode off', () => { + instance.zoneTagMode = 'some_zone_id'; + instance.activeZoneInstance = newZoneInstance; + instance.zoneInstanceModeChanged(ZoneInstanceMode.NONE, newZoneInstance, ZoneInstanceType.POLICY); + expect(instance.eventListenerService.notifyObservers).toHaveBeenCalledWith(GRAPH_EVENTS.ON_CANVAS_TAG_END, newZoneInstance); + expect(instance.activeZoneInstance.mode).toBe(ZoneInstanceMode.SELECTED) + }) + + it('we are not in tag mode and policy instance mode changed to NONE - group and zone tag indication need to be removed', () => { + instance.zoneInstanceModeChanged(ZoneInstanceMode.NONE, newZoneInstance, ZoneInstanceType.POLICY); + expect(instance.compositionGraphZoneUtils.hideZoneTagIndications).toHaveBeenCalledWith(instance._cy); + expect(instance.compositionGraphZoneUtils.hideGroupZoneIndications).toHaveBeenCalledWith(instance.zones[ZoneInstanceType.GROUP].instances); + }) + + it('we are not in tag mode and active zone instance gets hover/none - we dont actually change mode', () => { + let newMode = ZoneInstanceMode.SELECTED; + instance.zoneInstanceModeChanged(newMode, newZoneInstance, ZoneInstanceType.POLICY); + expect(newZoneInstance.mode).toBe(newMode); + }) + + it('we are not in tag mode and zone instance mode changed to HOVER mode', () => { + instance.zoneInstanceModeChanged(ZoneInstanceMode.HOVER, newZoneInstance, ZoneInstanceType.POLICY); + expect(instance.compositionGraphZoneUtils.showZoneTagIndications).toHaveBeenCalledWith(instance._cy, newZoneInstance); + expect(instance.compositionGraphZoneUtils.showGroupZoneIndications).toHaveBeenCalledWith(instance.zones[ZoneInstanceType.GROUP].instances, newZoneInstance); + expect(instance.eventListenerService.notifyObservers).not.toHaveBeenCalled(); + }) + + it('we are not in tag mode and mode changed to SELECTED', () => { + instance.zoneInstanceModeChanged(ZoneInstanceMode.SELECTED, newZoneInstance, ZoneInstanceType.POLICY); + expect(instance.compositionGraphZoneUtils.showZoneTagIndications).toHaveBeenCalledWith(instance._cy, newZoneInstance); + expect(instance.compositionGraphZoneUtils.showGroupZoneIndications).toHaveBeenCalledWith(instance.zones[ZoneInstanceType.GROUP].instances, newZoneInstance); + expect(instance.activeZoneInstance).toBe(newZoneInstance); + expect(instance.eventListenerService.notifyObservers).toHaveBeenCalledWith(GRAPH_EVENTS.ON_ZONE_INSTANCE_SELECTED, newZoneInstance); + expect(instance.store.dispatch).toHaveBeenCalledWith(new SetSelectedComponentAction({ + component: newZoneInstance.instanceData, + type: SelectedComponentType[ZoneInstanceType[newZoneInstance.type]] + })); + expect(instance.eventListenerService.notifyObservers).not.toHaveBeenCalledWith(GRAPH_EVENTS.ON_CANVAS_TAG_START, ZoneInstanceType.POLICY); + }) + + + it('we are not in tag mode and and zone instance mode changed to TAG', () => { + instance.zoneInstanceModeChanged(ZoneInstanceMode.TAG, newZoneInstance, ZoneInstanceType.POLICY); + expect(instance.compositionGraphZoneUtils.showZoneTagIndications).toHaveBeenCalledWith(instance._cy, newZoneInstance); + expect(instance.compositionGraphZoneUtils.showGroupZoneIndications).toHaveBeenCalledWith(instance.zones[ZoneInstanceType.GROUP].instances, newZoneInstance); + expect(instance.activeZoneInstance).toBe(newZoneInstance); + expect(instance.eventListenerService.notifyObservers).toHaveBeenCalledWith(GRAPH_EVENTS.ON_ZONE_INSTANCE_SELECTED, newZoneInstance); + expect(instance.store.dispatch).toHaveBeenCalledWith(new SetSelectedComponentAction({ + component: newZoneInstance.instanceData, + type: SelectedComponentType[ZoneInstanceType[newZoneInstance.type]] + })); + expect(instance.compositionGraphZoneUtils.startCyTagMode).toHaveBeenCalledWith(instance._cy); + expect(instance.eventListenerService.notifyObservers).toHaveBeenCalledWith(GRAPH_EVENTS.ON_CANVAS_TAG_START, ZoneInstanceType.POLICY); + }) + }) + + it('unset active zone instance', () => { + instance.activeZoneInstance = createPolicyInstance(); + instance.unsetActiveZoneInstance(); + expect(instance.activeZoneInstance).toBeNull(); + expect(instance.zoneTagMode).toBeNull(); + }) + + it('zone background clicked - we are not in tag mode and active zone instance exist', () => { + instance.activeZoneInstance = createPolicyInstance(); + jest.spyOn(instance, 'unsetActiveZoneInstance'); + jest.spyOn(instance, 'selectTopologyTemplate'); + instance.zoneBackgroundClicked(); + expect(instance.unsetActiveZoneInstance).toHaveBeenCalled(); + expect(instance.selectTopologyTemplate).toHaveBeenCalled(); + }) + + it('zone background clicked - we are not in tag mode and no active zone instance exist', () => { + jest.spyOn(instance, 'unsetActiveZoneInstance'); + jest.spyOn(instance, 'selectTopologyTemplate'); + instance.zoneBackgroundClicked(); + expect(instance.unsetActiveZoneInstance).not.toHaveBeenCalled(); + expect(instance.selectTopologyTemplate).not.toHaveBeenCalled(); + }) + + it('on zoom in', () => { + jest.spyOn(instance, 'zoom'); + instance.zoom(true); + expect(instance.generalGraphUtils.zoomGraphTo).toHaveBeenCalledWith(instance._cy, instance._cy.zoom() + .1); + }) + + it('on zoom out', () => { + jest.spyOn(instance, 'zoom'); + instance.zoom(false); + expect(instance.generalGraphUtils.zoomGraphTo).toHaveBeenCalledWith(instance._cy, instance._cy.zoom() - .1); + }) + + describe('cytoscape tap end event have been called', () => { + + it('canvas background was clicked while zone instance in tag mode, zone instance still selected in tag mode)', () => { + let event = <Cy.EventObject>{cyTarget: instance._cy}; + instance.zoneTagMode = 'instance_in_tag' + instance.onTapEnd(event); + expect(instance.zoneTagMode).toBe('instance_in_tag'); + }) + + it('canvas background was clicked and no zone instance selected, topology template is now selected', () => { + let event = <Cy.EventObject>{cyTarget: instance._cy}; + jest.spyOn(instance, 'selectTopologyTemplate'); + instance.onTapEnd(event); + expect(instance.selectTopologyTemplate).toHaveBeenCalled(); + expect(instance.eventListenerService.notifyObservers).toHaveBeenCalledWith(GRAPH_EVENTS.ON_GRAPH_BACKGROUND_CLICKED); + }) + + it('canvas background was clicked and zone instance was selected, topology template is now selected and zone instance is unselected', () => { + let event = <Cy.EventObject>{cyTarget: instance._cy}; + instance.activeZoneInstance = createPolicyInstance(); + jest.spyOn(instance, 'selectTopologyTemplate'); + jest.spyOn(instance, 'unsetActiveZoneInstance'); + instance.onTapEnd(event); + expect(instance.selectTopologyTemplate).toHaveBeenCalled(); + expect(instance.unsetActiveZoneInstance).toHaveBeenCalled(); + }) + + + it('canvas background was clicked and zone instance was selected, topology template is now selected and zone instance is unselected', () => { + let event = <Cy.EventObject>{cyTarget: instance._cy}; + instance.activeZoneInstance = createPolicyInstance(); + jest.spyOn(instance, 'selectTopologyTemplate'); + jest.spyOn(instance, 'unsetActiveZoneInstance'); + instance.onTapEnd(event); + expect(instance.selectTopologyTemplate).toHaveBeenCalled(); + expect(instance.unsetActiveZoneInstance).toHaveBeenCalled(); + }) + + it('on simple edge clicked, open link menu and handle link click', () => { + let event = <Cy.EventObject>{ + cyTarget: [{ + isEdge: jest.fn().mockReturnValue(true), + data: jest.fn().mockReturnValue({type: 'simple'}) + } + }]; + instance.openModifyLinkMenu = jest.fn(); + instance.onTapEnd(event); + expect(instance.compositionGraphLinkUtils.handleLinkClick).toHaveBeenCalledWith(instance._cy, event); + expect(instance.openModifyLinkMenu).toHaveBeenCalled(); + }) + + it('on service path edge clicked, no menu is opened', () => { + let event = <Cy.EventObject>{ + cyTarget: [{ + isEdge: jest.fn().mockReturnValue(true), + data: jest.fn().mockReturnValue({type: 'service-path-link'}) + }] + }; + instance.openModifyLinkMenu = jest.fn(); + instance.onTapEnd(event); + expect(instance.compositionGraphLinkUtils.handleLinkClick).toHaveBeenCalledWith(instance._cy, event); + expect(instance.openModifyLinkMenu).not.toHaveBeenCalled(); + }) + + it('on drop after drag event (position has changed), call onNodesPositionChanged to update node position', () => { + let event = <Cy.EventObject>{ + cyTarget: [{ + isEdge: jest.fn().mockReturnValue(false), + position: jest.fn().mockReturnValue({x:2.11, y:2.44}) + }] + }; + instance.currentlyClickedNodePosition = <Cy.Position>{x:2.33, y:2.44}; + instance.onTapEnd(event); + let nodesMoved: Cy.CollectionNodes = instance._cy.$(':grabbed'); + expect(instance.nodesGraphUtils.onNodesPositionChanged).toHaveBeenCalledWith(instance._cy, instance.topologyTemplate, nodesMoved); + + }) + + it('on node clicked (position not changed) while zone instance selected, unset active zone and call set selected instance', () => { + let event = <Cy.EventObject>{ + cyTarget: [{ + isEdge: jest.fn().mockReturnValue(false), + position: jest.fn().mockReturnValue({x:2.11, y:2.44}), + data: jest.fn().mockReturnValue({componentInstance: new ComponentInstance()}) + }], + }; + instance.currentlyClickedNodePosition = <Cy.Position>{x:2.11, y:2.44}; + instance.activeZoneInstance = createPolicyInstance(); + jest.spyOn(instance, 'unsetActiveZoneInstance'); + jest.spyOn(instance, 'selectComponentInstance'); + instance.onTapEnd(event); + expect(instance.unsetActiveZoneInstance).toHaveBeenCalled(); + expect(instance.selectComponentInstance).toHaveBeenCalledWith(event.cyTarget[0].data().componentInstance); + }) + }) + + it('initial view mode will turn off all cytoscape events', () => { + jest.spyOn(instance, 'isViewOnly').mockReturnValue(true); + jest.spyOn(instance._cy, 'off'); + instance.initViewMode(); + expect(instance._cy.off).toHaveBeenCalledWith('drag'); + expect(instance._cy.off).toHaveBeenCalledWith('handlemouseout'); + expect(instance._cy.off).toHaveBeenCalledWith('handlemouseover'); + expect(instance._cy.off).toHaveBeenCalledWith('canvasredraw'); + expect(instance._cy.off).toHaveBeenCalledWith('handletagclick'); + + }) +}); diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.component.ts b/catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.component.ts new file mode 100644 index 0000000000..69ca3faaf5 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.component.ts @@ -0,0 +1,768 @@ +/** + * Created by ob0695 on 4/24/2018. + */ +import { AfterViewInit, Component, ElementRef, HostBinding, Input } from '@angular/core'; +import { Select, Store } from '@ngxs/store'; +import { + ButtonModel, + Component as TopologyTemplate, + ComponentInstance, + CompositionCiNodeBase, + ConnectRelationModel, + GroupInstance, + LeftPaletteComponent, + LinkMenu, + Match, + ModalModel, + NodesFactory, + Point, + PolicyInstance, + PropertyBEModel, + Relationship, + StepModel, + Zone, + ZoneInstance, + ZoneInstanceAssignmentType, + ZoneInstanceMode, + ZoneInstanceType +} from 'app/models'; +import { ForwardingPath } from 'app/models/forwarding-path'; +import { CompositionCiServicePathLink } from 'app/models/graph/graph-links/composition-graph-links/composition-ci-service-path-link'; +import { UIZoneInstanceObject } from 'app/models/ui-models/ui-zone-instance-object'; +import { CompositionService } from 'app/ng2/pages/composition/composition.service'; +import { CommonGraphUtils } from 'app/ng2/pages/composition/graph/common/common-graph-utils'; +import { ComponentInstanceNodesStyle } from 'app/ng2/pages/composition/graph/common/style/component-instances-nodes-style'; +import { ConnectionPropertiesViewComponent } from 'app/ng2/pages/composition/graph/connection-wizard/connection-properties-view/connection-properties-view.component'; +import { ConnectionWizardHeaderComponent } from 'app/ng2/pages/composition/graph/connection-wizard/connection-wizard-header/connection-wizard-header.component'; +import { ConnectionWizardService } from 'app/ng2/pages/composition/graph/connection-wizard/connection-wizard.service'; +import { FromNodeStepComponent } from 'app/ng2/pages/composition/graph/connection-wizard/from-node-step/from-node-step.component'; +import { PropertiesStepComponent } from 'app/ng2/pages/composition/graph/connection-wizard/properties-step/properties-step.component'; +import { ToNodeStepComponent } from 'app/ng2/pages/composition/graph/connection-wizard/to-node-step/to-node-step.component'; +import { WorkspaceService } from 'app/ng2/pages/workspace/workspace.service'; +import { ComponentInstanceServiceNg2 } from 'app/ng2/services/component-instance-services/component-instance.service'; +import { TopologyTemplateService } from 'app/ng2/services/component-services/topology-template.service'; +import { ModalService } from 'app/ng2/services/modal.service'; +import { ComponentGenericResponse } from 'app/ng2/services/responses/component-generic-response'; +import { ServiceGenericResponse } from 'app/ng2/services/responses/service-generic-response'; +import { WorkspaceState } from 'app/ng2/store/states/workspace.state'; +import { EventListenerService } from 'app/services'; +import { ComponentInstanceFactory, EVENTS, SdcElementType } from 'app/utils'; +import { ComponentType, GRAPH_EVENTS, GraphColors, DEPENDENCY_EVENTS } from 'app/utils/constants'; +import * as _ from 'lodash'; +import { DndDropEvent } from 'ngx-drag-drop/ngx-drag-drop'; +import { SdcUiServices } from 'onap-ui-angular'; +import { NotificationSettings } from 'onap-ui-angular/dist/notifications/utilities/notification.config'; +import { menuItem } from 'onap-ui-angular/dist/simple-popup-menu/menu-data.interface'; +import { CytoscapeEdgeEditation } from '../../../../../third-party/cytoscape.js-edge-editation/CytoscapeEdgeEditation.js'; +import { SelectedComponentType, SetSelectedComponentAction } from '../common/store/graph.actions'; +import { GraphState } from '../common/store/graph.state'; +import { + CompositionGraphGeneralUtils, + CompositionGraphNodesUtils, + CompositionGraphZoneUtils, + MatchCapabilitiesRequirementsUtils +} from './utils'; +import { CompositionGraphLinkUtils } from './utils/composition-graph-links-utils'; +import { CompositionGraphPaletteUtils } from './utils/composition-graph-palette-utils'; +import { ServicePathGraphUtils } from './utils/composition-graph-service-path-utils'; + +declare const window: any; + +@Component({ + selector: 'composition-graph', + templateUrl: './composition-graph.component.html', + styleUrls: ['./composition-graph.component.less'] +}) + +export class CompositionGraphComponent implements AfterViewInit { + + @Select(WorkspaceState.isViewOnly) isViewOnly$: boolean; + @Select(GraphState.withSidebar) withSidebar$: boolean; + @Input() topologyTemplate: TopologyTemplate; + @HostBinding('attr.data-tests-id') dataTestId: string; + @Input() testId: string; + + // tslint:disable:variable-name + private _cy: Cy.Instance; + private zoneTagMode: string; + private activeZoneInstance: ZoneInstance; + private zones: Zone[]; + private currentlyClickedNodePosition: Cy.Position; + private dragElement: JQuery; + private dragComponent: ComponentInstance; + private componentInstanceNames: string[]; + private topologyTemplateId: string; + private topologyTemplateType: string; + + constructor(private elRef: ElementRef, + private nodesFactory: NodesFactory, + private eventListenerService: EventListenerService, + private compositionGraphZoneUtils: CompositionGraphZoneUtils, + private generalGraphUtils: CompositionGraphGeneralUtils, + private compositionGraphLinkUtils: CompositionGraphLinkUtils, + private nodesGraphUtils: CompositionGraphNodesUtils, + private connectionWizardService: ConnectionWizardService, + private commonGraphUtils: CommonGraphUtils, + private modalService: ModalService, + private compositionGraphPaletteUtils: CompositionGraphPaletteUtils, + private topologyTemplateService: TopologyTemplateService, + private componentInstanceService: ComponentInstanceServiceNg2, + private matchCapabilitiesRequirementsUtils: MatchCapabilitiesRequirementsUtils, + private store: Store, + private compositionService: CompositionService, + private loaderService: SdcUiServices.LoaderService, + private workspaceService: WorkspaceService, + private notificationService: SdcUiServices.NotificationsService, + private simplePopupMenuService: SdcUiServices.simplePopupMenuService, + private servicePathGraphUtils: ServicePathGraphUtils) { + } + + ngOnInit() { + this.dataTestId = this.testId; + this.topologyTemplateId = this.workspaceService.metadata.uniqueId; + this.topologyTemplateType = this.workspaceService.metadata.componentType; + + this.store.dispatch(new SetSelectedComponentAction({ + component: this.topologyTemplate, + type: SelectedComponentType.TOPOLOGY_TEMPLATE + })); + this.eventListenerService.registerObserverCallback(EVENTS.ON_CHECKOUT, () => { + this.loadGraphData(); + }); + this.loadCompositionData(); + } + + ngAfterViewInit() { + this.loadGraph(); + } + + ngOnDestroy() { + this._cy.destroy(); + _.forEach(GRAPH_EVENTS, (event) => { + this.eventListenerService.unRegisterObserver(event); + }); + this.eventListenerService.unRegisterObserver(EVENTS.ON_CHECKOUT); + this.eventListenerService.unRegisterObserver(DEPENDENCY_EVENTS.ON_DEPENDENCY_CHANGE); + } + + public isViewOnly = (): boolean => { + return this.store.selectSnapshot((state) => state.workspace.isViewOnly); + } + + public zoom = (zoomIn: boolean): void => { + const currentZoom: number = this._cy.zoom(); + if (zoomIn) { + this.generalGraphUtils.zoomGraphTo(this._cy, currentZoom + .1); + } else { + this.generalGraphUtils.zoomGraphTo(this._cy, currentZoom - .1); + } + } + + public zoomAllWithoutSidebar = () => { + setTimeout(() => { // wait for sidebar changes to take effect before zooming + this.generalGraphUtils.zoomAll(this._cy); + }); + } + + public getAutoCompleteValues = (searchTerm: string) => { + if (searchTerm.length > 1) { // US requirement: only display search results after 2nd letter typed. + const nodes: Cy.CollectionNodes = this.nodesGraphUtils.getMatchingNodesByName(this._cy, searchTerm); + this.componentInstanceNames = _.map(nodes, (node) => node.data('name')); + } else { + this.componentInstanceNames = []; + } + } + + public highlightSearchMatches = (searchTerm: string) => { + this.nodesGraphUtils.highlightMatchingNodesByName(this._cy, searchTerm); + const matchingNodes: Cy.CollectionNodes = this.nodesGraphUtils.getMatchingNodesByName(this._cy, searchTerm); + this.generalGraphUtils.zoomAll(this._cy, matchingNodes); + } + + public onDrop = (dndEvent: DndDropEvent) => { + this.compositionGraphPaletteUtils.addNodeFromPalette(this._cy, dndEvent); + } + + public openServicePathMenu = ($event): void => { + + const menuConfig: menuItem[] = []; + if (!this.isViewOnly()) { + menuConfig.push({ + text: 'Create Service Flow', + action: () => this.servicePathGraphUtils.onCreateServicePath() + }); + } + menuConfig.push({ + text: 'Service Flows List', + type: '', + action: () => this.servicePathGraphUtils.onListServicePath() + }); + const popup = this.simplePopupMenuService.openBaseMenu(menuConfig, { + x: $event.x, + y: $event.y + }); + + } + + public deletePathsOnCy = () => { + this.servicePathGraphUtils.deletePathsFromGraph(this._cy); + } + + public drawPathOnCy = (data: ForwardingPath) => { + this.servicePathGraphUtils.drawPath(this._cy, data); + } + + public onTapEnd = (event: Cy.EventObject) => { + if (this.zoneTagMode) { + return; + } + if (event.cyTarget === this._cy) { // On Background clicked + if (this._cy.$('node:selected').length === 0) { // if the background click but not dragged + if (this.activeZoneInstance) { + this.unsetActiveZoneInstance(); + this.selectTopologyTemplate(); + } else { + this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_GRAPH_BACKGROUND_CLICKED); + this.selectTopologyTemplate(); + } + + } + } else if (event.cyTarget[0].isEdge()) { // and Edge clicked + this.compositionGraphLinkUtils.handleLinkClick(this._cy, event); + if (event.cyTarget[0].data().type === CompositionCiServicePathLink.LINK_TYPE) { + return; + } + this.openModifyLinkMenu(this.compositionGraphLinkUtils.getModifyLinkMenu(event.cyTarget[0], event), event); + } else { // On Node clicked + + this._cy.nodes(':grabbed').style({'overlay-opacity': 0}); + + const newPosition = event.cyTarget[0].position(); + // node position changed (drop after drag event) - we need to update position + if (this.currentlyClickedNodePosition.x !== newPosition.x || this.currentlyClickedNodePosition.y !== newPosition.y) { + const nodesMoved: Cy.CollectionNodes = this._cy.$(':grabbed'); + this.nodesGraphUtils.onNodesPositionChanged(this._cy, this.topologyTemplate, nodesMoved); + } else { + if (this.activeZoneInstance) { + this.unsetActiveZoneInstance(); + } + this.selectComponentInstance(event.cyTarget[0].data().componentInstance); + } + } + } + + private registerCytoscapeGraphEvents() { + + this._cy.on('addedgemouseup', (event, data) => { + const connectRelationModel: ConnectRelationModel = this.compositionGraphLinkUtils.onLinkDrawn(this._cy, data.source, data.target); + if (connectRelationModel != null) { + this.connectionWizardService.setRelationMenuDirectiveObj(connectRelationModel); + this.connectionWizardService.selectedMatch = null; + + const steps: StepModel[] = []; + const fromNodeName: string = connectRelationModel.fromNode.componentInstance.name; + const toNodeName: string = connectRelationModel.toNode.componentInstance.name; + steps.push(new StepModel(fromNodeName, FromNodeStepComponent)); + steps.push(new StepModel(toNodeName, ToNodeStepComponent)); + steps.push(new StepModel('Properties', PropertiesStepComponent)); + const wizardTitle = 'Connect: ' + fromNodeName + ' to ' + toNodeName; + const modalInstance = this.modalService.createMultiStepsWizard(wizardTitle, steps, this.createLinkFromMenu, ConnectionWizardHeaderComponent); + modalInstance.instance.open(); + } + }); + + this._cy.on('tapstart', 'node', (event: Cy.EventObject) => { + this.currentlyClickedNodePosition = angular.copy(event.cyTarget[0].position()); // update node position on drag + }); + + this._cy.on('drag', 'node', (event: Cy.EventObject) => { + if (event.cyTarget.data().componentSubType !== SdcElementType.POLICY && event.cyTarget.data().componentSubType !== SdcElementType.GROUP) { + event.cyTarget.style({'overlay-opacity': 0.24}); + if (this.generalGraphUtils.isValidDrop(this._cy, event.cyTarget)) { + event.cyTarget.style({'overlay-color': GraphColors.NODE_BACKGROUND_COLOR}); + } else { + event.cyTarget.style({'overlay-color': GraphColors.NODE_OVERLAPPING_BACKGROUND_COLOR}); + } + } + }); + + this._cy.on('handlemouseover', (event, payload) => { + // no need to add opacity while we are dragging and hovering othe nodes- or if opacity was already calculated for these nodes + if (payload.node.grabbed() || this._cy.scratch('_edge_editation_highlights') === true) { + return; + } + + if (this.zoneTagMode) { + this.zoneTagMode = this.zones[this.activeZoneInstance.type].getHoverTagModeId(); + return; + } + + const nodesData = this.nodesGraphUtils.getAllNodesData(this._cy.nodes()); + const nodesLinks = this.generalGraphUtils.getAllCompositionCiLinks(this._cy); + const instance = payload.node.data().componentInstance; + const filteredNodesData = this.matchCapabilitiesRequirementsUtils.findMatchingNodesToComponentInstance(instance, nodesData, nodesLinks); + this.matchCapabilitiesRequirementsUtils.highlightMatchingComponents(filteredNodesData, this._cy); + this.matchCapabilitiesRequirementsUtils.fadeNonMachingComponents(filteredNodesData, nodesData, this._cy, payload.node.data()); + + this._cy.scratch()._edge_editation_highlights = true; + }); + + this._cy.on('handlemouseout', () => { + if (this.zoneTagMode) { + this.zoneTagMode = this.zones[this.activeZoneInstance.type].getTagModeId(); + return; + } + if (this._cy.scratch('_edge_editation_highlights') === true) { + this._cy.removeScratch('_edge_editation_highlights'); + this._cy.emit('hidehandles'); + this.matchCapabilitiesRequirementsUtils.resetFadedNodes(this._cy); + } + }); + + this._cy.on('tapend', (event: Cy.EventObject) => { + this.onTapEnd(event); + }); + + this._cy.on('boxselect', 'node', (event: Cy.EventObject) => { + this.unsetActiveZoneInstance(); + this.selectComponentInstance(event.cyTarget.data().componentInstance); + }); + + this._cy.on('canvasredraw', (event: Cy.EventObject) => { + if (this.zoneTagMode) { + this.compositionGraphZoneUtils.showZoneTagIndications(this._cy, this.activeZoneInstance); + } + }); + + this._cy.on('handletagclick', (event: Cy.EventObject, eventData: any) => { + this.compositionGraphZoneUtils.handleTagClick(this._cy, this.activeZoneInstance, eventData.nodeId); + }); + } + + private initViewMode() { + + if (this.isViewOnly()) { + // remove event listeners + this._cy.off('drag'); + this._cy.off('handlemouseout'); + this._cy.off('handlemouseover'); + this._cy.off('canvasredraw'); + this._cy.off('handletagclick'); + this._cy.edges().unselectify(); + } + } + + private saveChangedCapabilityProperties = (): Promise<PropertyBEModel[]> => { + return new Promise<PropertyBEModel[]>((resolve) => { + const capabilityPropertiesBE: PropertyBEModel[] = this.connectionWizardService.changedCapabilityProperties.map((prop) => { + prop.value = prop.getJSONValue(); + const propBE = new PropertyBEModel(prop); + propBE.parentUniqueId = this.connectionWizardService.selectedMatch.relationship.relation.capabilityOwnerId; + return propBE; + }); + if (capabilityPropertiesBE.length > 0) { + // if there are capability properties to update, then first update capability properties and then resolve promise + this.componentInstanceService + .updateInstanceCapabilityProperties( + this.topologyTemplate, + this.connectionWizardService.selectedMatch.toNode, + this.connectionWizardService.selectedMatch.capability, + capabilityPropertiesBE + ) + .subscribe((response) => { + console.log('Update resource instance capability properties response: ', response); + this.connectionWizardService.changedCapabilityProperties = []; + resolve(capabilityPropertiesBE); + }); + } else { + // no capability properties to update, immediately resolve promise + resolve(capabilityPropertiesBE); + } + }); + } + + private loadCompositionData = () => { + this.loaderService.activate(); + this.topologyTemplateService.getComponentCompositionData(this.topologyTemplateId, this.topologyTemplateType).subscribe((response: ComponentGenericResponse) => { + if (this.topologyTemplateType === ComponentType.SERVICE) { + this.compositionService.forwardingPaths = (response as ServiceGenericResponse).forwardingPaths; + } + this.compositionService.componentInstances = response.componentInstances; + this.compositionService.componentInstancesRelations = response.componentInstancesRelations; + this.compositionService.groupInstances = response.groupInstances; + this.compositionService.policies = response.policies; + this.loadGraphData(); + this.loaderService.deactivate(); + }, (error) => { this.loaderService.deactivate(); }); + } + + private loadGraph = () => { + const graphEl = this.elRef.nativeElement.querySelector('.sdc-composition-graph-wrapper'); + this.initGraph(graphEl); + this.zones = this.compositionGraphZoneUtils.createCompositionZones(); + this.registerCytoscapeGraphEvents(); + this.registerCustomEvents(); + this.initViewMode(); + } + + private initGraphNodes() { + + setTimeout(() => { + const handles = new CytoscapeEdgeEditation(); + handles.init(this._cy); + if (!this.isViewOnly()) { // Init nodes handle extension - enable dynamic links + handles.initNodeEvents(); + handles.registerHandle(ComponentInstanceNodesStyle.getAddEdgeHandle()); + } + handles.registerHandle(ComponentInstanceNodesStyle.getTagHandle()); + handles.registerHandle(ComponentInstanceNodesStyle.getTaggedPolicyHandle()); + handles.registerHandle(ComponentInstanceNodesStyle.getTaggedGroupHandle()); + }, 0); + + _.each(this.compositionService.componentInstances, (instance) => { + const compositionGraphNode: CompositionCiNodeBase = this.nodesFactory.createNode(instance); + this.commonGraphUtils.addComponentInstanceNodeToGraph(this._cy, compositionGraphNode); + }); + + } + + private loadGraphData = () => { + this.initGraphNodes(); + this.compositionGraphLinkUtils.initGraphLinks(this._cy, this.compositionService.componentInstancesRelations); + this.compositionGraphZoneUtils.initZoneInstances(this.zones); + setTimeout(() => { // Need setTimeout so that angular canvas changes will take effect before resize & center + this.generalGraphUtils.zoomAllWithMax(this._cy, 1); + }); + this.componentInstanceNames = _.map(this._cy.nodes(), (node) => node.data('name')); + } + + private initGraph(graphEl: JQuery) { + + this._cy = cytoscape({ + container: graphEl, + style: ComponentInstanceNodesStyle.getCompositionGraphStyle(), + zoomingEnabled: true, + maxZoom: 1.2, + minZoom: .1, + userZoomingEnabled: false, + userPanningEnabled: true, + selectionType: 'single', + boxSelectionEnabled: true, + autolock: this.isViewOnly(), + autoungrabify: this.isViewOnly() + }); + + // Testing Bridge that allows Cypress tests to select a component on canvas not via DOM + if (window.Cypress) { + window.testBridge = this.createCanvasTestBridge(); + } + } + + private createCanvasTestBridge(): any { + return { + selectComponentInstance: (componentName: string) => { + const matchingNodesByName = this.nodesGraphUtils.getMatchingNodesByName(this._cy, componentName); + const component = new ComponentInstance(matchingNodesByName.first().data().componentInstance); + this.selectComponentInstance(component); + } + }; + } + + // -------------------------------------------- ZONES---------------------------------------------------------// + private zoneMinimizeToggle = (zoneType: ZoneInstanceType): void => { + this.zones[zoneType].minimized = !this.zones[zoneType].minimized; + } + + private zoneInstanceModeChanged = (newMode: ZoneInstanceMode, instance: ZoneInstance, zoneId: ZoneInstanceType): void => { + if (this.zoneTagMode) { // we're in tag mode. + if (instance === this.activeZoneInstance && newMode === ZoneInstanceMode.NONE) { // we want to turn tag mode off. + this.zoneTagMode = null; + this.activeZoneInstance.mode = ZoneInstanceMode.SELECTED; + this.compositionGraphZoneUtils.endCyTagMode(this._cy); + this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_CANVAS_TAG_END, instance); + + } + } else { + // when active zone instance gets hover/none, don't actually change mode, just show/hide indications + if (instance !== this.activeZoneInstance || (instance === this.activeZoneInstance && newMode > ZoneInstanceMode.HOVER)) { + instance.mode = newMode; + } + + if (newMode === ZoneInstanceMode.NONE) { + this.compositionGraphZoneUtils.hideZoneTagIndications(this._cy); + if (this.zones[ZoneInstanceType.GROUP]) { + this.compositionGraphZoneUtils.hideGroupZoneIndications(this.zones[ZoneInstanceType.GROUP].instances); + } + } + if (newMode >= ZoneInstanceMode.HOVER) { + this.compositionGraphZoneUtils.showZoneTagIndications(this._cy, instance); + if (instance.type === ZoneInstanceType.POLICY && this.zones[ZoneInstanceType.GROUP]) { + this.compositionGraphZoneUtils.showGroupZoneIndications(this.zones[ZoneInstanceType.GROUP].instances, instance); + } + } + if (newMode >= ZoneInstanceMode.SELECTED) { + this._cy.$('node:selected').unselect(); + if (this.activeZoneInstance && this.activeZoneInstance !== instance && newMode >= ZoneInstanceMode.SELECTED) { + this.activeZoneInstance.mode = ZoneInstanceMode.NONE; + } + this.activeZoneInstance = instance; + this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_ZONE_INSTANCE_SELECTED, instance); + this.store.dispatch(new SetSelectedComponentAction({ + component: instance.instanceData, + type: SelectedComponentType[ZoneInstanceType[instance.type]] + })); + } + if (newMode === ZoneInstanceMode.TAG) { + this.compositionGraphZoneUtils.startCyTagMode(this._cy); + this.zoneTagMode = this.zones[zoneId].getTagModeId(); + this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_CANVAS_TAG_START, zoneId); + } + } + } + + private zoneInstanceTagged = (taggedInstance: ZoneInstance) => { + this.activeZoneInstance.addOrRemoveAssignment(taggedInstance.instanceData.uniqueId, ZoneInstanceAssignmentType.GROUPS); + const newHandle: string = this.compositionGraphZoneUtils.getCorrectHandleForNode(taggedInstance.instanceData.uniqueId, this.activeZoneInstance); + taggedInstance.showHandle(newHandle); + } + + private unsetActiveZoneInstance = (): void => { + if (this.activeZoneInstance) { + this.activeZoneInstance.mode = ZoneInstanceMode.NONE; + this.activeZoneInstance = null; + this.zoneTagMode = null; + } + } + + private selectComponentInstance = (componentInstance: ComponentInstance) => { + this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_NODE_SELECTED, componentInstance); + this.store.dispatch(new SetSelectedComponentAction({ + component: componentInstance, + type: SelectedComponentType.COMPONENT_INSTANCE + })); + } + + private selectTopologyTemplate = () => { + this.store.dispatch(new SetSelectedComponentAction({ + component: this.topologyTemplate, + type: SelectedComponentType.TOPOLOGY_TEMPLATE + })); + } + + private zoneBackgroundClicked = (): void => { + if (!this.zoneTagMode && this.activeZoneInstance) { + this.unsetActiveZoneInstance(); + this.selectTopologyTemplate(); + } + } + + private zoneAssignmentSaveStart = () => { + this.loaderService.activate(); + } + + private zoneAssignmentSaveComplete = (success: boolean) => { + this.loaderService.deactivate(); + if (!success) { + this.notificationService.push(new NotificationSettings('error', 'Update Failed', 'Error')); + } + } + + private deleteZoneInstance = (deletedInstance: UIZoneInstanceObject) => { + if (deletedInstance.type === ZoneInstanceType.POLICY) { + this.compositionService.policies = this.compositionService.policies.filter((policy) => policy.uniqueId !== deletedInstance.uniqueId); + } else if (deletedInstance.type === ZoneInstanceType.GROUP) { + this.compositionService.groupInstances = this.compositionService.groupInstances.filter((group) => group.uniqueId !== deletedInstance.uniqueId); + } + // remove it from zones + this.zones[deletedInstance.type].removeInstance(deletedInstance.uniqueId); + if (deletedInstance.type === ZoneInstanceType.GROUP && !_.isEmpty(this.zones[ZoneInstanceType.POLICY])) { + this.compositionGraphZoneUtils.updateTargetsOrMembersOnCanvasDelete(deletedInstance.uniqueId, [this.zones[ZoneInstanceType.POLICY]], ZoneInstanceAssignmentType.GROUPS); + } + this.selectTopologyTemplate(); + } + // -------------------------------------------------------------------------------------------------------------// + + private registerCustomEvents() { + + this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_GROUP_INSTANCE_UPDATE, (groupInstance: GroupInstance) => { + this.compositionGraphZoneUtils.findAndUpdateZoneInstanceData(this.zones, groupInstance); + this.notificationService.push(new NotificationSettings('success', 'Group Updated', 'Success')); + }); + + this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_POLICY_INSTANCE_UPDATE, (policyInstance: PolicyInstance) => { + this.compositionGraphZoneUtils.findAndUpdateZoneInstanceData(this.zones, policyInstance); + this.notificationService.push(new NotificationSettings('success', 'Policy Updated', 'Success')); + }); + + this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_PALETTE_COMPONENT_HOVER_IN, (leftPaletteComponent: LeftPaletteComponent) => { + this.compositionGraphPaletteUtils.onComponentHoverIn(leftPaletteComponent, this._cy); + }); + + this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_ADD_ZONE_INSTANCE_FROM_PALETTE, + (component: TopologyTemplate, paletteComponent: LeftPaletteComponent, startPosition: Point) => { + + const zoneType: ZoneInstanceType = this.compositionGraphZoneUtils.getZoneTypeForPaletteComponent(paletteComponent.categoryType); + this.compositionGraphZoneUtils.showZone(this.zones[zoneType]); + + this.loaderService.activate(); + this.compositionGraphZoneUtils.createZoneInstanceFromLeftPalette(zoneType, paletteComponent.type).subscribe((zoneInstance: ZoneInstance) => { + this.loaderService.deactivate(); + this.compositionGraphZoneUtils.addInstanceToZone(this.zones[zoneInstance.type], zoneInstance, true); + this.compositionGraphZoneUtils.createPaletteToZoneAnimation(startPosition, zoneType, zoneInstance); + }, (error) => { + this.loaderService.deactivate(); + }); + }); + + this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_PALETTE_COMPONENT_HOVER_OUT, () => { + this._cy.emit('hidehandles'); + this.matchCapabilitiesRequirementsUtils.resetFadedNodes(this._cy); + }); + + this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_PALETTE_COMPONENT_DRAG_START, (dragElement, dragComponent) => { + this.dragElement = dragElement; + this.dragComponent = ComponentInstanceFactory.createComponentInstanceFromComponent(dragComponent); + }); + + this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_PALETTE_COMPONENT_DRAG_ACTION, (position: Point) => { + const draggedElement = document.getElementById('draggable_element'); + draggedElement.className = this.compositionGraphPaletteUtils.isDragValid(this._cy, position) ? 'valid-drag' : 'invalid-drag'; + }); + + this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_PALETTE_COMPONENT_DROP, (event: DndDropEvent) => { + this.onDrop(event); + }); + + this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_COMPONENT_INSTANCE_NAME_CHANGED, (component: ComponentInstance) => { + const selectedNode = this._cy.getElementById(component.uniqueId); + selectedNode.data().componentInstance.name = component.name; + selectedNode.data('name', component.name); // used for tooltip + selectedNode.data('displayName', selectedNode.data().getDisplayName()); // abbreviated + }); + + this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_DELETE_COMPONENT_INSTANCE, (componentInstanceId: string) => { + const nodeToDelete = this._cy.getElementById(componentInstanceId); + this.nodesGraphUtils.deleteNode(this._cy, this.topologyTemplate, nodeToDelete); + this.selectTopologyTemplate(); + }); + + this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_DELETE_ZONE_INSTANCE, (deletedInstance: UIZoneInstanceObject) => { + this.deleteZoneInstance(deletedInstance); + }); + + this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_DELETE_COMPONENT_INSTANCE_SUCCESS, (componentInstanceId: string) => { + if (!_.isEmpty(this.zones)) { + this.compositionGraphZoneUtils.updateTargetsOrMembersOnCanvasDelete(componentInstanceId, this.zones, ZoneInstanceAssignmentType.COMPONENT_INSTANCES); + } + }); + + this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_DELETE_EDGE, (releaseLoading: boolean, linksToDelete: Cy.CollectionEdges) => { + this.compositionGraphLinkUtils.deleteLink(this._cy, this.topologyTemplate, releaseLoading, linksToDelete); + }); + + this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_VERSION_CHANGED, (component: ComponentInstance) => { + // Remove everything from graph and reload it all + this._cy.elements().remove(); + this.loadCompositionData(); + setTimeout(() => { this._cy.getElementById(component.uniqueId).select(); }, 1000); + this.selectComponentInstance(component); + }); + this.eventListenerService.registerObserverCallback(DEPENDENCY_EVENTS.ON_DEPENDENCY_CHANGE, (ischecked: boolean) => { + if (ischecked) { + this._cy.$('node:selected').addClass('dependent'); + } else { + // due to defect in cytoscape, just changing the class does not replace the icon, and i need to revert to original icon with no markings. + this._cy.$('node:selected').removeClass('dependent'); + this._cy.$('node:selected').style({'background-image': this._cy.$('node:selected').data('originalImg')}); + } + }); + } + private createLinkFromMenu = (): void => { + this.saveChangedCapabilityProperties().then(() => { + this.compositionGraphLinkUtils.createLinkFromMenu(this._cy, this.connectionWizardService.selectedMatch); + }); + } + + private deleteRelation = (link: Cy.CollectionEdges) => { + // if multiple edges selected, delete the VL itself so edges get deleted automatically + if (this._cy.$('edge:selected').length > 1) { + this.nodesGraphUtils.deleteNode(this._cy, this.topologyTemplate, this._cy.$('node:selected')); + } else { + this.compositionGraphLinkUtils.deleteLink(this._cy, this.topologyTemplate, true, link); + } + } + + private viewRelation = (link: Cy.CollectionEdges) => { + + const linkData = link.data(); + const sourceNode: CompositionCiNodeBase = link.source().data(); + const targetNode: CompositionCiNodeBase = link.target().data(); + const relationship: Relationship = linkData.relation.relationships[0]; + + this.compositionGraphLinkUtils.getRelationRequirementCapability(relationship, sourceNode.componentInstance, targetNode.componentInstance).then((objReqCap) => { + const capability = objReqCap.capability; + const requirement = objReqCap.requirement; + + this.connectionWizardService.connectRelationModel = new ConnectRelationModel(sourceNode, targetNode, []); + this.connectionWizardService.selectedMatch = new Match(requirement, capability, true, linkData.source, linkData.target); + this.connectionWizardService.selectedMatch.relationship = relationship; + + const title = `Connection Properties`; + const saveButton: ButtonModel = new ButtonModel('Save', 'blue', () => { + this.saveChangedCapabilityProperties().then(() => { + this.modalService.closeCurrentModal(); + }); + }); + const cancelButton: ButtonModel = new ButtonModel('Cancel', 'white', () => { + this.modalService.closeCurrentModal(); + }); + const modal = new ModalModel('xl', title, '', [saveButton, cancelButton]); + const modalInstance = this.modalService.createCustomModal(modal); + this.modalService.addDynamicContentToModal(modalInstance, ConnectionPropertiesViewComponent); + modalInstance.instance.open(); + + new Promise((resolve) => { + if (!this.connectionWizardService.selectedMatch.capability.properties) { + this.componentInstanceService.getInstanceCapabilityProperties(this.topologyTemplateType, this.topologyTemplateId, linkData.target, capability) + .subscribe(() => { + resolve(); + }, () => { /* do nothing */ }); + } else { + resolve(); + } + }).then(() => { + this.modalService.addDynamicContentToModal(modalInstance, ConnectionPropertiesViewComponent); + }); + }, () => { /* do nothing */ }); + } + + private openModifyLinkMenu = (linkMenuObject: LinkMenu, $event) => { + + const menuConfig: menuItem[] = [{ + text: 'View', + iconName: 'eye-o', + iconType: 'common', + iconMode: 'secondary', + iconSize: 'small', + type: '', + action: () => this.viewRelation(linkMenuObject.link as Cy.CollectionEdges) + }]; + + if (!this.isViewOnly()) { + menuConfig.push({ + text: 'Delete', + iconName: 'trash-o', + iconType: 'common', + iconMode: 'secondary', + iconSize: 'small', + type: '', + action: () => this.deleteRelation(linkMenuObject.link as Cy.CollectionEdges) + }); + } + this.simplePopupMenuService.openBaseMenu(menuConfig, { + x: $event.originalEvent.x, + y: $event.originalEvent.y + }); + } + +} diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.module.ts b/catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.module.ts new file mode 100644 index 0000000000..e58d160c4d --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/composition-graph.module.ts @@ -0,0 +1,55 @@ +import {NgModule} from "@angular/core"; +import {CommonModule} from "@angular/common"; +import {CompositionGraphComponent} from "./composition-graph.component"; +import {ZoneModules} from "./canvas-zone/zones-module"; +import {CompositionGraphZoneUtils} from "./utils/composition-graph-zone-utils"; +import {CompositionGraphGeneralUtils} from "./utils/composition-graph-general-utils"; +import {CommonGraphUtils} from "./common/common-graph-utils"; +import {LinksFactory} from "app/models/graph/graph-links/links-factory"; +import {NodesFactory} from "app/models/graph/nodes/nodes-factory"; +import {ImageCreatorService} from "./common/image-creator.service"; +import {MatchCapabilitiesRequirementsUtils} from "./utils/match-capability-requierment-utils"; +import {CompositionGraphNodesUtils} from "./utils/composition-graph-nodes-utils"; +import {ConnectionWizardService} from "app/ng2/pages/composition/graph/connection-wizard/connection-wizard.service"; +import {CompositionGraphPaletteUtils} from "./utils/composition-graph-palette-utils"; +import {QueueServiceUtils} from "app/ng2/utils/queue-service-utils"; +import {DndModule} from "ngx-drag-drop"; +import { MenuListNg2Module } from "app/ng2/components/downgrade-wrappers/menu-list-ng2/menu-list-ng2.module"; +import { UiElementsModule } from "app/ng2/components/ui/ui-elements.module"; +import {ServicePathSelectorModule} from "./service-path-selector/service-path-selector.module"; +import {SdcUiComponentsModule, SdcUiServices} from "onap-ui-angular"; +import {CanvasSearchModule} from "./canvas-search/canvas-search.module"; +import {CompositionGraphLinkUtils, ServicePathGraphUtils} from "./utils"; + + +@NgModule({ + declarations: [CompositionGraphComponent], + imports: [CommonModule, + ServicePathSelectorModule, + SdcUiComponentsModule, + MenuListNg2Module, + UiElementsModule, + ZoneModules, + CanvasSearchModule, + DndModule], + exports: [CompositionGraphComponent], + entryComponents: [CompositionGraphComponent], + providers: [ + CompositionGraphZoneUtils, + CompositionGraphGeneralUtils, + MatchCapabilitiesRequirementsUtils, + CompositionGraphNodesUtils, + CompositionGraphLinkUtils, + CommonGraphUtils, + NodesFactory, + LinksFactory, + ImageCreatorService, + ConnectionWizardService, + CompositionGraphPaletteUtils, + QueueServiceUtils, + SdcUiServices.simplePopupMenuService, + ServicePathGraphUtils + ] +}) +export class CompositionGraphModule { +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/connection-wizard/connection-properties-view/connection-properties-view.component.html b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-properties-view/connection-properties-view.component.html index b24e469554..b24e469554 100644 --- a/catalog-ui/src/app/ng2/pages/connection-wizard/connection-properties-view/connection-properties-view.component.html +++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-properties-view/connection-properties-view.component.html diff --git a/catalog-ui/src/app/ng2/pages/connection-wizard/connection-properties-view/connection-properties-view.component.less b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-properties-view/connection-properties-view.component.less index 07f9aa2135..07f9aa2135 100644 --- a/catalog-ui/src/app/ng2/pages/connection-wizard/connection-properties-view/connection-properties-view.component.less +++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-properties-view/connection-properties-view.component.less diff --git a/catalog-ui/src/app/ng2/pages/connection-wizard/connection-properties-view/connection-properties-view.component.ts b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-properties-view/connection-properties-view.component.ts index 5abb879013..5abb879013 100644 --- a/catalog-ui/src/app/ng2/pages/connection-wizard/connection-properties-view/connection-properties-view.component.ts +++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-properties-view/connection-properties-view.component.ts diff --git a/catalog-ui/src/app/ng2/pages/connection-wizard/connection-wizard-header/connection-wizard-header.component.html b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard-header/connection-wizard-header.component.html index 7e7e82d85f..7e7e82d85f 100644 --- a/catalog-ui/src/app/ng2/pages/connection-wizard/connection-wizard-header/connection-wizard-header.component.html +++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard-header/connection-wizard-header.component.html diff --git a/catalog-ui/src/app/ng2/pages/connection-wizard/connection-wizard-header/connection-wizard-header.component.less b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard-header/connection-wizard-header.component.less index 72fa6e813f..d8bab288d3 100644 --- a/catalog-ui/src/app/ng2/pages/connection-wizard/connection-wizard-header/connection-wizard-header.component.less +++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard-header/connection-wizard-header.component.less @@ -1,5 +1,5 @@ -@import '../../../../../assets/styles/sprite-proxy-services-icons'; -@import '../../../../../assets/styles/variables'; +@import '../../../../../../../assets/styles/sprite-proxy-services-icons'; +@import '../../../../../../../assets/styles/variables'; .header-main-container{ background-color: #f8f8f8; width: 100%; diff --git a/catalog-ui/src/app/ng2/pages/connection-wizard/connection-wizard-header/connection-wizard-header.component.ts b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard-header/connection-wizard-header.component.ts index f5bc3b7ca4..f5bc3b7ca4 100644 --- a/catalog-ui/src/app/ng2/pages/connection-wizard/connection-wizard-header/connection-wizard-header.component.ts +++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard-header/connection-wizard-header.component.ts diff --git a/catalog-ui/src/app/ng2/pages/connection-wizard/connection-wizard.module.ts b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard.module.ts index 6b4b4128c1..80464dc970 100644 --- a/catalog-ui/src/app/ng2/pages/connection-wizard/connection-wizard.module.ts +++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard.module.ts @@ -3,9 +3,9 @@ import {NgModule} from "@angular/core"; import {FromNodeStepComponent} from "./from-node-step/from-node-step.component"; import {PropertiesStepComponent} from "./properties-step/properties-step.component"; import {ConnectionWizardService} from "./connection-wizard.service"; -import {SelectRequirementOrCapabilityModule} from "../../components/logic/select-requirement-or-capability/select-requirement-or-capability.module"; -import {PropertyTableModule} from "../../components/logic/properties-table/property-table.module"; -import {FormElementsModule} from "../../components/ui/form-components/form-elements.module"; +import {SelectRequirementOrCapabilityModule} from "../../../../components/logic/select-requirement-or-capability/select-requirement-or-capability.module"; +import {PropertyTableModule} from "../../../../components/logic/properties-table/property-table.module"; +import {FormElementsModule} from "../../../../components/ui/form-components/form-elements.module"; import {ConnectionWizardHeaderComponent} from "./connection-wizard-header/connection-wizard-header.component"; import {ConnectionPropertiesViewComponent} from "./connection-properties-view/connection-properties-view.component"; import {BrowserModule} from "@angular/platform-browser"; diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard.service.spec.ts b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard.service.spec.ts new file mode 100644 index 0000000000..8a5c5fcefb --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard.service.spec.ts @@ -0,0 +1,85 @@ +import {TestBed} from "@angular/core/testing"; +import {WorkspaceService} from "../../../../pages/workspace/workspace.service"; +import { ConnectionWizardService } from "app/ng2/pages/composition/graph/connection-wizard/connection-wizard.service"; +import { ConnectRelationModel, Match, Requirement, Capability } from "app/models"; +import { Mock } from "ts-mockery/dist"; + +describe('Connection Wizard Service', () => { + + let service: ConnectionWizardService; + + const connectRelationModelMock = Mock.of<ConnectRelationModel>({ + possibleRelations: [ + Mock.of<Match>({isFromTo: true, requirement: Mock.of<Requirement>({uniqueId: 'requirement1', capability: "cap1"}), capability: Mock.of<Capability>({uniqueId: 'capability1', type: 'othertype'})}), + Mock.of<Match>({isFromTo: true, requirement: Mock.of<Requirement>({uniqueId: 'requirement2', capability: "cap1"}), capability: Mock.of<Capability>({uniqueId: 'capability2', type: 'tosca'})}), + Mock.of<Match>({isFromTo: true, requirement: Mock.of<Requirement>({uniqueId: 'requirement3', capability: "cap1"}), capability: Mock.of<Capability>({uniqueId: 'capability3', type: 'tosca'})}), + Mock.of<Match>({isFromTo: true, requirement: Mock.of<Requirement>({uniqueId: 'requirement4', capability: "cap1"}), capability: Mock.of<Capability>({uniqueId: 'capability2', type: 'tosca'})}), + Mock.of<Match>({isFromTo: true, requirement: Mock.of<Requirement>({uniqueId: 'requirement5', capability: "cap2"}), capability: Mock.of<Capability>({uniqueId: 'capability1', type: 'tosca'})}), + Mock.of<Match>({isFromTo: false, requirement: Mock.of<Requirement>({uniqueId: 'requirement6', capability: "cap2"}), capability: Mock.of<Capability>({uniqueId: 'capability2', type: 'tosca'})}), + Mock.of<Match>({isFromTo: false, requirement: Mock.of<Requirement>({uniqueId: 'requirement7', capability: "cap2"}), capability: Mock.of<Capability>({uniqueId: 'capability1', type: 'othertype'})}) + ] + }); + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [], + providers: [ConnectionWizardService, + {provide: WorkspaceService, useValue: {}} + ] + }); + + service = TestBed.get(ConnectionWizardService); + service.connectRelationModel = connectRelationModelMock; + }); + + describe('getOptionalRequirementsByInstanceUniqueId', () => { + it('if no capability to match is sent in and isFromTo is true, ALL isFromTo==true requirements are returned', () => { + const requirements = service.getOptionalRequirementsByInstanceUniqueId(true); + expect(requirements['cap1'].length).toBe(4); + expect(requirements['cap2'].length).toBe(1); + }); + + it('if no capability to match is sent in and isFromTo is false, ALL isFromTo==false requirements are returned', () => { + const requirements = service.getOptionalRequirementsByInstanceUniqueId(false); + expect(requirements['cap1']).toBeUndefined(); + expect(requirements['cap2'].length).toBe(2); + }); + + it('if capability to match IS sent in and isFromTo is true, matches with the same uniqueID and isFromTo==true are returned', () => { + const capability = Mock.of<Capability>({uniqueId: 'capability1'}); + const requirements = service.getOptionalRequirementsByInstanceUniqueId(true, capability); + expect(requirements['cap1'].length).toBe(1); + expect(requirements['cap2'].length).toBe(1); + }); + + it('if capability to match IS sent in and isFromTo is false, requirements with the same uniqueID and isFromTo==false are returned', () => { + const capability = Mock.of<Capability>({uniqueId: 'capability1'}); + const requirements = service.getOptionalRequirementsByInstanceUniqueId(false, capability); + expect(requirements['cap1']).toBeUndefined(); + expect(requirements['cap2'].length).toBe(1); + }); + }) + + describe('getOptionalCapabilitiesByInstanceUniqueId', () => { + it('if requirement to match IS sent in and isFromTo is true, matches with the same uniqueID and isFromTo==true are returned', () => { + const requirement = Mock.of<Requirement>({uniqueId: 'requirement1'}); + const capabilities = service.getOptionalCapabilitiesByInstanceUniqueId(true, requirement); + expect(capabilities['othertype'].length).toBe(1); + expect(capabilities['tosca']).toBeUndefined(); + }); + + it('if no requirement to match is sent in and isFromTo is true, a UNIQUE list of all capabilities with isFromTo==true are returned', () => { + const capabilities = service.getOptionalCapabilitiesByInstanceUniqueId(true); + expect(capabilities['othertype'].length).toBe(1); + expect(capabilities['tosca'].length).toBe(2); + }); + + it('if no requirement to match is sent in and isFromTo is false, all capabilities with isFromTo==false are returned', () => { + const capabilities = service.getOptionalCapabilitiesByInstanceUniqueId(false); + expect(capabilities['othertype'].length).toBe(1); + expect(capabilities['tosca'].length).toBe(1); + }); + }); + +}); + diff --git a/catalog-ui/src/app/ng2/pages/connection-wizard/connection-wizard.service.ts b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard.service.ts index af8dcb4956..2eb5428f61 100644 --- a/catalog-ui/src/app/ng2/pages/connection-wizard/connection-wizard.service.ts +++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard.service.ts @@ -1,20 +1,23 @@ import * as _ from "lodash"; -import {ConnectRelationModel} from "../../../models/graph/connectRelationModel"; +import {ConnectRelationModel} from "app/models/graph/connectRelationModel"; import {Injectable} from "@angular/core"; import { Requirement, Capability} from "app/models"; import {Dictionary} from "lodash"; import {Match, Component, PropertyFEModel} from "app/models"; +import {Store} from "@ngxs/store"; +import {WorkspaceService} from "../../../workspace/workspace.service"; @Injectable() export class ConnectionWizardService { connectRelationModel:ConnectRelationModel; - currentComponent:Component; selectedMatch:Match; changedCapabilityProperties:PropertyFEModel[]; - constructor() { + + constructor(private workspaceService: WorkspaceService) { this.changedCapabilityProperties = []; + } public setRelationMenuDirectiveObj = (connectRelationModel:ConnectRelationModel) => { diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/from-node-step/__snapshots__/from-node-step.component.spec.ts.snap b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/from-node-step/__snapshots__/from-node-step.component.spec.ts.snap new file mode 100644 index 0000000000..739ce3d8fe --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/from-node-step/__snapshots__/from-node-step.component.spec.ts.snap @@ -0,0 +1,12 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`from-node-step component should match current snapshot 1`] = ` +<from-node-step + connectWizardService={[Function Object]} + preventBack={[Function Function]} + preventNext={[Function Function]} + updateSelectedReqOrCap={[Function Function]} +> + <select-requirement-or-capability /> +</from-node-step> +`; diff --git a/catalog-ui/src/app/ng2/pages/connection-wizard/from-node-step/from-node-step.component.html b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/from-node-step/from-node-step.component.html index 1cb3df735c..0a70069748 100644 --- a/catalog-ui/src/app/ng2/pages/connection-wizard/from-node-step/from-node-step.component.html +++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/from-node-step/from-node-step.component.html @@ -17,7 +17,6 @@ <select-requirement-or-capability [optionalRequirementsMap]="optionalRequirementsMap" [optionalCapabilitiesMap]="optionalCapabilitiesMap" [selectedReqOrCapModel]="connectWizardService.selectedMatch && (connectWizardService.selectedMatch.isFromTo ? connectWizardService.selectedMatch.requirement : connectWizardService.selectedMatch.capability)" - [currentComponent]="connectWizardService.currentComponent" [componentInstanceId]="connectWizardService.connectRelationModel.fromNode.componentInstance.uniqueId" (updateSelectedReqOrCap)="updateSelectedReqOrCap($event)"> </select-requirement-or-capability>
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/from-node-step/from-node-step.component.spec.ts b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/from-node-step/from-node-step.component.spec.ts new file mode 100644 index 0000000000..59ff72adda --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/from-node-step/from-node-step.component.spec.ts @@ -0,0 +1,114 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { Capability, Match } from 'app/models'; +import { ConfigureFn, configureTests } from '../../../../../../../jest/test-config.helper'; +import { Requirement } from '../../../../../../models/requirement'; +import { ConnectionWizardService } from '../connection-wizard.service'; +import { FromNodeStepComponent } from './from-node-step.component'; + +describe('from-node-step component', () => { + + let fixture: ComponentFixture<FromNodeStepComponent>; + let connectionWizardServiceMockWithoutSelectedMatch: Partial<ConnectionWizardService>; + let connectionWizardServiceMockWithSelectedMatch: Partial<ConnectionWizardService>; + + const connectionWizardServiceMockSelectedMatchWithRequirements = {requirement: 'val'}; + + connectionWizardServiceMockWithoutSelectedMatch = { + getOptionalRequirementsByInstanceUniqueId: jest.fn().mockReturnValue(5), + getOptionalCapabilitiesByInstanceUniqueId: jest.fn().mockReturnValue(10), + + connectRelationModel: { + fromNode: { + componentInstance: { + uniqueId : 'testUniqueID' + } + } + } + }; + + connectionWizardServiceMockWithSelectedMatch = { + selectedMatch: connectionWizardServiceMockSelectedMatchWithRequirements, + getOptionalRequirementsByInstanceUniqueId: jest.fn().mockReturnValue(5), + getOptionalCapabilitiesByInstanceUniqueId: jest.fn().mockReturnValue(10) + }; + + let expectedConnectionWizardServiceMock = connectionWizardServiceMockWithoutSelectedMatch; + + beforeEach( + async(() => { + const configure: ConfigureFn = testBed => { + testBed.configureTestingModule({ + declarations: [FromNodeStepComponent], + imports: [], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + {provide: ConnectionWizardService, useValue: expectedConnectionWizardServiceMock} + ], + }); + }; + + configureTests(configure).then(testBed => { + fixture = testBed.createComponent(FromNodeStepComponent); + }); + }) + ); + + + it('should match current snapshot', () => { + expect(fixture).toMatchSnapshot(); + }); + + it('preventBack return true - always', () => { + fixture.componentInstance.ngOnInit(); + const result = fixture.componentInstance.preventBack(); + expect(result).toEqual(true); + }); + + it('preventNext return true since selectedMatch does not exist in connectionWizardServiceMock', () => { + fixture.componentInstance.ngOnInit(); + const result = fixture.componentInstance.preventNext(); + expect(result).toEqual(true); + }); + + it('preventNext return false since to selectedMatch or selectedMatch.capability & selectedMatch.requirement does exist in connectionWizardServiceMock', () => { + fixture.componentInstance.connectWizardService = connectionWizardServiceMockWithSelectedMatch; + fixture.componentInstance.ngOnInit(); + const result = fixture.componentInstance.preventNext(); + expect(result).toEqual(false); + }); + + it('updateSelectedReqOrCap is called with instance of requirement, the selectMatch will be set to an Instance of Match of type Requirement', () => { + const requirement = new Requirement(); + fixture.componentInstance.updateSelectedReqOrCap(requirement); + const expectedSelectedMatch = fixture.componentInstance.connectWizardService.selectedMatch; + + expect(expectedSelectedMatch).toBeInstanceOf(Match); + expect(expectedSelectedMatch.capability).toBe(null); + expect(expectedSelectedMatch.fromNode).toBe('testUniqueID'); + expect(expectedSelectedMatch.isFromTo).toBe(true); + expect(expectedSelectedMatch.toNode).toBe(null); + expect(expectedSelectedMatch.requirement).toBeInstanceOf(Requirement); + }); + + it('updateSelectedReqOrCap is called with instance of capability, the selectMatch will be set to an Instance of Match of type Capability', () => { + const capability = new Capability(); + fixture.componentInstance.updateSelectedReqOrCap(capability); + const expectedSelectedMatch = fixture.componentInstance.connectWizardService.selectedMatch; + + expect(expectedSelectedMatch).toBeInstanceOf(Match); + expect(expectedSelectedMatch.requirement).toBe(null); + expect(expectedSelectedMatch.fromNode).toBe(null); + expect(expectedSelectedMatch.isFromTo).toBe(false); + expect(expectedSelectedMatch.toNode).toBe('testUniqueID'); + expect(expectedSelectedMatch.capability).toBeInstanceOf(Capability); + }); + + it('updateSelectedReqOrCap is called with null, the selectMatch will be set to null', () => { + fixture.componentInstance.updateSelectedReqOrCap(null); + const expectedSelectedMatch = fixture.componentInstance.connectWizardService.selectedMatch; + + expect(expectedSelectedMatch).toBe(null); + }); + +});
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/connection-wizard/from-node-step/from-node-step.component.ts b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/from-node-step/from-node-step.component.ts index 054d38b063..cffd58c9ea 100644 --- a/catalog-ui/src/app/ng2/pages/connection-wizard/from-node-step/from-node-step.component.ts +++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/from-node-step/from-node-step.component.ts @@ -1,11 +1,10 @@ -import {Component, OnInit, Inject, forwardRef} from "@angular/core"; -import {IStepComponent} from "../../../../models/wizard-step"; -import {Dictionary} from "lodash"; -import { Match} from "app/models"; -import {ConnectionWizardService} from "../connection-wizard.service"; -import {Requirement} from "../../../../models/requirement"; -import {Capability} from "../../../../models/capability"; -import {PropertyModel} from "../../../../models/properties"; +import { Component, forwardRef, Inject, OnInit } from '@angular/core'; +import { Match } from 'app/models'; +import { Capability } from 'app/models/capability'; +import { Requirement } from 'app/models/requirement'; +import { IStepComponent } from 'app/models/wizard-step'; +import { Dictionary } from 'lodash'; +import { ConnectionWizardService } from '../connection-wizard.service'; @Component({ selector: 'from-node-step', @@ -14,31 +13,31 @@ import {PropertyModel} from "../../../../models/properties"; export class FromNodeStepComponent implements IStepComponent, OnInit{ - constructor(@Inject(forwardRef(() => ConnectionWizardService)) public connectWizardService: ConnectionWizardService) {} - optionalRequirementsMap: Dictionary<Requirement[]>; optionalCapabilitiesMap: Dictionary<Capability[]>; - ngOnInit(){ + constructor(@Inject(forwardRef(() => ConnectionWizardService)) public connectWizardService: ConnectionWizardService) {} + + ngOnInit() { this.optionalRequirementsMap = this.connectWizardService.getOptionalRequirementsByInstanceUniqueId(true); this.optionalCapabilitiesMap = this.connectWizardService.getOptionalCapabilitiesByInstanceUniqueId(false); } - preventNext = ():boolean => { + preventNext = (): boolean => { return !this.connectWizardService.selectedMatch || (!this.connectWizardService.selectedMatch.capability && !this.connectWizardService.selectedMatch.requirement); } - preventBack = ():boolean => { + preventBack = (): boolean => { return true; } - private updateSelectedReqOrCap = (selected:Requirement|Capability):void => { - if(!selected){ + private updateSelectedReqOrCap = (selected: Requirement|Capability): void => { + if (!selected) { this.connectWizardService.selectedMatch = null; - } else if(selected instanceof Requirement){ + } else if (selected instanceof Requirement) { this.connectWizardService.selectedMatch = new Match(<Requirement>selected, null, true, this.connectWizardService.connectRelationModel.fromNode.componentInstance.uniqueId, null); - } else{ - this.connectWizardService.selectedMatch = new Match(null,<Capability>selected , false, null, this.connectWizardService.connectRelationModel.fromNode.componentInstance.uniqueId); + } else { + this.connectWizardService.selectedMatch = new Match(null, <Capability>selected , false, null, this.connectWizardService.connectRelationModel.fromNode.componentInstance.uniqueId); } } diff --git a/catalog-ui/src/app/ng2/pages/connection-wizard/properties-step/properties-step.component.html b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/properties-step/properties-step.component.html index 293ebf9822..a8177595a5 100644 --- a/catalog-ui/src/app/ng2/pages/connection-wizard/properties-step/properties-step.component.html +++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/properties-step/properties-step.component.html @@ -13,8 +13,6 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> - - <div class="title"> <span class="capability-name"> {{(connectWizardService.selectedMatch.capability && connectWizardService.selectedMatch.capability.getTitle()) || connectWizardService.selectedMatch.relationship.relation.capability}} diff --git a/catalog-ui/src/app/ng2/pages/connection-wizard/properties-step/properties-step.component.less b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/properties-step/properties-step.component.less index 8e9e07c0d5..c8ad4d38d2 100644 --- a/catalog-ui/src/app/ng2/pages/connection-wizard/properties-step/properties-step.component.less +++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/properties-step/properties-step.component.less @@ -1,4 +1,4 @@ -@import '../../../../../assets/styles/variables'; +@import '../../../../../../../assets/styles/variables'; .title{ margin-bottom: 20px; .capability-name-label{ diff --git a/catalog-ui/src/app/ng2/pages/connection-wizard/properties-step/properties-step.component.ts b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/properties-step/properties-step.component.ts index 946d1858dc..2c12e0daed 100644 --- a/catalog-ui/src/app/ng2/pages/connection-wizard/properties-step/properties-step.component.ts +++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/properties-step/properties-step.component.ts @@ -7,10 +7,10 @@ import {Component, Inject, forwardRef} from '@angular/core'; import {IStepComponent} from "app/models" import {ConnectionWizardService} from "../connection-wizard.service"; -import {PropertyFEModel} from "../../../../models/properties-inputs/property-fe-model"; -import {InstanceFePropertiesMap} from "../../../../models/properties-inputs/property-fe-map"; -import {PropertiesUtils} from "../../properties-assignment/services/properties.utils"; -import {ComponentInstanceServiceNg2} from "../../../services/component-instance-services/component-instance.service"; +import {PropertyFEModel} from "app/models/properties-inputs/property-fe-model"; +import {InstanceFePropertiesMap} from "app/models/properties-inputs/property-fe-map"; +import {PropertiesUtils} from "app/ng2/pages/properties-assignment/services/properties.utils"; +import { ComponentInstanceServiceNg2 } from "app/ng2/services/component-instance-services/component-instance.service"; @Component({ selector: 'properties-step', diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/to-node-step/__snapshots__/to-node-step.component.spec.ts.snap b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/to-node-step/__snapshots__/to-node-step.component.spec.ts.snap new file mode 100644 index 0000000000..ea587bce71 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/to-node-step/__snapshots__/to-node-step.component.spec.ts.snap @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`to-node-step component should match current snapshot 1`] = ` +<to-node-step + connectWizardService={[Function Object]} + optionalCapabilitiesMap={[Function Object]} + optionalRequirementsMap={[Function Object]} + preventBack={[Function Function]} + preventNext={[Function Function]} + updateSelectedReqOrCap={[Function Function]} +> + <select-requirement-or-capability /> +</to-node-step> +`; diff --git a/catalog-ui/src/app/ng2/pages/connection-wizard/to-node-step/to-node-step.component.html b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/to-node-step/to-node-step.component.html index 775a1a7fc2..4892b7fadc 100644 --- a/catalog-ui/src/app/ng2/pages/connection-wizard/to-node-step/to-node-step.component.html +++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/to-node-step/to-node-step.component.html @@ -13,13 +13,10 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> - - <select-requirement-or-capability [optionalRequirementsMap]="optionalRequirementsMap" [optionalCapabilitiesMap]="optionalCapabilitiesMap" [selectedReqOrCapModel]="connectWizardService.selectedMatch.isFromTo ? connectWizardService.selectedMatch.capability : connectWizardService.selectedMatch.requirement" [selectedReqOrCapOption]="displayRequirementsOrCapabilities" - [currentComponent]="connectWizardService.currentComponent" [componentInstanceId]="connectWizardService.connectRelationModel.toNode.componentInstance.uniqueId" (updateSelectedReqOrCap)="updateSelectedReqOrCap($event)"> </select-requirement-or-capability>
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/to-node-step/to-node-step.component.spec.ts b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/to-node-step/to-node-step.component.spec.ts new file mode 100644 index 0000000000..9d453f21dd --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/to-node-step/to-node-step.component.spec.ts @@ -0,0 +1,71 @@ +import {async, ComponentFixture, TestBed} from "@angular/core/testing"; +import {NO_ERRORS_SCHEMA} from "@angular/core"; +import {ToNodeStepComponent} from "./to-node-step.component"; +import {ConnectionWizardService} from "../connection-wizard.service"; +import {ConfigureFn, configureTests} from "../../../../../../../jest/test-config.helper"; +import {Match} from "../../../../../../models/graph/match-relation"; + + +describe('to-node-step component', () => { + + let fixture: ComponentFixture<ToNodeStepComponent>; + let connectionWizardServiceMock: Partial<ConnectionWizardService>; + + beforeEach( + async(() => { + + connectionWizardServiceMock = { + // selectedMatch: new Match(null, null, true, '',''), + selectedMatch: { + isFromTo: false + }, + getOptionalRequirementsByInstanceUniqueId: jest.fn().mockReturnValue(5), + getOptionalCapabilitiesByInstanceUniqueId: jest.fn().mockReturnValue(10) + } + + const configure: ConfigureFn = testBed => { + testBed.configureTestingModule({ + declarations: [ToNodeStepComponent], + imports: [], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + {provide: ConnectionWizardService, useValue: connectionWizardServiceMock} + ], + }); + }; + + configureTests(configure).then(testBed => { + fixture = testBed.createComponent(ToNodeStepComponent); + }); + }) + ); + + + it('should match current snapshot', () => { + expect(fixture).toMatchSnapshot(); + }); + + it('should test the ngOnInit with isFromTo = false', () => { + const component = TestBed.createComponent(ToNodeStepComponent); + let service = TestBed.get(ConnectionWizardService); + service.selectedMatch.isFromTo = false; + component.componentInstance.ngOnInit(); + expect(component.componentInstance.displayRequirementsOrCapabilities).toEqual("Requirement"); + expect(connectionWizardServiceMock.getOptionalRequirementsByInstanceUniqueId).toHaveBeenCalledWith(false, connectionWizardServiceMock.selectedMatch.capability); + expect(component.componentInstance.optionalRequirementsMap).toEqual(5); + expect(component.componentInstance.optionalCapabilitiesMap).toEqual({}); + }); + + + it('should test the ngOnInit with isFromTo = true', () => { + const component = TestBed.createComponent(ToNodeStepComponent); + let service = TestBed.get(ConnectionWizardService); + service.selectedMatch.isFromTo = true; + component.componentInstance.ngOnInit(); + expect(component.componentInstance.displayRequirementsOrCapabilities).toEqual("Capability"); + expect(connectionWizardServiceMock.getOptionalCapabilitiesByInstanceUniqueId).toHaveBeenCalledWith(true, connectionWizardServiceMock.selectedMatch.requirement); + expect(component.componentInstance.optionalCapabilitiesMap).toEqual(10); + expect(component.componentInstance.optionalRequirementsMap).toEqual({}); + }); + +});
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/connection-wizard/to-node-step/to-node-step.component.ts b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/to-node-step/to-node-step.component.ts index ea3b129c7b..67dc381284 100644 --- a/catalog-ui/src/app/ng2/pages/connection-wizard/to-node-step/to-node-step.component.ts +++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/to-node-step/to-node-step.component.ts @@ -2,10 +2,10 @@ import {Component, forwardRef, Inject} from '@angular/core'; import {IStepComponent} from "app/models" import {Dictionary} from "lodash"; import {ConnectionWizardService} from "../connection-wizard.service"; -import {Match} from "../../../../models/graph/match-relation"; -import {Requirement} from "../../../../models/requirement"; -import {Capability} from "../../../../models/capability"; -import {PropertyModel} from "../../../../models/properties"; +import {Match} from "app/models/graph/match-relation"; +import {Requirement} from "app/models/requirement"; +import {Capability} from "app/models/capability"; +import {PropertyModel} from "app/models/properties"; @Component({ selector: 'to-node-step', diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/__snapshots__/link-row.component.spec.ts.snap b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/__snapshots__/link-row.component.spec.ts.snap new file mode 100644 index 0000000000..094f41bd84 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/__snapshots__/link-row.component.spec.ts.snap @@ -0,0 +1,29 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`artifact form component should match current snapshot of artifact form component 1`] = ` +<link-row + source={[Function Array]} + srcCP={[Function Array]} + target={[Function Array]} + targetCP={[Function Array]} +> + <ui-element-dropdown + class="cell link-selector" + data-tests-id="linkSrc" + /><ui-element-dropdown + class="cell link-selector" + data-tests-id="linkSrcCP" + /><ui-element-dropdown + class="cell link-selector" + data-tests-id="linkTarget" + /><ui-element-dropdown + class="cell link-selector" + data-tests-id="linkTargetCP" + /><div + class="cell remove" + data-tests-id="removeLnk" + > + + </div> +</link-row> +`; diff --git a/catalog-ui/src/app/ng2/pages/service-path-creator/link-row/link-row.component.html b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/link-row.component.html index 0abdda1cc6..0abdda1cc6 100644 --- a/catalog-ui/src/app/ng2/pages/service-path-creator/link-row/link-row.component.html +++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/link-row.component.html diff --git a/catalog-ui/src/app/ng2/pages/service-path-creator/link-row/link-row.component.less b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/link-row.component.less index beec9bd567..2a1d0d98c8 100644 --- a/catalog-ui/src/app/ng2/pages/service-path-creator/link-row/link-row.component.less +++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/link-row.component.less @@ -1,4 +1,4 @@ -@import './../../../../../assets/styles/variables.less'; +@import './../../../../../../../assets/styles/variables.less'; .remove { display: flex; align-items: center; diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/link-row.component.spec.ts b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/link-row.component.spec.ts new file mode 100644 index 0000000000..5cbad6ea5d --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/link-row.component.spec.ts @@ -0,0 +1,478 @@ +import {async, ComponentFixture} from "@angular/core/testing"; +import {CacheService} from "../../../../../services/cache.service"; +import {ConfigureFn, configureTests} from "../../../../../../../jest/test-config.helper"; +import {NO_ERRORS_SCHEMA} from "@angular/core"; +import {LinkRowComponent} from "./link-row.component"; +import {DropdownValue} from "../../../../../components/ui/form-components/dropdown/ui-element-dropdown.component"; +import {MapItemData, ServicePathMapItem} from "../../../../../../models/graph/nodes-and-links-map"; + +describe('artifact form component', () => { + + let fixture: ComponentFixture<LinkRowComponent>; + let cacheServiceMock: Partial<CacheService>; + + beforeEach( + async(() => { + + + cacheServiceMock = { + contains: jest.fn(), + remove: jest.fn(), + set: jest.fn(), + get: jest.fn() + } + + const configure: ConfigureFn = testBed => { + testBed.configureTestingModule({ + declarations: [LinkRowComponent], + imports: [], + schemas: [NO_ERRORS_SCHEMA], + providers: [] + , + }); + }; + + configureTests(configure).then(testBed => { + fixture = testBed.createComponent(LinkRowComponent); + }); + }) + ); + + + it('should match current snapshot of artifact form component', () => { + expect(fixture).toMatchSnapshot(); + }); + + + it('ngOnChanges() -> in case data exist -> call to parseInitialData()' ,() => { + // init values / mock functions + let data = 'something'; + fixture.componentInstance.parseInitialData = jest.fn(); + fixture.componentInstance.data = data; + + // call to the tested function + fixture.componentInstance.ngOnChanges(); + + // expect that + expect(fixture.componentInstance.parseInitialData).toHaveBeenCalledWith(data); + }); + + it('onSourceSelected() -> in case id -> srcCP, link.fromCP, link.toNode, link.toCP, target, targetCP should be updated accordingly' ,() => { + // init values / mock functions + let id = 'id'; + let data = 'data'; + let link = { + fromCP:'testVal', + toNode:'testVal', + toCP:'testVal' + } + let target = ['val1', 'val2']; + let targetCP = ['val1', 'val2']; + + fixture.componentInstance.findOptions = jest.fn(); + fixture.componentInstance.convertValuesToDropDownOptions = jest.fn(() => 'dummyConvertedVal'); + fixture.componentInstance.data = data; + fixture.componentInstance.link = link; + fixture.componentInstance.target = target; + fixture.componentInstance.targetCP = targetCP; + + // call to the tested function + fixture.componentInstance.onSourceSelected(id); + + // expect that + expect(fixture.componentInstance.findOptions).toHaveBeenCalledWith(data, id); + expect(fixture.componentInstance.srcCP).toBe('dummyConvertedVal'); + expect(fixture.componentInstance.link.fromCP).toBe(''); + expect(fixture.componentInstance.link.toNode).toBe(''); + expect(fixture.componentInstance.link.toCP).toBe(''); + expect(fixture.componentInstance.target.length).toBe(0); + expect(fixture.componentInstance.targetCP.length).toBe(0); + }); + + it('onSourceSelected() -> in case id undefined -> No Change to srcCP, link.fromCP, link.toNode, link.toCP, target, targetCP' ,() => { + // init values / mock functions + let id; + let data = 'data'; + let link = { + fromCP:'testVal', + toNode:'testVal', + toCP:'testVal' + } + let target = ['val1', 'val2']; + let targetCP = ['val1', 'val2']; + + fixture.componentInstance.findOptions = jest.fn(); + fixture.componentInstance.convertValuesToDropDownOptions = jest.fn(() => 'dummyConvertedVal'); + fixture.componentInstance.data = data; + fixture.componentInstance.link = link; + fixture.componentInstance.target = target; + fixture.componentInstance.targetCP = targetCP; + + // call to the tested function + fixture.componentInstance.onSourceSelected(id); + + // expect that + expect(fixture.componentInstance.link.fromCP).toBe(link.fromCP); + expect(fixture.componentInstance.link.toNode).toBe(link.toNode); + expect(fixture.componentInstance.link.toCP).toBe(link.toCP); + expect(fixture.componentInstance.target.length).toBe(2); + expect(fixture.componentInstance.target[0]).toBe('val1') + expect(fixture.componentInstance.targetCP.length).toBe(2); + expect(fixture.componentInstance.targetCP[1]).toBe('val2'); + }); + + it('onSrcCPSelected() -> in case id -> Verify target, link.fromCPOriginId, link.toNode, link.toCP, targetCP.length' ,() => { + // init values / mock functions + let id = 'id'; + let link = { + fromNode:'testVal', + toCPOriginId: 'initValue_ShouldBeChanged' + }; + let option1 = { + id: 'something' + }; + let option2 = { + id: 'id', + data: {"ownerId":1} + }; + + fixture.componentInstance.link = link; + fixture.componentInstance.findOptions = jest.fn(() => [option1, option2]); + fixture.componentInstance.convertValuesToDropDownOptions = jest.fn(() => 'dummyConvertedVal'); + + // call to the tested function + fixture.componentInstance.onSrcCPSelected(id); + + // expect that + expect(fixture.componentInstance.target).toBe('dummyConvertedVal'); + expect(fixture.componentInstance.link.fromCPOriginId).toBe(option2.data.ownerId); + expect(fixture.componentInstance.link.toNode).toBe(''); + expect(fixture.componentInstance.link.toCP).toBe(''); + expect(fixture.componentInstance.targetCP.length).toBe(0); + + }); + + it('onSrcCPSelected() -> in case id undefined -> Verify target, link.fromCPOriginId, link.toNode, link.toCP, targetCP.length' ,() => { + // init values / mock functions + let id; + + let targetInput:Array<DropdownValue> = [{value:'Value', label:'Label', hidden:true, selected:true}]; + + let linkInput = { + fromCPOriginId:'expectedLinkFromCPOriginId', + toNode:'expectedLinkToNode', + toCP:'expectedLinkToCP', + // Link Object + canEdit:true, + canRemove:true, + isFirst:true, + // ForwardingPathLink Object + ownerId:'', + fromNode:'', + fromCP:'', + toCPOriginId:'' + } + + fixture.componentInstance.target = targetInput; + fixture.componentInstance.link = linkInput; + fixture.componentInstance.targetCP = targetInput; + + + // call to the tested function + fixture.componentInstance.onSrcCPSelected(id); + + // expect that + expect(fixture.componentInstance.target).toBe(targetInput); + expect(fixture.componentInstance.link.fromCPOriginId).toBe('expectedLinkFromCPOriginId'); + expect(fixture.componentInstance.link.toNode).toBe('expectedLinkToNode'); + expect(fixture.componentInstance.link.toCP).toBe('expectedLinkToCP'); + expect(fixture.componentInstance.targetCP.length).toBe(1); + }); + + it('onTargetSelected() -> in case id -> Verify targetCP & link.toCP' ,() => { + // init values / mock functions + let id = 'id'; + let link = { + toCP:'testVal' + } + let targetCP = ['val1', 'val2']; + + fixture.componentInstance.findOptions = jest.fn(); + fixture.componentInstance.convertValuesToDropDownOptions = jest.fn(() => 'dummyConvertedVal'); + fixture.componentInstance.link = link; + fixture.componentInstance.targetCP = targetCP; + + // call to the tested function + fixture.componentInstance.onTargetSelected(id); + + // expect that + expect(fixture.componentInstance.targetCP).toBe('dummyConvertedVal'); + expect(fixture.componentInstance.link.toCP).toBe(''); + + }); + + it('onTargetSelected() -> in case id undefined -> Verify targetCP & link.toCP' ,() => { + // init values / mock functions + let id; + let link = { + toCP:'toCP_testVal' + } + let targetCP = ['val1', 'val2']; + + fixture.componentInstance.findOptions = jest.fn(); + fixture.componentInstance.convertValuesToDropDownOptions = jest.fn(() => 'dummyConvertedVal'); + fixture.componentInstance.link = link; + fixture.componentInstance.targetCP = targetCP; + + // call to the tested function + fixture.componentInstance.onTargetSelected(id); + + // expect that + expect(fixture.componentInstance.targetCP.length).toBe(2); + expect(fixture.componentInstance.targetCP).toEqual(['val1', 'val2']); + expect(fixture.componentInstance.link.toCP).toBe('toCP_testVal'); + }); + + it('onTargetCPSelected() -> in case id -> Validate toCPOriginId' ,() => { + // init values / mock functions + let id = 'id'; + let link = { + toNode:'testVal', + toCPOriginId: 'initValue_ShouldBeChanged' + }; + let option1 = { + id: 'something' + }; + let option2 = { + id: 'id', + data: {"ownerId":1} + }; + fixture.componentInstance.link = link; + fixture.componentInstance.findOptions = jest.fn(() => [option1, option2]); + + // call to the tested function + fixture.componentInstance.onTargetCPSelected(id); + + // expect that + expect(fixture.componentInstance.link.toCPOriginId).toBe(option2.data.ownerId); + }); + + it('onTargetCPSelected() -> in case id undefined -> Validate toCPOriginId' ,() => { + // init values / mock functions + let id; + let link = { + toNode:'testVal', + toCPOriginId: 'initValue_ShouldRemain' + }; + let option1 = { + id: 'something' + }; + let option2 = { + id: 'id', + data: {"ownerId":1} + }; + fixture.componentInstance.link = link; + fixture.componentInstance.findOptions = jest.fn(() => [option1, option2]); + + // call to the tested function + fixture.componentInstance.onTargetCPSelected(id); + + // expect that + expect(fixture.componentInstance.link.toCPOriginId).toBe('initValue_ShouldRemain'); + }); + + + it('findOptions() -> in case item.data.options -> Validate return item.data.options' ,() => { + // init values / mock functions + const innerMapItemData1: MapItemData = { id: 'innerMapItemData1_id', name: 'innerMapItemData1_name', options: []}; + const innerServicePathItem: ServicePathMapItem = { id: 'innerServicePathItem_id', data: innerMapItemData1 }; + const mapItemData1: MapItemData = { id: 'mapItemData1_id', name: 'mapItemData1_name', options: [innerServicePathItem]}; + + const servicePathItem: ServicePathMapItem = { id: 'servicePathItem_id', data: mapItemData1 }; + const arrServicePathItems: ServicePathMapItem[] = [servicePathItem]; + + let nodeOrCPId: string = servicePathItem.id; + + // call to the tested function + let res = fixture.componentInstance.findOptions(arrServicePathItems, nodeOrCPId); + + // expect that + expect(res).toEqual([innerServicePathItem]); + }); + + it('findOptions() -> in case NOT item || item.data || item.data.options -> Validate return null' ,() => { + // init values / mock functions + let item = [{ + // data: { + data:{ + name:'data_name', + id: 'data_id' + }, + name:'name', + id: 'id' + // } + }]; + let items: Array<ServicePathMapItem> = item; + let nodeOrCPId: string = 'someString'; + + // call to the tested function + let res = fixture.componentInstance.findOptions(items, nodeOrCPId); + + // expect that + expect(res).toBe(null); + }); + + it('convertValuesToDropDownOptions() -> Verify that the result is sorted' ,() => { + // init values / mock functions + const mapItemData1: MapItemData = { id: 'Z_ID', name: 'Z_NAME'}; + const servicePathItem1: ServicePathMapItem = { id: 'Z_servicePathItem_id', data: mapItemData1 }; + + const mapItemData2: MapItemData = { id: 'A_ID', name: 'A_NAME'}; + const servicePathItem2: ServicePathMapItem = { id: 'A_servicePathItem_id', data: mapItemData2 }; + + const mapItemData3: MapItemData = { id: 'M_ID', name: 'M_NAME'}; + const servicePathItem3: ServicePathMapItem = { id: 'M_servicePathItem_id', data: mapItemData3 }; + + const arrServicePathItems: ServicePathMapItem[] = [servicePathItem1, servicePathItem2, servicePathItem3]; + + // call to the tested function + let res = fixture.componentInstance.convertValuesToDropDownOptions(arrServicePathItems); + + // expect that + expect(res.length).toBe(3); + expect(res[0].value).toBe("A_servicePathItem_id"); + expect(res[0].label).toBe("A_NAME"); + expect(res[1].value).toBe("M_servicePathItem_id"); + expect(res[1].label).toBe("M_NAME"); + expect(res[2].value).toBe("Z_servicePathItem_id"); + expect(res[2].label).toBe("Z_NAME"); + + }); + + it('parseInitialData() -> link.fromNode Exist => Verify srcCP' ,() => { + // init values / mock functions + + //Simulate Array<ServicePathMapItem to pass to the function + const mapItemData1: MapItemData = { id: 'mapItemID', name: 'mapItemName'}; + const servicePathItem1: ServicePathMapItem = { id: 'servicePathItemId', data: mapItemData1 }; + const arrServicePathItems: ServicePathMapItem[] = [servicePathItem1]; + + //Simulate link + let link = { + fromNode:'testVal' + }; + fixture.componentInstance.link = link; + + //Simulate the response from convertValuesToDropDownOptions() + const value = "expected_id_fromNode"; + const label = "expected_label_fromNode" + let result:Array<DropdownValue> = []; + result[0] = new DropdownValue(value, label); + fixture.componentInstance.convertValuesToDropDownOptions = jest.fn(() => result); + + //Simulate the response from findOptions() + const innerMapItemData1: MapItemData = { id: 'innerMapItemData1_id', name: 'innerMapItemData1_name', options: []}; + const options: ServicePathMapItem = { id: 'innerServicePathItem_id', data: innerMapItemData1 }; + fixture.componentInstance.findOptions = jest.fn(() => options); + + + // call to the tested function + fixture.componentInstance.parseInitialData(arrServicePathItems); + + // expect that + expect(fixture.componentInstance.srcCP.length).toBe(1); + expect(fixture.componentInstance.srcCP[0]).toEqual({ + "value": value, + "label": label, + "hidden": false, + "selected": false + }); + }); + + it('parseInitialData() -> link.fromNode & link.fromCP Exist => Verify srcCP' ,() => { + // init values / mock functions + + //Simulate Array<ServicePathMapItem to pass to the function + const mapItemData1: MapItemData = { id: 'mapItemID', name: 'mapItemName'}; + const servicePathItem1: ServicePathMapItem = { id: 'servicePathItemId', data: mapItemData1 }; + const arrServicePathItems: ServicePathMapItem[] = [servicePathItem1]; + + //Simulate link + let link = { + fromNode:'testVal', + fromCP: 'testVal' + }; + fixture.componentInstance.link = link; + + //Simulate the response from convertValuesToDropDownOptions() + const value = "expected_id_fromNode_and_fromCP"; + const label = "expected_label_fromNode_and_fromCP" + let result:Array<DropdownValue> = []; + result[0] = new DropdownValue(value, label); + fixture.componentInstance.convertValuesToDropDownOptions = jest.fn(() => result); + + //Simulate the response from findOptions() + const innerMapItemData1: MapItemData = { id: 'innerMapItemData1_id', name: 'innerMapItemData1_name', options: []}; + const options: ServicePathMapItem = { id: 'innerServicePathItem_id', data: innerMapItemData1 }; + fixture.componentInstance.findOptions = jest.fn(() => options); + + + // call to the tested function + fixture.componentInstance.parseInitialData(arrServicePathItems); + + // expect that + expect(fixture.componentInstance.srcCP.length).toBe(1); + expect(fixture.componentInstance.srcCP[0]).toEqual({ + "value": value, + "label": label, + "hidden": false, + "selected": false + }); + }); + + + it('parseInitialData() -> link.fromNode & link.fromCP & link.toNode Exist => Verify srcCP' ,() => { + // init values / mock functions + + //Simulate Array<ServicePathMapItem to pass to the function + const mapItemData1: MapItemData = { id: 'mapItemID', name: 'mapItemName'}; + const servicePathItem1: ServicePathMapItem = { id: 'servicePathItemId', data: mapItemData1 }; + const arrServicePathItems: ServicePathMapItem[] = [servicePathItem1]; + + //Simulate link + let link = { + fromNode:'testVal', + fromCP: 'testVal', + toNode: 'testVal' + }; + fixture.componentInstance.link = link; + + //Simulate the response from convertValuesToDropDownOptions() + const value = "expected_id_fromNode_and_fromCP_and_toNode"; + const label = "expected_label_fromNode_and_fromCP_and_toNode" + let result:Array<DropdownValue> = []; + result[0] = new DropdownValue(value, label); + fixture.componentInstance.convertValuesToDropDownOptions = jest.fn(() => result); + + //Simulate the response from findOptions() + const innerMapItemData1: MapItemData = { id: 'innerMapItemData1_id', name: 'innerMapItemData1_name', options: []}; + const options: ServicePathMapItem = { id: 'innerServicePathItem_id', data: innerMapItemData1 }; + fixture.componentInstance.findOptions = jest.fn(() => options); + + + // call to the tested function + fixture.componentInstance.parseInitialData(arrServicePathItems); + + // expect that + expect(fixture.componentInstance.srcCP.length).toBe(1); + expect(fixture.componentInstance.srcCP[0]).toEqual({ + "value": value, + "label": label, + "hidden": false, + "selected": false + }); + }); + + + +});
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/service-path-creator/link-row/link-row.component.ts b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/link-row.component.ts index e4fc1d4522..83c30b1a60 100644 --- a/catalog-ui/src/app/ng2/pages/service-path-creator/link-row/link-row.component.ts +++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/link-row.component.ts @@ -2,6 +2,7 @@ import {Component, Input} from '@angular/core'; import {DropdownValue} from "app/ng2/components/ui/form-components/dropdown/ui-element-dropdown.component"; import {Link} from './link.model'; import {ServicePathMapItem} from "app/models/graph/nodes-and-links-map"; +import * as _ from "lodash"; @Component({ selector: 'link-row', diff --git a/catalog-ui/src/app/ng2/pages/service-path-creator/link-row/link.model.ts b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/link.model.ts index 80128eb42e..80128eb42e 100644 --- a/catalog-ui/src/app/ng2/pages/service-path-creator/link-row/link.model.ts +++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/link.model.ts diff --git a/catalog-ui/src/app/ng2/pages/service-path-creator/service-path-creator.component.html b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/service-path-creator.component.html index cc14b4961f..db0d912934 100644 --- a/catalog-ui/src/app/ng2/pages/service-path-creator/service-path-creator.component.html +++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/service-path-creator.component.html @@ -13,7 +13,6 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> - <div class="service-path-creator"> <form class="w-sdc-form"> <div class="i-sdc-form-item" > diff --git a/catalog-ui/src/app/ng2/pages/service-path-creator/service-path-creator.component.less b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/service-path-creator.component.less index 5c9e53e229..2a3efbdd3c 100644 --- a/catalog-ui/src/app/ng2/pages/service-path-creator/service-path-creator.component.less +++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/service-path-creator.component.less @@ -1,4 +1,4 @@ -@import './../../../../assets/styles/variables.less'; +@import './../../../../../../assets/styles/variables.less'; .service-path-creator { font-family: @font-opensans-regular; .separator-buttons { diff --git a/catalog-ui/src/app/ng2/pages/service-path-creator/service-path-creator.component.ts b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/service-path-creator.component.ts index bffb1c5e7e..17c2081a75 100644 --- a/catalog-ui/src/app/ng2/pages/service-path-creator/service-path-creator.component.ts +++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/service-path-creator.component.ts @@ -25,6 +25,7 @@ import {ForwardingPath} from 'app/models/forwarding-path'; import {ServiceServiceNg2} from "app/ng2/services/component-services/service.service"; import {ForwardingPathLink} from "app/models/forwarding-path-link"; import {ServicePathMapItem} from "app/models/graph/nodes-and-links-map"; +import {CompositionService} from "app/ng2/pages/composition/composition.service"; @Component({ selector: 'service-path-creator', @@ -43,7 +44,8 @@ export class ServicePathCreatorComponent { forwardingPath:ForwardingPath; //isExtendAllowed:boolean = false; - constructor(private serviceService: ServiceServiceNg2) { + constructor(private serviceService: ServiceServiceNg2, + private compositionService: CompositionService) { this.forwardingPath = new ForwardingPath(); this.links = [new Link(new ForwardingPathLink('', '', '', '', '', ''), true, false, true)]; this.headers = ['Source', 'Source Connection Point', 'Target', 'Target Connection Point', ' ']; @@ -57,7 +59,7 @@ export class ServicePathCreatorComponent { } ngOnInit() { - this.serviceService.getNodesAndLinksMap(this.input.service).subscribe((res:any) => { + this.serviceService.getNodesAndLinksMap(this.input.serviceId).subscribe((res:any) => { this.linksMap = res; }); this.processExistingPath(); @@ -66,7 +68,7 @@ export class ServicePathCreatorComponent { private processExistingPath() { if (this.input.pathId) { - let forwardingPath = <ForwardingPath>{...this.input.service.forwardingPaths[this.input.pathId]}; + let forwardingPath = <ForwardingPath>{...this.compositionService.forwardingPaths[this.input.pathId]}; this.forwardingPath.name = forwardingPath.name; this.forwardingPath.destinationPortNumber = forwardingPath.destinationPortNumber; this.forwardingPath.protocol = forwardingPath.protocol; diff --git a/catalog-ui/src/app/ng2/pages/service-path-creator/service-path-creator.module.ts b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/service-path-creator.module.ts index 78005317a2..78005317a2 100644 --- a/catalog-ui/src/app/ng2/pages/service-path-creator/service-path-creator.module.ts +++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/service-path-creator.module.ts diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/service-path-selector/service-path-selector.component.html b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-selector/service-path-selector.component.html new file mode 100644 index 0000000000..e1a4f68a9b --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-selector/service-path-selector.component.html @@ -0,0 +1,27 @@ +<!-- + ~ Copyright (C) 2018 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. + --> + +<div class="service-path-selector"> + <label>Service Flows:</label> + <ui-element-dropdown + class="path-dropdown" + data-tests-id="service-path-selector" + [readonly]="dropdownOptions.length < 3" + [(value)]="selectedPathId" + [values]="dropdownOptions" + (valueChange)="onSelectPath()"> + </ui-element-dropdown> +</div> diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/service-path-selector/service-path-selector.component.less b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-selector/service-path-selector.component.less new file mode 100644 index 0000000000..f618d6b6f4 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-selector/service-path-selector.component.less @@ -0,0 +1,24 @@ +@import './../../../../../../assets/styles/variables.less'; +.service-path-selector { + margin: 10px 35px 10px 0; + display: flex; + font-size: 12px; + + /deep/ .path-dropdown { + width: 150px; + select { + font-size: 14px; + font-family: @font-opensans-regular; + padding: 4px 10px; + } + } + + label { + margin-right: 10px; + align-self: center; + font-size: 14px; + font-family: @font-opensans-regular; + font-weight: normal; + margin-bottom: initial; + } +} diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/service-path-selector/service-path-selector.component.ts b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-selector/service-path-selector.component.ts new file mode 100644 index 0000000000..0dba906f64 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-selector/service-path-selector.component.ts @@ -0,0 +1,142 @@ +import {Component, Input, KeyValueDiffer, IterableDiffers, KeyValueDiffers, DoCheck} from '@angular/core'; +import {Service} from "app/models/components/service"; +import {TranslateService} from "app/ng2/shared/translator/translate.service"; +import {ForwardingPath} from "app/models/forwarding-path"; +import {DropdownValue} from "app/ng2/components/ui/form-components/dropdown/ui-element-dropdown.component"; +import {CompositionService} from "app/ng2/pages/composition/composition.service"; +import {EventListenerService} from "app/services/event-listener-service"; +import {GRAPH_EVENTS} from "app/utils/constants"; + +@Component({ + selector: 'service-path-selector', + templateUrl: './service-path-selector.component.html', + styleUrls: ['service-path-selector.component.less'] +}) + +export class ServicePathSelectorComponent { + + defaultSelectedId: string; + hideAllValue: string; + hideAllId: string = '0'; + showAllValue: string; + showAllId: string = '1'; + + paths: Array<ForwardingPath> = []; + dropdownOptions: Array<DropdownValue>; + differ: KeyValueDiffer<string, ForwardingPath>; + + @Input() drawPath: Function; + @Input() deletePaths: Function; + @Input() selectedPathId: string; + + constructor(private differs: KeyValueDiffers, + private translateService: TranslateService, + private compositionService: CompositionService, + private eventListenerService: EventListenerService + ) { + + this.defaultSelectedId = this.hideAllId; + this.convertPathsToDropdownOptions(); + + this.translateService.languageChangedObservable.subscribe(lang => { + this.hideAllValue = this.translateService.translate("SERVICE_PATH_SELECTOR_HIDE_ALL_VALUE"); + this.showAllValue = this.translateService.translate("SERVICE_PATH_SELECTOR_SHOW_ALL_VALUE"); + this.convertPathsToDropdownOptions(); + }); + + } + + ngOnInit(): void { + + this.selectedPathId = this.defaultSelectedId; + this.differ = this.differs.find(this.compositionService.forwardingPaths).create(); + this.updatePaths(); + this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_SERVICE_PATH_CREATED, (createdId) => { + this.selectedPathId = createdId; + this.updatePaths(); + } ) + + } + + updatePaths(): void { + + const pathsChanged = this.differ.diff(this.compositionService.forwardingPaths); + + if (pathsChanged) { + let oldPaths = _.cloneDeep(this.paths); + this.populatePathsFromService(); + + if (!(_.isEqual(oldPaths, this.paths))) { + this.convertPathsToDropdownOptions(); + + let temp = this.selectedPathId; + this.selectedPathId = '-1'; + + setTimeout(() => { + this.selectedPathId = temp; + this.onSelectPath(); + }, 0); + } + } + + } + + populatePathsFromService(): void { + + this.paths = []; + + _.forEach(this.compositionService.forwardingPaths, path => { + this.paths.push(path); + }); + this.paths.sort((a: ForwardingPath, b: ForwardingPath) => { + return a.name.localeCompare(b.name); + }); + + } + + convertPathsToDropdownOptions(): void { + + let result = [ + new DropdownValue(this.hideAllId, this.hideAllValue), + new DropdownValue(this.showAllId, this.showAllValue) + ]; + + _.forEach(this.paths, (value: ForwardingPath) => { + result[result.length] = new DropdownValue(value.uniqueId, value.name); + }); + + this.dropdownOptions = result; + + } + + onSelectPath = (): void => { + + if (this.selectedPathId !== '-1') { + this.deletePaths(); + + switch (this.selectedPathId) { + case this.hideAllId: + break; + + case this.showAllId: + _.forEach(this.paths, path => + this.drawPath(path) + ); + break; + + default: + let path = this.paths.find(path => + path.uniqueId === this.selectedPathId + ); + if (!path) { + this.selectedPathId = this.defaultSelectedId; + this.onSelectPath(); // currently does nothing in default case, but if one day it does, we want the selection to behave accordingly. + break; + } + this.drawPath(path); + break; + } + } + + } +} diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/service-path-selector/service-path-selector.module.ts b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-selector/service-path-selector.module.ts new file mode 100644 index 0000000000..6782c88b76 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-selector/service-path-selector.module.ts @@ -0,0 +1,22 @@ +import { NgModule } from "@angular/core"; +import {CommonModule} from "@angular/common"; +import {ServicePathSelectorComponent} from "./service-path-selector.component"; +import {UiElementsModule} from "app/ng2/components/ui/ui-elements.module"; +import {CompositionService} from "app/ng2/pages/composition/composition.service"; + +@NgModule({ + declarations: [ + ServicePathSelectorComponent + ], + imports: [ + CommonModule, + UiElementsModule + ], + exports: [ServicePathSelectorComponent], + entryComponents: [ + ServicePathSelectorComponent + ], + providers: [CompositionService] +}) +export class ServicePathSelectorModule { +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/service-paths-list/service-paths-list.component.html b/catalog-ui/src/app/ng2/pages/composition/graph/service-paths-list/service-paths-list.component.html index 33a0090372..39c41916a2 100644 --- a/catalog-ui/src/app/ng2/pages/service-paths-list/service-paths-list.component.html +++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-paths-list/service-paths-list.component.html @@ -1,19 +1,3 @@ -<!-- - ~ Copyright (C) 2018 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. - --> - <div class="service-path-list"> <div class="add-path-link" *ngIf="!isViewOnly"><a (click)="onAddServicePath()" data-tests-id="add-service-path-lnk" >+ Add Flow</a></div> <div class="generic-table table-container" > diff --git a/catalog-ui/src/app/ng2/pages/service-paths-list/service-paths-list.component.less b/catalog-ui/src/app/ng2/pages/composition/graph/service-paths-list/service-paths-list.component.less index 291119f58c..17f70926ff 100644 --- a/catalog-ui/src/app/ng2/pages/service-paths-list/service-paths-list.component.less +++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-paths-list/service-paths-list.component.less @@ -1,4 +1,4 @@ -@import './../../../../assets/styles/variables.less'; +@import './../../../../../../assets/styles/variables.less'; .add-path-link { display: flex; diff --git a/catalog-ui/src/app/ng2/pages/service-paths-list/service-paths-list.component.ts b/catalog-ui/src/app/ng2/pages/composition/graph/service-paths-list/service-paths-list.component.ts index 1625ab4b66..81abe42cb3 100644 --- a/catalog-ui/src/app/ng2/pages/service-paths-list/service-paths-list.component.ts +++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-paths-list/service-paths-list.component.ts @@ -24,6 +24,7 @@ import {ForwardingPath} from "app/models/forwarding-path"; import {ServiceServiceNg2} from "app/ng2/services/component-services/service.service"; import {ModalService} from "app/ng2/services/modal.service"; import {ModalComponent} from "app/ng2/components/ui/modal/modal.component"; +import {CompositionService} from "app/ng2/pages/composition/composition.service"; @Component({ selector: 'service-paths-list', @@ -31,7 +32,7 @@ import {ModalComponent} from "app/ng2/components/ui/modal/modal.component"; styleUrls:['service-paths-list.component.less'], providers: [ServiceServiceNg2, ModalService] }) -export default class ServicePathsListComponent { +export class ServicePathsListComponent { modalInstance: ComponentRef<ModalComponent>; headers: Array<string> = []; paths: Array<ForwardingPath> = []; @@ -40,12 +41,13 @@ export default class ServicePathsListComponent { onEditServicePath: Function; isViewOnly: boolean; - constructor(private serviceService:ServiceServiceNg2) { + constructor(private serviceService:ServiceServiceNg2, + private compositionService: CompositionService) { this.headers = ['Flow Name','Actions']; } ngOnInit() { - _.forEach(this.input.service.forwardingPaths, (path: ForwardingPath)=> { + _.forEach(this.compositionService.forwardingPaths, (path: ForwardingPath)=> { this.paths[this.paths.length] = path; }); this.paths.sort((a:ForwardingPath, b:ForwardingPath)=> { @@ -57,8 +59,8 @@ export default class ServicePathsListComponent { } deletePath = (id:string):void => { - this.serviceService.deleteServicePath(this.input.service, id).subscribe((res:any) => { - delete this.input.service.forwardingPaths[id]; + this.serviceService.deleteServicePath(this.input.serviceId, id).subscribe((res:any) => { + delete this.compositionService.forwardingPaths[id]; this.paths = this.paths.filter(function(path){ return path.uniqueId !== id; }); diff --git a/catalog-ui/src/app/ng2/pages/service-paths-list/service-paths-list.module.ts b/catalog-ui/src/app/ng2/pages/composition/graph/service-paths-list/service-paths-list.module.ts index c236934002..5121627a9d 100644 --- a/catalog-ui/src/app/ng2/pages/service-paths-list/service-paths-list.module.ts +++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-paths-list/service-paths-list.module.ts @@ -1,6 +1,6 @@ import { NgModule } from "@angular/core"; import {CommonModule} from "@angular/common"; -import ServicePathsListComponent from "./service-paths-list.component"; +import { ServicePathsListComponent } from "./service-paths-list.component"; @NgModule({ declarations: [ diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-general-utils.ts b/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-general-utils.ts new file mode 100644 index 0000000000..bc8bd691c9 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-general-utils.ts @@ -0,0 +1,268 @@ +/*- + * ============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========================================================= + */ + +import * as _ from "lodash"; +import {ComponentInstance, Match, CompositionCiLinkBase, CompositionCiNodeUcpeCp} from "app/models"; +import {Dictionary, GraphUIObjects} from "app/utils"; +import {MatchCapabilitiesRequirementsUtils} from "./match-capability-requierment-utils"; +import {CommonGraphUtils} from "../common/common-graph-utils"; +import {Injectable} from "@angular/core"; +import {QueueServiceUtils} from "app/ng2/utils/queue-service-utils"; +import {ComponentServiceNg2} from "app/ng2/services/component-services/component.service"; +import {RequirementsGroup} from "app/models/requirement"; +import {CapabilitiesGroup} from "app/models/capability"; +import {TopologyTemplateService} from "app/ng2/services/component-services/topology-template.service"; +import {CompositionService} from "../../composition.service"; +import {WorkspaceService} from "app/ng2/pages/workspace/workspace.service"; +import {NotificationsService} from "onap-ui-angular/dist/notifications/services/notifications.service"; +import {NotificationSettings} from "onap-ui-angular/dist/notifications/utilities/notification.config"; + +export interface RequirementAndCapabilities { + capabilities: CapabilitiesGroup; + requirements: RequirementsGroup; +} + +@Injectable() +export class CompositionGraphGeneralUtils { + + public componentRequirementsAndCapabilitiesCaching = new Dictionary<string, RequirementAndCapabilities>(); + + constructor(private commonGraphUtils: CommonGraphUtils, + private matchCapabilitiesRequirementsUtils: MatchCapabilitiesRequirementsUtils, + private queueServiceUtils: QueueServiceUtils, + private componentService: ComponentServiceNg2, + private topologyTemplateService: TopologyTemplateService, + private compositionService: CompositionService, + private workspaceService: WorkspaceService) { + } + + /** + * Get the offset for the link creation Menu + * @param point + * @returns {Cy.Position} + */ + public calcMenuOffset: Function = (point: Cy.Position): Cy.Position => { + point.x = point.x + 60; + point.y = point.y + 105; + return point; + }; + + /** + * return the top left position of the link menu + * @param cy + * @param targetNodePosition + * @returns {Cy.Position} + */ + public getLinkMenuPosition = (cy: Cy.Instance, targetNodePosition: Cy.Position) => { + let menuPosition: Cy.Position = this.calcMenuOffset(targetNodePosition); //get the link mid point + if ($(document.body).height() < menuPosition.y + GraphUIObjects.LINK_MENU_HEIGHT + $(document.getElementsByClassName('sdc-composition-graph-wrapper')).offset().top) { // if position menu is overflow bottom + menuPosition.y = $(document.body).height() - GraphUIObjects.TOP_HEADER_HEIGHT - GraphUIObjects.LINK_MENU_HEIGHT; + } + return menuPosition; + }; + + public zoomGraphTo = (cy: Cy.Instance, zoomLevel: number): void => { + let zy = cy.height() / 2; + let zx = cy.width() / 2; + cy.zoom({ + level: zoomLevel, + renderedPosition: {x: zx, y: zy} + }); + } + + //saves the current zoom, and then sets a temporary maximum zoom for zoomAll, and then reverts to old value + public zoomAllWithMax = (cy: Cy.Instance, maxZoom: number): void => { + + let oldMaxZoom: number = cy.maxZoom(); + + cy.maxZoom(maxZoom); + this.zoomAll(cy); + cy.maxZoom(oldMaxZoom); + + }; + + //Zooms to fit all of the nodes in the collection passed in. If no nodes are passed in, will zoom to fit all nodes on graph + public zoomAll = (cy: Cy.Instance, nodes?: Cy.CollectionNodes): void => { + + if (!nodes || !nodes.length) { + nodes = cy.nodes(); + } + + cy.resize(); + cy.animate({ + fit: {eles: nodes, padding: 20}, + center: {eles: nodes} + }, {duration: 400}); + }; + + /** + * will return true/false if two nodes overlapping + * + * @param graph node + */ + private isNodesOverlapping(node: Cy.CollectionFirstNode, draggedNode: Cy.CollectionFirstNode): boolean { + + let nodeBoundingBox: Cy.BoundingBox = node.renderedBoundingBox(); + let secondNodeBoundingBox: Cy.BoundingBox = draggedNode.renderedBoundingBox(); + + return this.isBBoxOverlapping(nodeBoundingBox, secondNodeBoundingBox); + } + + /** + * Checks whether the bounding boxes of two nodes are overlapping on any side + * @param nodeOneBBox + * @param nodeTwoBBox + * @returns {boolean} + */ + private isBBoxOverlapping(nodeOneBBox: Cy.BoundingBox, nodeTwoBBox: Cy.BoundingBox) { + return (((nodeOneBBox.x1 < nodeTwoBBox.x1 && nodeOneBBox.x2 > nodeTwoBBox.x1) || + (nodeOneBBox.x1 < nodeTwoBBox.x2 && nodeOneBBox.x2 > nodeTwoBBox.x2) || + (nodeTwoBBox.x1 < nodeOneBBox.x1 && nodeTwoBBox.x2 > nodeOneBBox.x2)) && + ((nodeOneBBox.y1 < nodeTwoBBox.y1 && nodeOneBBox.y2 > nodeTwoBBox.y1) || + (nodeOneBBox.y1 < nodeTwoBBox.y2 && nodeOneBBox.y2 > nodeTwoBBox.y2) || + (nodeTwoBBox.y1 < nodeOneBBox.y1 && nodeTwoBBox.y2 > nodeOneBBox.y2))) + } + + /** + * Checks whether a specific topologyTemplate instance can be hosted on the UCPE instance + * @param cy - Cytoscape instance + * @param fromUcpeInstance + * @param toComponentInstance + * @returns {Match} + */ + public canBeHostedOn(cy: Cy.Instance, fromUcpeInstance: ComponentInstance, toComponentInstance: ComponentInstance): Match { + + let matches: Array<Match> = this.matchCapabilitiesRequirementsUtils.getMatchedRequirementsCapabilities(fromUcpeInstance, toComponentInstance, this.getAllCompositionCiLinks(cy)); + let hostedOnMatch: Match = _.find(matches, (match: Match) => { + return match.requirement.capability.toLowerCase() === 'tosca.capabilities.container'; + }); + + return hostedOnMatch; + }; + + /** + * Checks whether node can be dropped into UCPE + * @param cy + * @param nodeToInsert + * @param ucpeNode + * @returns {boolean} + */ + private isValidDropInsideUCPE(cy: Cy.Instance, nodeToInsert: ComponentInstance, ucpeNode: ComponentInstance): boolean { + + let hostedOnMatch: Match = this.canBeHostedOn(cy, ucpeNode, nodeToInsert); + let result: boolean = !angular.isUndefined(hostedOnMatch) || nodeToInsert.isVl(); //group validation + return result; + + }; + + /** + * For drops from palette, checks whether the node can be dropped. If node is being held over another node, check if capable of hosting + * @param cy + * @param pseudoNodeBBox + * @param paletteComponentInstance + * @returns {boolean} + */ + public isPaletteDropValid(cy: Cy.Instance, pseudoNodeBBox: Cy.BoundingBox) { + + let illegalOverlappingNodes = _.filter(cy.nodes("[isSdcElement]"), (graphNode: Cy.CollectionFirstNode) => { + if (this.isBBoxOverlapping(pseudoNodeBBox, graphNode.renderedBoundingBox())) { + return true; + } + return false; + }); + + return illegalOverlappingNodes.length === 0; + } + + /** + * will return true/false if a drop of a single node is valid + * + * @param graph node + */ + public isValidDrop(cy: Cy.Instance, draggedNode: Cy.CollectionFirstNode): boolean { + + let illegalOverlappingNodes = _.filter(cy.nodes("[isSdcElement]"), (graphNode: Cy.CollectionFirstNode) => { //all sdc nodes, removing child nodes (childe node allways collaps + + if (draggedNode.data().isUcpe && (graphNode.isChild() || graphNode.data().isInsideGroup)) { //ucpe cps always inside ucpe, no overlapping + return false; + } + if (draggedNode.data().isInsideGroup && (!draggedNode.active() || graphNode.data().isUcpe)) { + return false; + } + + if (!draggedNode.data().isUcpe && !(draggedNode.data() instanceof CompositionCiNodeUcpeCp) && graphNode.data().isUcpe) { //case we are dragging a node into UCPE + let isEntirelyInUCPE: boolean = this.commonGraphUtils.isFirstBoxContainsInSecondBox(draggedNode.renderedBoundingBox(), graphNode.renderedBoundingBox()); + if (isEntirelyInUCPE) { + if (this.isValidDropInsideUCPE(cy, draggedNode.data().componentInstance, graphNode.data().componentInstance)) { //if this is valid insert into ucpe, we return false - no illegal overlapping nodes + return false; + } + } + } + return graphNode.data().id !== draggedNode.data().id && this.isNodesOverlapping(draggedNode, graphNode); + + }); + // return false; + return illegalOverlappingNodes.length === 0; + }; + + /** + * will return true/false if the move of the nodes is valid (no node overlapping and verifying if insert into UCPE is valid) + * + * @param nodesArray - the selected drags nodes + */ + public isGroupValidDrop(cy: Cy.Instance, nodesArray: Cy.CollectionNodes): boolean { + let filterDraggedNodes = nodesArray.filter('[?isDraggable]'); + let isValidDrop = _.every(filterDraggedNodes, (node: Cy.CollectionFirstNode) => { + return this.isValidDrop(cy, node); + + }); + return isValidDrop; + }; + + /** + * get all links in diagram + * @param cy + * @returns {any[]|boolean[]} + */ + public getAllCompositionCiLinks = (cy: Cy.Instance): Array<CompositionCiLinkBase> => { + return _.map(cy.edges("[isSdcElement]"), (edge: Cy.CollectionEdges) => { + return edge.data(); + }); + }; + + /** + * + * @param blockAction - true/false if this is a block action + * @param instances + * @param component + */ + public pushMultipleUpdateComponentInstancesRequestToQueue = (instances: Array<ComponentInstance>): void => { + this.queueServiceUtils.addNonBlockingUIAction(() => { + return new Promise<boolean>((resolve, reject) => { + let uniqueId = this.workspaceService.metadata.uniqueId; + let topologyType = this.workspaceService.metadata.componentType; + this.topologyTemplateService.updateMultipleComponentInstances(uniqueId, topologyType, instances).subscribe(instancesResult => { + this.compositionService.updateComponentInstances(instancesResult); + resolve(true); + }); + }); + }); + } +} diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-links-utils.ts b/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-links-utils.ts new file mode 100644 index 0000000000..6035d05b7f --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-links-utils.ts @@ -0,0 +1,342 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +/** + * Created by obarda on 6/28/2016. + */ +import * as _ from "lodash"; +import {GraphUIObjects} from "app/utils"; +import { + Match, + CompositionCiNodeBase, + RelationshipModel, + ConnectRelationModel, + LinksFactory, + Component, + LinkMenu, + Point, + CompositionCiLinkBase, + Requirement, + Capability, + Relationship, + ComponentInstance +} from "app/models"; +import {CommonGraphUtils} from "../common/common-graph-utils"; +import {CompositionGraphGeneralUtils} from "./composition-graph-general-utils"; +import {MatchCapabilitiesRequirementsUtils} from "./match-capability-requierment-utils"; +import {CompositionCiServicePathLink} from "app/models/graph/graph-links/composition-graph-links/composition-ci-service-path-link"; +import {Injectable} from "@angular/core"; +import {QueueServiceUtils} from "app/ng2/utils/queue-service-utils"; +import {TopologyTemplateService} from "app/ng2/services/component-services/topology-template.service"; +import {SdcUiServices} from "onap-ui-angular"; +import {CompositionService} from "../../composition.service"; +import {WorkspaceService} from "app/ng2/pages/workspace/workspace.service"; + +@Injectable() +export class CompositionGraphLinkUtils { + + constructor(private linksFactory: LinksFactory, + private generalGraphUtils: CompositionGraphGeneralUtils, + private commonGraphUtils: CommonGraphUtils, + private queueServiceUtils: QueueServiceUtils, + private matchCapabilitiesRequirementsUtils: MatchCapabilitiesRequirementsUtils, + private topologyTemplateService: TopologyTemplateService, + private loaderService: SdcUiServices.LoaderService, + private compositionService: CompositionService, + private workspaceService: WorkspaceService) { + + + } + + /** + * Delete the link on server and then remove it from graph + * @param component + * @param releaseLoading - true/false release the loader when finished + * @param link - the link to delete + */ + public deleteLink = (cy: Cy.Instance, component: Component, releaseLoading: boolean, link: Cy.CollectionEdges) => { + + this.loaderService.activate(); + this.queueServiceUtils.addBlockingUIAction(() => { + this.topologyTemplateService.deleteRelation(this.workspaceService.metadata.uniqueId, this.workspaceService.metadata.componentType, link.data().relation).subscribe((deletedRelation) => { + this.compositionService.deleteRelation(deletedRelation); + cy.remove(link); + this.loaderService.deactivate(); + }, (error) => {this.loaderService.deactivate()}); + }); + }; + + /** + * create the link on server and than draw it on graph + * @param link - the link to create + * @param cy + * @param component + */ + public createLink = (link: CompositionCiLinkBase, cy: Cy.Instance): void => { + + this.loaderService.activate(); + link.updateLinkDirection(); + + this.queueServiceUtils.addBlockingUIAction(() => { + this.topologyTemplateService.createRelation(this.workspaceService.metadata.uniqueId, this.workspaceService.metadata.componentType, link.relation).subscribe((relation) => { + link.setRelation(relation); + this.insertLinkToGraph(cy, link); + this.compositionService.addRelation(relation); + this.loaderService.deactivate(); + }, (error) => {this.loaderService.deactivate()}) + }); + }; + + private createSimpleLink = (match: Match, cy: Cy.Instance): void => { + let newRelation: RelationshipModel = match.matchToRelationModel(); + let linkObg: CompositionCiLinkBase = this.linksFactory.createGraphLink(cy, newRelation, newRelation.relationships[0]); + this.createLink(linkObg, cy); + }; + + public createLinkFromMenu = (cy: Cy.Instance, chosenMatch: Match): void => { + + if (chosenMatch) { + if (chosenMatch && chosenMatch instanceof Match) { + this.createSimpleLink(chosenMatch, cy); + } + } + } + + /** + * open the connect link menu if the link drawn is valid - match requirements & capabilities + * @param cy + * @param fromNode + * @param toNode + * @returns {any} + */ + public onLinkDrawn(cy: Cy.Instance, fromNode: Cy.CollectionFirstNode, toNode: Cy.CollectionFirstNode): ConnectRelationModel { + + let linkModel: Array<CompositionCiLinkBase> = this.generalGraphUtils.getAllCompositionCiLinks(cy); + + let possibleRelations: Array<Match> = this.matchCapabilitiesRequirementsUtils.getMatchedRequirementsCapabilities(fromNode.data().componentInstance, + toNode.data().componentInstance, linkModel); + + //if found possibleRelations between the nodes we create relation menu directive and open the link menu + if (possibleRelations.length) { + // let menuPosition = this.generalGraphUtils.getLinkMenuPosition(cy, toNode.renderedPoint()); + return new ConnectRelationModel(fromNode.data(), toNode.data(), possibleRelations); + } + return null; + }; + + private handlePathLink(cy: Cy.Instance, event: Cy.EventObject) { + let linkData = event.cyTarget.data(); + let selectedPathId = linkData.pathId; + let pathEdges = cy.collection(`[pathId='${selectedPathId}']`); + if (pathEdges.length > 1) { + setTimeout(() => { + pathEdges.select(); + }, 0); + } + } + + private handleVLLink(event: Cy.EventObject) { + let vl: Cy.CollectionNodes = event.cyTarget[0].target('.vl-node'); + let connectedEdges: Cy.CollectionEdges = vl.connectedEdges(`[type!="${CompositionCiServicePathLink.LINK_TYPE}"]`); + if (vl.length && connectedEdges.length > 1) { + setTimeout(() => { + vl.select(); + connectedEdges.select(); + }, 0); + } + } + + + /** + * Handles click event on links. + * If one edge selected: do nothing. + * Two or more edges: first click - select all, secondary click - select single. + * @param cy + * @param event + */ + public handleLinkClick(cy: Cy.Instance, event: Cy.EventObject) { + if (cy.$('edge:selected').length > 1 && event.cyTarget[0].selected()) { + cy.$(':selected').unselect(); + } else { + if (event.cyTarget[0].data().type === CompositionCiServicePathLink.LINK_TYPE) { + this.handlePathLink(cy, event); + } + else { + this.handleVLLink(event); + } + } + } + + + /** + * Calculates the position for the menu that modifies an existing link + * @param event + * @param elementWidth + * @param elementHeight + * @returns {Point} + */ + public calculateLinkMenuPosition(event, elementWidth, elementHeight): Point { + let point: Point = new Point(event.originalEvent.clientX, event.originalEvent.clientY); + if (event.originalEvent.view.screen.height - elementHeight < point.y) { + point.y = event.originalEvent.view.screen.height - elementHeight; + } + if (event.originalEvent.view.screen.width - elementWidth < point.x) { + point.x = event.originalEvent.view.screen.width - elementWidth; + } + return point; + }; + + + /** + * Gets the menu that is displayed when you click an existing link. + * @param link + * @param event + * @returns {LinkMenu} + */ + public getModifyLinkMenu(link: Cy.CollectionFirstEdge, event: Cy.EventObject): LinkMenu { + let point: Point = this.calculateLinkMenuPosition(event, GraphUIObjects.MENU_LINK_VL_WIDTH_OFFSET, GraphUIObjects.MENU_LINK_VL_HEIGHT_OFFSET); + let menu: LinkMenu = new LinkMenu(point, true, link); + return menu; + }; + + /** + * Returns relation source and target nodes. + * @param nodes - all nodes in graph in order to find the edge connecting the two nodes + * @param fromNodeId + * @param toNodeId + * @returns [source, target] array of source node and target node. + */ + public getRelationNodes(nodes: Cy.CollectionNodes, fromNodeId: string, toNodeId: string) { + return [ + _.find(nodes, (node: Cy.CollectionFirst) => node.data().id === fromNodeId), + _.find(nodes, (node: Cy.CollectionFirst) => node.data().id === toNodeId) + ]; + } + + + /** + * go over the relations and draw links on the graph + * @param cy + * @param getRelationRequirementCapability - function to get requirement and capability of a relation + */ + public initGraphLinks(cy: Cy.Instance, relations: RelationshipModel[]) { + if (relations) { + _.forEach(relations, (relationshipModel: RelationshipModel) => { + _.forEach(relationshipModel.relationships, (relationship: Relationship) => { + let linkToCreate = this.linksFactory.createGraphLink(cy, relationshipModel, relationship); + this.insertLinkToGraph(cy, linkToCreate); + }); + }); + } + } + + /** + * Add link to graph - only draw the link + * @param cy + * @param link + * @param getRelationRequirementCapability + */ + public insertLinkToGraph = (cy: Cy.Instance, link: CompositionCiLinkBase) => { + const relationNodes = this.getRelationNodes(cy.nodes(), link.source, link.target); + const sourceNode: CompositionCiNodeBase = relationNodes[0] && relationNodes[0].data(); + const targetNode: CompositionCiNodeBase = relationNodes[1] && relationNodes[1].data(); + if ((sourceNode && !sourceNode.certified) || (targetNode && !targetNode.certified)) { + link.classes = 'not-certified-link'; + } + let linkElement = cy.add({ + group: 'edges', + data: link, + classes: link.classes + }); + + const getLinkRequirementCapability = () => + this.getRelationRequirementCapability(link.relation.relationships[0], sourceNode.componentInstance, targetNode.componentInstance); + this.commonGraphUtils.initLinkTooltip(linkElement, link.relation.relationships[0], getLinkRequirementCapability); + }; + + public syncComponentByRelation(relation: RelationshipModel) { + let componentInstances = this.compositionService.getComponentInstances(); + relation.relationships.forEach((rel) => { + if (rel.capability) { + const toComponentInstance: ComponentInstance = componentInstances.find((inst) => inst.uniqueId === relation.toNode); + const toComponentInstanceCapability: Capability = toComponentInstance.findCapability( + rel.capability.type, rel.capability.uniqueId, rel.capability.ownerId, rel.capability.name); + const isCapabilityFulfilled: boolean = rel.capability.isFulfilled(); + if (isCapabilityFulfilled && toComponentInstanceCapability) { + // if capability is fulfilled and in component, then remove it + console.log('Capability is fulfilled', rel.capability.getFullTitle(), rel.capability.leftOccurrences); + toComponentInstance.capabilities[rel.capability.type].splice( + toComponentInstance.capabilities[rel.capability.type].findIndex((cap) => cap === toComponentInstanceCapability), 1 + ) + } else if (!isCapabilityFulfilled && !toComponentInstanceCapability) { + // if capability is unfulfilled and not in component, then add it + console.log('Capability is unfulfilled', rel.capability.getFullTitle(), rel.capability.leftOccurrences); + toComponentInstance.capabilities[rel.capability.type].push(rel.capability); + } + } + if (rel.requirement) { + const fromComponentInstance: ComponentInstance = componentInstances.find((inst) => inst.uniqueId === relation.fromNode); + const fromComponentInstanceRequirement: Requirement = fromComponentInstance.findRequirement( + rel.requirement.capability, rel.requirement.uniqueId, rel.requirement.ownerId, rel.requirement.name); + const isRequirementFulfilled: boolean = rel.requirement.isFulfilled(); + if (isRequirementFulfilled && fromComponentInstanceRequirement) { + // if requirement is fulfilled and in component, then remove it + console.log('Requirement is fulfilled', rel.requirement.getFullTitle(), rel.requirement.leftOccurrences); + fromComponentInstance.requirements[rel.requirement.capability].splice( + fromComponentInstance.requirements[rel.requirement.capability].findIndex((req) => req === fromComponentInstanceRequirement), 1 + ) + } else if (!isRequirementFulfilled && !fromComponentInstanceRequirement) { + // if requirement is unfulfilled and not in component, then add it + console.log('Requirement is unfulfilled', rel.requirement.getFullTitle(), rel.requirement.leftOccurrences); + fromComponentInstance.requirements[rel.requirement.capability].push(rel.requirement); + } + } + }); + } + + public getRelationRequirementCapability(relationship: Relationship, sourceNode: ComponentInstance, targetNode: ComponentInstance): Promise<{ requirement: Requirement, capability: Capability }> { + // try find the requirement and capability in the source and target component instances: + let capability: Capability = targetNode.findCapability(undefined, + relationship.relation.capabilityUid, + relationship.relation.capabilityOwnerId, + relationship.relation.capability); + let requirement: Requirement = sourceNode.findRequirement(undefined, + relationship.relation.requirementUid, + relationship.relation.requirementOwnerId, + relationship.relation.requirement); + + return new Promise<{ requirement: Requirement, capability: Capability }>((resolve, reject) => { + if (capability && requirement) { + resolve({capability, requirement}); + } + else { + // if requirement and/or capability is missing, then fetch the full relation with its requirement and capability: + this.topologyTemplateService.fetchRelation(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId, relationship.relation.id).subscribe((fetchedRelation) => { + this.syncComponentByRelation(fetchedRelation); + resolve({ + capability: capability || fetchedRelation.relationships[0].capability, + requirement: requirement || fetchedRelation.relationships[0].requirement + }); + }, reject); + } + }); + } +} + diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-nodes-utils.spec.ts b/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-nodes-utils.spec.ts new file mode 100644 index 0000000000..9dcc47f7cc --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-nodes-utils.spec.ts @@ -0,0 +1,158 @@ +import { TestBed } from '@angular/core/testing'; +import { SdcUiServices } from 'onap-ui-angular'; +import { Observable } from 'rxjs/Rx'; +import CollectionNodes = Cy.CollectionNodes; +import { Mock } from 'ts-mockery'; +import { ComponentInstance } from '../../../../../models'; +import { ComponentMetadata } from '../../../../../models/component-metadata'; +import { Resource } from '../../../../../models/components/resource'; +import { CompositionCiNodeCp } from '../../../../../models/graph/nodes/composition-graph-nodes/composition-ci-node-cp'; +import { CompositionCiNodeVl } from '../../../../../models/graph/nodes/composition-graph-nodes/composition-ci-node-vl'; +import { EventListenerService } from '../../../../../services'; +import CollectionEdges = Cy.CollectionEdges; +import { GRAPH_EVENTS } from '../../../../../utils/constants'; +import { ServiceServiceNg2 } from '../../../../services/component-services/service.service'; +import { TopologyTemplateService } from '../../../../services/component-services/topology-template.service'; +import { ComponentGenericResponse } from '../../../../services/responses/component-generic-response'; +import { QueueServiceUtils } from '../../../../utils/queue-service-utils'; +import { WorkspaceService } from '../../../workspace/workspace.service'; +import { CompositionService } from '../../composition.service'; +import { CommonGraphUtils } from '../common/common-graph-utils'; +import { CompositionGraphGeneralUtils } from './composition-graph-general-utils'; +import { CompositionGraphNodesUtils } from './composition-graph-nodes-utils'; + +describe('composition graph nodes utils', () => { + + const CP_TO_DELETE_ID = 'cp1'; + const VL_TO_DELETE_ID = 'vl'; + const CP2_ID = 'cp2'; + + let loaderServiceMock: Partial<SdcUiServices.LoaderService>; + let service: CompositionGraphNodesUtils; + let topologyServiceMock: TopologyTemplateService; + let queueServiceMock: QueueServiceUtils; + let workspaceServiceMock: WorkspaceService; + let compositionServiceMock: CompositionService; + let eventListenerServiceMock: EventListenerService; + const cpInstanceMock: ComponentInstance = Mock.of<ComponentInstance>({ + uniqueId: CP_TO_DELETE_ID, + isVl: () => false + }); + const vlInstanceMock: ComponentInstance = Mock.of<ComponentInstance>({ + uniqueId: VL_TO_DELETE_ID, + isVl: () => true + }); + const cp2InstanceMock: ComponentInstance = Mock.of<ComponentInstance>({ + uniqueId: CP2_ID, + isVl: () => false + }); + + const cyMock = Mock.of<Cy.Instance>({ + remove: jest.fn(), + collection: jest.fn() + }); + + const serviceServiceMock = Mock.of<ServiceServiceNg2>({ + getComponentCompositionData : () => Observable.of(Mock.of<ComponentGenericResponse>()) + }); + + // Instances on the graph cp, vl, cp2 + const cp = Mock.from<CompositionCiNodeCp>({ id: CP_TO_DELETE_ID, componentInstance: cpInstanceMock }); + const vl = Mock.from<CompositionCiNodeVl>({ id: VL_TO_DELETE_ID, componentInstance: vlInstanceMock }); + const cp2 = Mock.from<CompositionCiNodeCp>({ id: CP2_ID, componentInstance: cp2InstanceMock }); + + beforeEach(() => { + + loaderServiceMock = { + activate: jest.fn(), + deactivate: jest.fn() + }; + + topologyServiceMock = Mock.of<TopologyTemplateService>({ + deleteComponentInstance : () => Observable.of(cpInstanceMock) + }); + + queueServiceMock = Mock.of<QueueServiceUtils>({ + addBlockingUIAction : ( (f) => f() ) + }); + + workspaceServiceMock = Mock.of<WorkspaceService>({ + metadata: Mock.of<ComponentMetadata>( { uniqueId: 'topologyTemplateUniqueId' } ) + }); + + compositionServiceMock = Mock.of<CompositionService>({ + deleteComponentInstance : jest.fn() + }); + + eventListenerServiceMock = Mock.of<EventListenerService>({ + notifyObservers : jest.fn() + }); + + TestBed.configureTestingModule({ + imports: [], + providers: [ + CompositionGraphNodesUtils, + {provide: WorkspaceService, useValue: workspaceServiceMock}, + {provide: TopologyTemplateService, useValue: topologyServiceMock}, + {provide: CompositionService, useValue: compositionServiceMock}, + {provide: CompositionGraphGeneralUtils, useValue: {}}, + {provide: CommonGraphUtils, useValue: {}}, + {provide: EventListenerService, useValue: eventListenerServiceMock}, + {provide: QueueServiceUtils, useValue: queueServiceMock}, + {provide: ServiceServiceNg2, useValue: serviceServiceMock}, + {provide: SdcUiServices.LoaderService, useValue: loaderServiceMock} + ] + }); + service = TestBed.get(CompositionGraphNodesUtils); + }); + + it('When a CP is deleted which is connected to a VL that has another leg to another CP, the VL is deleted as well', () => { + // Prepare a VL that is connected to both CP and CP2 + const vlToDelete = Mock.of<CollectionNodes>({ + data: () => vl, + connectedEdges: () => Mock.of<CollectionEdges>({ + length: 2, + connectedNodes: () => [cp, cp2] as CollectionNodes + }) + }); + + // Prepare a CP which is connected to a VL + const cpToDelete = Mock.of<CollectionNodes>({ + data: () => cp, + connectedEdges: () => Mock.of<CollectionEdges>({ + length: 1, + connectedNodes: () => [vlToDelete] as CollectionNodes + }) + }); + service.deleteNode(cyMock, Mock.of<Resource>(), cpToDelete); + expect(compositionServiceMock.deleteComponentInstance).toHaveBeenCalledWith(CP_TO_DELETE_ID); + expect(eventListenerServiceMock.notifyObservers).toHaveBeenCalledWith(GRAPH_EVENTS.ON_DELETE_COMPONENT_INSTANCE, VL_TO_DELETE_ID); + expect(eventListenerServiceMock.notifyObservers).toHaveBeenLastCalledWith(GRAPH_EVENTS.ON_DELETE_COMPONENT_INSTANCE_SUCCESS, CP_TO_DELETE_ID); + expect(cyMock.remove).toHaveBeenCalled(); + }); + + it('When a CP is deleted which is solely connected to another VL the VL is not deleted', () => { + // Prepare a VL that is connected only to 1 CP + const vlToDelete = Mock.of<CollectionNodes>({ + data: () => vl, + connectedEdges: () => Mock.of<CollectionEdges>({ + length: 1, + connectedNodes: () => [cp] as CollectionNodes + }) + }); + + // Prepare a CP which is connected to a VL + const cpToDelete = Mock.of<CollectionNodes>({ + data: () => cp, + connectedEdges: () => Mock.of<CollectionEdges>({ + length: 1, + connectedNodes: () => [vlToDelete] as CollectionNodes + }) + }); + service.deleteNode(cyMock, Mock.of<Resource>(), cpToDelete); + expect(compositionServiceMock.deleteComponentInstance).toHaveBeenCalledWith(CP_TO_DELETE_ID); + expect(eventListenerServiceMock.notifyObservers).toHaveBeenLastCalledWith(GRAPH_EVENTS.ON_DELETE_COMPONENT_INSTANCE_SUCCESS, CP_TO_DELETE_ID); + expect(eventListenerServiceMock.notifyObservers).toHaveBeenCalledTimes(1); + expect(cyMock.remove).toHaveBeenCalled(); + }); +}); diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-nodes-utils.ts b/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-nodes-utils.ts new file mode 100644 index 0000000000..ea876c6d1a --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-nodes-utils.ts @@ -0,0 +1,202 @@ +/*- + * ============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========================================================= + */ + +import { Injectable } from '@angular/core'; +import { Component as TopologyTemplate } from 'app/models'; +import { + ComponentInstance, + CompositionCiNodeVl, Service +} from 'app/models'; +import { CompositionCiServicePathLink } from 'app/models/graph/graph-links/composition-graph-links/composition-ci-service-path-link'; +import { WorkspaceService } from 'app/ng2/pages/workspace/workspace.service'; +import { ServiceServiceNg2 } from 'app/ng2/services/component-services/service.service'; +import { TopologyTemplateService } from 'app/ng2/services/component-services/topology-template.service'; +import { ServiceGenericResponse } from 'app/ng2/services/responses/service-generic-response'; +import { QueueServiceUtils } from 'app/ng2/utils/queue-service-utils'; +import { EventListenerService } from 'app/services'; +import { GRAPH_EVENTS } from 'app/utils'; +import * as _ from 'lodash'; +import { SdcUiServices } from 'onap-ui-angular'; +import { CompositionService } from '../../composition.service'; +import { CommonGraphUtils } from '../common/common-graph-utils'; +import { CompositionGraphGeneralUtils } from './composition-graph-general-utils'; + +/** + * Created by obarda on 11/9/2016. + */ +@Injectable() +export class CompositionGraphNodesUtils { + constructor(private generalGraphUtils: CompositionGraphGeneralUtils, + private commonGraphUtils: CommonGraphUtils, + private eventListenerService: EventListenerService, + private queueServiceUtils: QueueServiceUtils, + private serviceService: ServiceServiceNg2, + private loaderService: SdcUiServices.LoaderService, + private compositionService: CompositionService, + private topologyTemplateService: TopologyTemplateService, + private workspaceService: WorkspaceService) { + } + + /** + * Returns component instances for all nodes passed in + * @param nodes - Cy nodes + * @returns {any[]} + */ + public getAllNodesData(nodes: Cy.CollectionNodes) { + return _.map(nodes, (node: Cy.CollectionFirstNode) => { + return node.data(); + }); + } + + public highlightMatchingNodesByName = (cy: Cy.Instance, nameToMatch: string) => { + + cy.batch(() => { + cy.nodes("[name !@^= '" + nameToMatch + "']").style({'background-image-opacity': 0.4}); + cy.nodes("[name @^= '" + nameToMatch + "']").style({'background-image-opacity': 1}); + }); + + } + + // Returns all nodes whose name starts with searchTerm + public getMatchingNodesByName = (cy: Cy.Instance, nameToMatch: string): Cy.CollectionNodes => { + return cy.nodes("[name @^= '" + nameToMatch + "']"); + } + + /** + * Deletes component instances on server and then removes it from the graph as well + * @param cy + * @param component + * @param nodeToDelete + */ + public deleteNode(cy: Cy.Instance, component: TopologyTemplate, nodeToDelete: Cy.CollectionNodes): void { + + this.loaderService.activate(); + const onSuccess: (response: ComponentInstance) => void = (response: ComponentInstance) => { + // check whether the node is connected to any VLs that only have one other connection. If so, delete that VL as well + this.loaderService.deactivate(); + this.compositionService.deleteComponentInstance(response.uniqueId); + + const nodeToDeleteIsNotVl = nodeToDelete.data().componentInstance && !(nodeToDelete.data().componentInstance.isVl()); + if (nodeToDeleteIsNotVl) { + const connectedVls: Cy.CollectionFirstNode[] = this.getConnectedVlToNode(nodeToDelete); + this.handleConnectedVlsToDelete(connectedVls); + } + + // check whether there is a service path going through this node, and if so clean it from the graph. + const nodeId = nodeToDelete.data().id; + const connectedPathLinks = cy.collection(`[type="${CompositionCiServicePathLink.LINK_TYPE}"][source="${nodeId}"], [type="${CompositionCiServicePathLink.LINK_TYPE}"][target="${nodeId}"]`); + _.forEach(connectedPathLinks, (link, key) => { + cy.remove(`[pathId="${link.data().pathId}"]`); + }); + + // update service path list + this.serviceService.getComponentCompositionData(component).subscribe((serviceResponse: ServiceGenericResponse) => { + (component as Service).forwardingPaths = serviceResponse.forwardingPaths; + }); + + this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_DELETE_COMPONENT_INSTANCE_SUCCESS, nodeId); + + // update UI + cy.remove(nodeToDelete); + }; + + const onFailed: (response: any) => void = (response: any) => { + this.loaderService.deactivate(); + }; + + this.queueServiceUtils.addBlockingUIAction( + () => { + const uniqueId = this.workspaceService.metadata.uniqueId; + const componentType = this.workspaceService.metadata.componentType; + this.topologyTemplateService.deleteComponentInstance(componentType, uniqueId, nodeToDelete.data().componentInstance.uniqueId).subscribe(onSuccess, onFailed); + } + ); + } + + /** + * Finds all VLs connected to a single node + * @param node + * @returns {Array<Cy.CollectionFirstNode>} + */ + public getConnectedVlToNode = (node: Cy.CollectionNodes): Cy.CollectionFirstNode[] => { + const connectedVls: Cy.CollectionFirstNode[] = new Array<Cy.CollectionFirstNode>(); + _.forEach(node.connectedEdges().connectedNodes(), (connectedNode: Cy.CollectionFirstNode) => { + const connectedNodeIsVl = connectedNode.data().componentInstance.isVl(); + if (connectedNodeIsVl) { + connectedVls.push(connectedNode); + } + }); + return connectedVls; + } + + /** + * Delete all VLs that have only two connected nodes (this function is called when deleting a node) + * @param connectedVls + */ + public handleConnectedVlsToDelete = (connectedVls: Cy.CollectionFirstNode[]) => { + _.forEach(connectedVls, (vlToDelete: Cy.CollectionNodes) => { + + if (vlToDelete.connectedEdges().length === 2) { // if vl connected only to 2 nodes need to delete the vl + this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_DELETE_COMPONENT_INSTANCE, vlToDelete.data().componentInstance.uniqueId); + } + }); + } + + /** + * This function will update nodes position. + * @param cy + * @param component + * @param nodesMoved - the node/multiple nodes now moved by the user + */ + public onNodesPositionChanged = (cy: Cy.Instance, component: TopologyTemplate, nodesMoved: Cy.CollectionNodes): void => { + + if (nodesMoved.length === 0) { + return; + } + + const isValidMove: boolean = this.generalGraphUtils.isGroupValidDrop(cy, nodesMoved); + if (isValidMove) { + + const instancesToUpdate: ComponentInstance[] = new Array<ComponentInstance>(); + + _.each(nodesMoved, (node: Cy.CollectionFirstNode) => { // update all nodes new position + + // update position + const newPosition: Cy.Position = this.commonGraphUtils.getNodePosition(node); + node.data().componentInstance.updatePosition(newPosition.x, newPosition.y); + instancesToUpdate.push(node.data().componentInstance); + + }); + + if (instancesToUpdate.length > 0) { + this.generalGraphUtils.pushMultipleUpdateComponentInstancesRequestToQueue(instancesToUpdate); + } + } else { + // reset nodes position + nodesMoved.positions((i, node) => { + return { + x: +node.data().componentInstance.posX, + y: +node.data().componentInstance.posY + }; + }); + } + } + +} diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-palette-utils.ts b/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-palette-utils.ts new file mode 100644 index 0000000000..1776c2f9b9 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-palette-utils.ts @@ -0,0 +1,233 @@ +/*- + * ============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========================================================= + */ + +import {Injectable} from "@angular/core"; +import {CompositionGraphGeneralUtils, RequirementAndCapabilities} from "./composition-graph-general-utils"; +import {CommonGraphUtils} from "../common/common-graph-utils"; +import {EventListenerService} from "../../../../../services/event-listener-service"; +import {ResourceNamePipe} from "app/ng2/pipes/resource-name.pipe"; +import {ComponentInstanceFactory} from "app/utils/component-instance-factory"; +import {GRAPH_EVENTS, GraphUIObjects} from "app/utils/constants"; +import {TopologyTemplateService} from "app/ng2/services/component-services/topology-template.service"; +import {DndDropEvent} from "ngx-drag-drop/ngx-drag-drop"; +import {SdcUiServices} from "onap-ui-angular" +import { Component as TopologyTemplate, NodesFactory, CapabilitiesGroup, RequirementsGroup, + CompositionCiNodeBase, ComponentInstance, LeftPaletteComponent, Point } from "app/models"; +import {CompositionService} from "../../composition.service"; +import {WorkspaceService} from "app/ng2/pages/workspace/workspace.service"; +import { QueueServiceUtils } from "app/ng2/utils/queue-service-utils"; +import {ComponentGenericResponse} from "../../../../services/responses/component-generic-response"; +import {MatchCapabilitiesRequirementsUtils} from "./match-capability-requierment-utils"; +import {CompositionGraphNodesUtils} from "./index"; + +@Injectable() +export class CompositionGraphPaletteUtils { + + constructor(private generalGraphUtils:CompositionGraphGeneralUtils, + private nodesFactory:NodesFactory, + private commonGraphUtils:CommonGraphUtils, + private queueServiceUtils:QueueServiceUtils, + private eventListenerService:EventListenerService, + private topologyTemplateService: TopologyTemplateService, + private loaderService: SdcUiServices.LoaderService, + private compositionService: CompositionService, + private workspaceService: WorkspaceService, + private matchCapabilitiesRequirementsUtils: MatchCapabilitiesRequirementsUtils, + private nodesGraphUtils: CompositionGraphNodesUtils) { + } + + /** + * + * @param Calculate matching nodes, highlight the matching nodes and fade the non matching nodes + * @param leftPaletteComponent + * @param _cy + * @returns void + * @private + */ + + public onComponentHoverIn = (leftPaletteComponent: LeftPaletteComponent, _cy: Cy.Instance) => { + const nodesData = this.nodesGraphUtils.getAllNodesData(_cy.nodes()); + const nodesLinks = this.generalGraphUtils.getAllCompositionCiLinks(_cy); + + if (this.generalGraphUtils.componentRequirementsAndCapabilitiesCaching.containsKey(leftPaletteComponent.uniqueId)) { + const reqAndCap: RequirementAndCapabilities = this.generalGraphUtils.componentRequirementsAndCapabilitiesCaching.getValue(leftPaletteComponent.uniqueId); + const filteredNodesData = this.matchCapabilitiesRequirementsUtils.findMatchingNodesToComponentInstance( + { uniqueId: leftPaletteComponent.uniqueId, requirements: reqAndCap.requirements, capabilities: reqAndCap.capabilities} as ComponentInstance, nodesData, nodesLinks); + + this.matchCapabilitiesRequirementsUtils.highlightMatchingComponents(filteredNodesData, _cy); + this.matchCapabilitiesRequirementsUtils.fadeNonMachingComponents(filteredNodesData, nodesData, _cy); + } else { + + this.topologyTemplateService.getCapabilitiesAndRequirements(leftPaletteComponent.componentType, leftPaletteComponent.uniqueId).subscribe((response: ComponentGenericResponse) => { + let reqAndCap: RequirementAndCapabilities = { + capabilities: response.capabilities, + requirements: response.requirements + } + this.generalGraphUtils.componentRequirementsAndCapabilitiesCaching.setValue(leftPaletteComponent.uniqueId, reqAndCap); + }); + } + } + + /** + * Calculate the dragged element (html element) position on canvas + * @param cy + * @param event + * @param position + * @returns {Cy.BoundingBox} + * @private + */ + private _getNodeBBox(cy:Cy.Instance, event:DragEvent, position?:Cy.Position, eventPosition?: Point) { + let bbox = <Cy.BoundingBox>{}; + if (!position) { + position = event ? this.commonGraphUtils.getCytoscapeNodePosition(cy, event) : eventPosition; + } + let cushionWidth:number = 40; + let cushionHeight:number = 40; + + bbox.x1 = position.x - cushionWidth / 2; + bbox.y1 = position.y - cushionHeight / 2; + bbox.x2 = position.x + cushionWidth / 2; + bbox.y2 = position.y + cushionHeight / 2; + return bbox; + } + + /** + * Create the component instance, update data from parent component in the left palette and notify on_insert_to_ucpe if component was dragg into ucpe + * @param cy + * @param fullComponent + * @param event + * @param component + */ + private _createComponentInstanceOnGraphFromPaletteComponent(cy:Cy.Instance, fullComponent:LeftPaletteComponent, event:DragEvent) { + + let componentInstanceToCreate:ComponentInstance = ComponentInstanceFactory.createComponentInstanceFromComponent(fullComponent); + let cytoscapePosition:Cy.Position = this.commonGraphUtils.getCytoscapeNodePosition(cy, event); + componentInstanceToCreate.posX = cytoscapePosition.x; + componentInstanceToCreate.posY = cytoscapePosition.y; + + let onFailedCreatingInstance:(error:any) => void = (error:any) => { + this.loaderService.deactivate(); + }; + + //on success - update node data + let onSuccessCreatingInstance = (createInstance:ComponentInstance):void => { + + this.loaderService.deactivate(); + this.compositionService.addComponentInstance(createInstance); + createInstance.name = ResourceNamePipe.getDisplayName(createInstance.name); + createInstance.requirements = new RequirementsGroup(createInstance.requirements); + createInstance.capabilities = new CapabilitiesGroup(createInstance.capabilities); + createInstance.componentVersion = fullComponent.version; + createInstance.icon = fullComponent.icon; + createInstance.setInstanceRC(); + + let newNode:CompositionCiNodeBase = this.nodesFactory.createNode(createInstance); + this.commonGraphUtils.addComponentInstanceNodeToGraph(cy, newNode); + this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_CREATE_COMPONENT_INSTANCE); + }; + + this.queueServiceUtils.addBlockingUIAction(() => { + let uniqueId = this.workspaceService.metadata.uniqueId; + let componentType = this.workspaceService.metadata.componentType; + this.topologyTemplateService.createComponentInstance(componentType, uniqueId, componentInstanceToCreate).subscribe(onSuccessCreatingInstance, onFailedCreatingInstance); + + }); + } + // + // /** + // * Thid function applay red/green background when component dragged from palette + // * @param cy + // * @param event + // * @param dragElement + // * @param dragComponent + // */ + // public onComponentDrag(cy:Cy.Instance, event) { + // let draggedElement = document.getElementById("draggable_element"); + // // event.dataTransfer.setDragImage(draggableElement, 0, 0); + // if (event.clientX < GraphUIObjects.DIAGRAM_PALETTE_WIDTH_OFFSET || event.clientY < GraphUIObjects.DIAGRAM_HEADER_OFFSET) { //hovering over palette. Dont bother computing validity of drop + // draggedElement.className = 'invalid-drag'; + // event.dataTransfer.setDragImage(draggedElement.cloneNode(true), 0, 0); + // return; + // } + // + // let offsetPosition = { + // x: event.clientX - GraphUIObjects.DIAGRAM_PALETTE_WIDTH_OFFSET, + // y: event.clientY - GraphUIObjects.DIAGRAM_HEADER_OFFSET + // }; + // let bbox = this._getNodeBBox(cy, event, offsetPosition); + // + // if (this.generalGraphUtils.isPaletteDropValid(cy, bbox)) { + // draggedElement.className = 'valid-drag'; + // event.dataTransfer.setDragImage(draggedElement.cloneNode(true), 0, 0); + // // event.dataTransfer.setDragImage(draggedElement, 0, 0); + // // event.dataTransfer.setDragImage(draggedElement, 0, 0); + // + // } else { + // draggedElement.className = 'invalid-drag'; + // event.dataTransfer.setDragImage(draggedElement.cloneNode(true), 0, 0); + // } + // } + + public isDragValid(cy:Cy.Instance, position: Point):boolean { + if (position.x < GraphUIObjects.DIAGRAM_PALETTE_WIDTH_OFFSET || position.y < GraphUIObjects.DIAGRAM_HEADER_OFFSET) { //hovering over palette. Dont bother computing validity of drop + return false; + } + + let offsetPosition = { + x: position.x - GraphUIObjects.DIAGRAM_PALETTE_WIDTH_OFFSET, + y: position.y - GraphUIObjects.DIAGRAM_HEADER_OFFSET + }; + let bbox = this._getNodeBBox(cy, null, offsetPosition, position); + + if (this.generalGraphUtils.isPaletteDropValid(cy, bbox)) { + return true; + } else { + return false; + } + } + /** + * This function is called when after dropping node on canvas + * Check if the capability & requirements fulfilled and if not get from server + * @param cy + * @param dragEvent + * @param component + */ + public addNodeFromPalette(cy:Cy.Instance, dragEvent:DndDropEvent) { + this.loaderService.activate(); + + let draggedComponent:LeftPaletteComponent = dragEvent.data; + + if (this.generalGraphUtils.componentRequirementsAndCapabilitiesCaching.containsKey(draggedComponent.uniqueId)) { + let fullComponent = this.generalGraphUtils.componentRequirementsAndCapabilitiesCaching.getValue(draggedComponent.uniqueId); + draggedComponent.capabilities = fullComponent.capabilities; + draggedComponent.requirements = fullComponent.requirements; + this._createComponentInstanceOnGraphFromPaletteComponent(cy, draggedComponent, dragEvent.event); + + } else { + + this.topologyTemplateService.getFullComponent(draggedComponent.componentType, draggedComponent.uniqueId).subscribe((topologyTemplate:TopologyTemplate) => { + draggedComponent.capabilities = topologyTemplate.capabilities; + draggedComponent.requirements = topologyTemplate.requirements; + this._createComponentInstanceOnGraphFromPaletteComponent(cy, draggedComponent, dragEvent.event); + }); + } + } +} + diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-service-path-utils.ts b/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-service-path-utils.ts new file mode 100644 index 0000000000..bc124fe9d1 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-service-path-utils.ts @@ -0,0 +1,148 @@ +/*- + * ============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========================================================= + */ + +import * as _ from "lodash"; +import {CompositionGraphGeneralUtils} from "./composition-graph-general-utils"; +import {ServiceServiceNg2} from 'app/ng2/services/component-services/service.service'; +import {Service} from "app/models/components/service"; +import {ForwardingPath} from "app/models/forwarding-path"; +import {ForwardingPathLink} from "app/models/forwarding-path-link"; +import {ComponentRef, Injectable} from "@angular/core"; +import {CompositionCiServicePathLink} from "app/models/graph/graph-links/composition-graph-links/composition-ci-service-path-link"; +import {SdcUiServices} from "onap-ui-angular"; +import {QueueServiceUtils} from "app/ng2/utils/queue-service-utils"; +import {ServicePathsListComponent} from "app/ng2/pages/composition/graph/service-paths-list/service-paths-list.component"; +import {ButtonModel, ModalModel} from "app/models"; +import {ServicePathCreatorComponent} from "app/ng2/pages/composition/graph/service-path-creator/service-path-creator.component"; +import {ModalService} from "app/ng2/services/modal.service"; +import {ModalComponent} from "app/ng2/components/ui/modal/modal.component"; +import {Select, Store} from "@ngxs/store"; +import {WorkspaceState} from "app/ng2/store/states/workspace.state"; +import {WorkspaceService} from "app/ng2/pages/workspace/workspace.service"; +import {CompositionService} from "../../composition.service"; +import {CommonGraphUtils} from "../common/common-graph-utils"; +import {GRAPH_EVENTS} from "app/utils/constants"; +import {EventListenerService} from "app/services/event-listener-service"; + +@Injectable() +export class ServicePathGraphUtils { + + constructor( + private generalGraphUtils: CompositionGraphGeneralUtils, + private serviceService: ServiceServiceNg2, + private commonGraphUtils: CommonGraphUtils, + private loaderService: SdcUiServices.LoaderService, + private queueServiceUtils: QueueServiceUtils, + private modalService: ModalService, + private workspaceService: WorkspaceService, + private compositionService: CompositionService, + private store:Store, + private eventListenerService: EventListenerService + ) { + } + + private isViewOnly = (): boolean => { + return this.store.selectSnapshot(state => state.workspace.isViewOnly); + } + private modalInstance: ComponentRef<ModalComponent>; + + public deletePathsFromGraph(cy: Cy.Instance) { + cy.remove(`[type="${CompositionCiServicePathLink.LINK_TYPE}"]`); + } + + public drawPath(cy: Cy.Instance, forwardingPath: ForwardingPath) { + let pathElements = forwardingPath.pathElements.listToscaDataDefinition; + + _.forEach(pathElements, (link: ForwardingPathLink) => { + let data: CompositionCiServicePathLink = new CompositionCiServicePathLink(link); + data.source = _.find( + this.compositionService.componentInstances, + instance => instance.name === data.forwardingPathLink.fromNode + ).uniqueId; + data.target = _.find( + this.compositionService.componentInstances, + instance => instance.name === data.forwardingPathLink.toNode + ).uniqueId; + data.pathId = forwardingPath.uniqueId; + data.pathName = forwardingPath.name; + this.commonGraphUtils.insertServicePathLinkToGraph(cy, data); + }); + } + + public createOrUpdateServicePath = (path: any): void => { + this.loaderService.activate(); + + let onSuccess: (response: ForwardingPath) => void = (response: ForwardingPath) => { + this.loaderService.deactivate(); + this.compositionService.forwardingPaths[response.uniqueId] = response; + this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_SERVICE_PATH_CREATED, response.uniqueId) + }; + + this.queueServiceUtils.addBlockingUIAction( + () => this.serviceService.createOrUpdateServicePath(this.workspaceService.metadata.uniqueId, path).subscribe(onSuccess + , (error) => {this.loaderService.deactivate()}) + ); + }; + + public onCreateServicePath = (): void => { + // this.showServicePathMenu = false; + let cancelButton: ButtonModel = new ButtonModel('Cancel', 'outline white', this.modalService.closeCurrentModal); + let saveButton: ButtonModel = new ButtonModel('Create', 'blue', this.createPath, this.getDisabled); + let modalModel: ModalModel = new ModalModel('l', 'Create Service Flow', '', [saveButton, cancelButton], 'standard', true); + this.modalInstance = this.modalService.createCustomModal(modalModel); + this.modalService.addDynamicContentToModal(this.modalInstance, ServicePathCreatorComponent, {serviceId: this.workspaceService.metadata.uniqueId}); + this.modalInstance.instance.open(); + }; + + public onListServicePath = (): void => { + // this.showServicePathMenu = false; + let cancelButton: ButtonModel = new ButtonModel('Close', 'outline white', this.modalService.closeCurrentModal); + let modalModel: ModalModel = new ModalModel('md', 'Service Flows List', '', [cancelButton], 'standard', true); + this.modalInstance = this.modalService.createCustomModal(modalModel); + this.modalService.addDynamicContentToModal(this.modalInstance, ServicePathsListComponent, { + serviceId: this.workspaceService.metadata.uniqueId, + onCreateServicePath: this.onCreateServicePath, + onEditServicePath: this.onEditServicePath, + isViewOnly: this.isViewOnly() + }); + this.modalInstance.instance.open(); + }; + + public onEditServicePath = (id: string): void => { + let cancelButton: ButtonModel = new ButtonModel('Cancel', 'outline white', this.modalService.closeCurrentModal); + let saveButton: ButtonModel = new ButtonModel('Save', 'blue', this.createPath, this.getDisabled); + let modalModel: ModalModel = new ModalModel('l', 'Edit Path', '', [saveButton, cancelButton], 'standard', true); + this.modalInstance = this.modalService.createCustomModal(modalModel); + this.modalService.addDynamicContentToModal(this.modalInstance, ServicePathCreatorComponent, { + serviceId: this.workspaceService.metadata.uniqueId, + pathId: id + }); + this.modalInstance.instance.open(); + }; + + public getDisabled = (): boolean => { + return this.isViewOnly() || !this.modalInstance.instance.dynamicContent.instance.checkFormValidForSubmit(); + }; + + public createPath = (): void => { + this.createOrUpdateServicePath(this.modalInstance.instance.dynamicContent.instance.createServicePathData()); + this.modalService.closeCurrentModal(); + }; +} diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-zone-utils.ts b/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-zone-utils.ts new file mode 100644 index 0000000000..9e97ec0f00 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/utils/composition-graph-zone-utils.ts @@ -0,0 +1,204 @@ +import { + Point, + PolicyInstance, + Zone, + LeftPaletteMetadataTypes, + ZoneInstance, + ZoneInstanceType, + ZoneInstanceAssignmentType +} from "app/models"; +import {CanvasHandleTypes} from "app/utils"; +import {Observable} from "rxjs"; +import {GroupInstance} from "app/models/graph/zones/group-instance"; +import {Injectable} from "@angular/core"; +import {DynamicComponentService} from "app/ng2/services/dynamic-component.service"; +import {PoliciesService} from "app/ng2/services/policies.service"; +import {GroupsService} from "app/ng2/services/groups.service"; +import {Store} from "@ngxs/store"; +import {CompositionService} from "../../composition.service"; +import {WorkspaceService} from "app/ng2/pages/workspace/workspace.service"; +import { PaletteAnimationComponent } from "app/ng2/pages/composition/palette/palette-animation/palette-animation.component"; + +@Injectable() +export class CompositionGraphZoneUtils { + + constructor(private dynamicComponentService: DynamicComponentService, + private policiesService: PoliciesService, + private groupsService: GroupsService, + private workspaceService: WorkspaceService, + private compositionService: CompositionService) { + } + + + public createCompositionZones = (): Array<Zone> => { + let zones: Array<Zone> = []; + + zones[ZoneInstanceType.POLICY] = new Zone('Policies', 'P', ZoneInstanceType.POLICY); + zones[ZoneInstanceType.GROUP] = new Zone('Groups', 'G', ZoneInstanceType.GROUP); + + return zones; + } + + public showZone = (zone: Zone): void => { + zone.visible = true; + zone.minimized = false; + } + + public getZoneTypeForPaletteComponent = (componentCategory: LeftPaletteMetadataTypes) => { + if (componentCategory == LeftPaletteMetadataTypes.Group) { + return ZoneInstanceType.GROUP; + } else if (componentCategory == LeftPaletteMetadataTypes.Policy) { + return ZoneInstanceType.POLICY; + } + }; + + public initZoneInstances(zones: Array<Zone>) { + + if (this.compositionService.groupInstances && this.compositionService.groupInstances.length) { + this.showZone(zones[ZoneInstanceType.GROUP]); + zones[ZoneInstanceType.GROUP].instances = []; + _.forEach(this.compositionService.groupInstances, (group: GroupInstance) => { + let newInstance = new ZoneInstance(group, this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId); + this.addInstanceToZone(zones[ZoneInstanceType.GROUP], newInstance); + }); + } + + if (this.compositionService.policies && this.compositionService.policies.length) { + this.showZone(zones[ZoneInstanceType.POLICY]); + zones[ZoneInstanceType.POLICY].instances = []; + _.forEach(this.compositionService.policies, (policy: PolicyInstance) => { + let newInstance = new ZoneInstance(policy, this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId); + this.addInstanceToZone(zones[ZoneInstanceType.POLICY], newInstance); + + }); + } + } + + public findAndUpdateZoneInstanceData(zones: Array<Zone>, instanceData: PolicyInstance | GroupInstance) { + _.forEach(zones, (zone: Zone) => { + _.forEach(zone.instances, (zoneInstance: ZoneInstance) => { + if (zoneInstance.instanceData.uniqueId === instanceData.uniqueId) { + zoneInstance.updateInstanceData(instanceData); + } + }); + }); + } + + public updateTargetsOrMembersOnCanvasDelete = (canvasNodeID: string, zones: Array<Zone>, type: ZoneInstanceAssignmentType): void => { + _.forEach(zones, (zone) => { + _.forEach(zone.instances, (zoneInstance: ZoneInstance) => { + if (zoneInstance.isAlreadyAssigned(canvasNodeID)) { + zoneInstance.addOrRemoveAssignment(canvasNodeID, type); + //remove it from our list of BE targets and members as well (so that it will not be sent in future calls to BE). + zoneInstance.instanceData.setSavedAssignments(zoneInstance.assignments); + } + }); + }); + }; + + public createZoneInstanceFromLeftPalette = (zoneType: ZoneInstanceType, paletteComponentType: string): Observable<ZoneInstance> => { + + if (zoneType === ZoneInstanceType.POLICY) { + return this.policiesService.createPolicyInstance(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId, paletteComponentType).map(response => { + let newInstance = new PolicyInstance(response); + this.compositionService.addPolicyInstance(newInstance); + return new ZoneInstance(newInstance, this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId); + }); + } else if (zoneType === ZoneInstanceType.GROUP) { + return this.groupsService.createGroupInstance(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId, paletteComponentType).map(response => { + let newInstance = new GroupInstance(response); + this.compositionService.addGroupInstance(newInstance); + return new ZoneInstance(newInstance, this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId); + }); + } + } + + public addInstanceToZone(zone: Zone, instance: ZoneInstance, hide?: boolean) { + if (hide) { + instance.hidden = true; + } + zone.instances.push(instance); + + }; + + private findZoneCoordinates(zoneType): Point { + let point: Point = new Point(0, 0); + let zone = angular.element(document.querySelector('.' + zoneType + '-zone')); + let wrapperZone = zone.offsetParent(); + point.x = zone.prop('offsetLeft') + wrapperZone.prop('offsetLeft'); + point.y = zone.prop('offsetTop') + wrapperZone.prop('offsetTop'); + return point; + } + + public createPaletteToZoneAnimation = (startPoint: Point, zoneType: ZoneInstanceType, newInstance: ZoneInstance) => { + let zoneTypeName = ZoneInstanceType[zoneType].toLowerCase(); + let paletteToZoneAnimation = this.dynamicComponentService.createDynamicComponent(PaletteAnimationComponent); + paletteToZoneAnimation.instance.from = startPoint; + paletteToZoneAnimation.instance.type = zoneType; + paletteToZoneAnimation.instance.to = this.findZoneCoordinates(zoneTypeName); + paletteToZoneAnimation.instance.zoneInstance = newInstance; + paletteToZoneAnimation.instance.iconName = zoneTypeName; + paletteToZoneAnimation.instance.runAnimation(); + } + + public startCyTagMode = (cy: Cy.Instance) => { + cy.autolock(true); + cy.nodes().unselectify(); + cy.emit('tagstart'); //dont need to show handles because they're already visible bcz of hover event + + }; + + public endCyTagMode = (cy: Cy.Instance) => { + cy.emit('tagend'); + cy.nodes().selectify(); + cy.autolock(false); + }; + + public handleTagClick = (cy: Cy.Instance, zoneInstance: ZoneInstance, nodeId: string) => { + zoneInstance.addOrRemoveAssignment(nodeId, ZoneInstanceAssignmentType.COMPONENT_INSTANCES); + this.showZoneTagIndicationForNode(nodeId, zoneInstance, cy); + }; + + public showGroupZoneIndications = (groupInstances: Array<ZoneInstance>, policyInstance: ZoneInstance) => { + groupInstances.forEach((groupInstance: ZoneInstance) => { + let handle: string = this.getCorrectHandleForNode(groupInstance.instanceData.uniqueId, policyInstance); + groupInstance.showHandle(handle); + }) + }; + + public hideGroupZoneIndications = (instances: Array<ZoneInstance>) => { + instances.forEach((instance) => { + instance.hideHandle(); + }) + } + + public showZoneTagIndications = (cy: Cy.Instance, zoneInstance: ZoneInstance) => { + + cy.nodes().forEach(node => { + let handleType: string = this.getCorrectHandleForNode(node.id(), zoneInstance); + cy.emit('showhandle', [node, handleType]); + }); + }; + + public showZoneTagIndicationForNode = (nodeId: string, zoneInstance: ZoneInstance, cy: Cy.Instance) => { + let node = cy.getElementById(nodeId); + let handleType: string = this.getCorrectHandleForNode(nodeId, zoneInstance); + cy.emit('showhandle', [node, handleType]); + } + + public hideZoneTagIndications = (cy: Cy.Instance) => { + cy.emit('hidehandles'); + }; + + public getCorrectHandleForNode = (nodeId: string, zoneInstance: ZoneInstance): string => { + if (zoneInstance.isAlreadyAssigned(nodeId)) { + if (zoneInstance.type == ZoneInstanceType.POLICY) { + return CanvasHandleTypes.TAGGED_POLICY; + } else { + return CanvasHandleTypes.TAGGED_GROUP; + } + } else { + return CanvasHandleTypes.TAG_AVAILABLE; + } + }; +} diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/utils/index.ts b/catalog-ui/src/app/ng2/pages/composition/graph/utils/index.ts new file mode 100644 index 0000000000..e7f11af248 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/utils/index.ts @@ -0,0 +1,29 @@ +/** + * Created by ob0695 on 6/3/2018. + */ +// export * from './composition-graph-general-utils'; +// export * from './composition-graph-links-utils'; +// export * from './composition-graph-nodes-utils'; +// export * from './composition-graph-palette-utils'; +// export * from './composition-graph-service-path-utils'; +// export * from './composition-graph-zone-utils'; + + +import {CompositionGraphGeneralUtils} from './composition-graph-general-utils'; +import {CompositionGraphNodesUtils} from './composition-graph-nodes-utils'; +import {MatchCapabilitiesRequirementsUtils} from './match-capability-requierment-utils' +import {CompositionGraphPaletteUtils} from './composition-graph-palette-utils'; +import {CompositionGraphZoneUtils} from './composition-graph-zone-utils'; +import {ServicePathGraphUtils} from './composition-graph-service-path-utils'; +import {CompositionGraphLinkUtils} from "./composition-graph-links-utils"; + + +export { + CompositionGraphGeneralUtils, + CompositionGraphLinkUtils, + CompositionGraphNodesUtils, + MatchCapabilitiesRequirementsUtils, + CompositionGraphPaletteUtils, + CompositionGraphZoneUtils, + ServicePathGraphUtils +};
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/utils/match-capability-requierment-utils.spec.ts b/catalog-ui/src/app/ng2/pages/composition/graph/utils/match-capability-requierment-utils.spec.ts new file mode 100644 index 0000000000..dbfc3e7219 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/utils/match-capability-requierment-utils.spec.ts @@ -0,0 +1,342 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Mock } from 'ts-mockery'; +import { + CapabilitiesGroup, + Capability, ComponentInstance, CompositionCiLinkBase, CompositionCiNodeBase, CompositionCiNodeCp, + CompositionCiNodeVf, CompositionCiNodeVl, + Requirement, RequirementsGroup +} from '../../../../../models'; +import { MatchCapabilitiesRequirementsUtils } from './match-capability-requierment-utils'; + +describe('match capability requirements utils service ', () => { + + const bindableReq = Mock.of<Requirement>({ + capability : 'tosca.capabilities.network.Bindable', + name: 'virtualBinding', + relationship: 'tosca.relationships.network.BindsTo', + uniqueId: 'eef99154-8039-4227-ba68-62a32e6b0d98.virtualBinding', + ownerId : 'extcp0', + ownerName : 's' + }); + + const virtualLinkReq = Mock.of<Requirement>({ + capability: 'tosca.capabilities.network.Linkable', + name: 'virtualLink', + relationship: 'tosca.relationships.network.LinksTo', + uniqueId: 'eef99154-8039-4227-ba68-62a32e6b0d98.virtualLink', + ownerId : '', + ownerName : 's' + }); + + const storeAttachmentReq = Mock.of<Requirement>({ + capability: 'tosca.capabilities.Attachment', + name: 'local_storage', + relationship: 'tosca.relationships.AttachesTo', + uniqueId: 'eef99154-8039-4227-ba68-62a32e6b0d98.local_storage', + node: 'tosca.nodes.BlockStorage', + ownerId : '', + ownerName : 's' + }); + + const vlAttachmentReq = Mock.of<Requirement>({ + capability: 'tosca.capabilities.Attachment', + name: 'local_storage', + relationship: 'tosca.relationships.AttachesTo', + uniqueId: 'eef99154-8039-4227-ba68-62a32e6b0d98.local_storage', + node: 'tosca.nodes.BlockStorage', + ownerId : '', + ownerName : 's' + }); + + const extVirtualLinkReq = Mock.of<Requirement>({ + capability: 'tosca.capabilities.network.Linkable', + name: 'external_virtualLink', + relationship: 'tosca.relationships.network.LinksTo', + uniqueId: 'eef99154-8039-4227-ba68-62a32e6b0d98.external_virtualLink' + }); + + const dependencyReq = Mock.of<Requirement>({ + capability: 'tosca.capabilities.Node', + name: 'dependency', + relationship: 'tosca.relationships.DependsOn', + uniqueId: 'eef99154-8039-4227-ba68-62a32e6b0d98.dependency' + }); + + const featureCap = Mock.of<Capability>({ + type: 'tosca.capabilities.Node', + name: 'feature', + uniqueId: 'capability.ddf1301e-866b-4fa3-bc4f-edbd81e532cd.feature', + maxOccurrences: 'UNBOUNDED', + minOccurrences: '1' + }); + + const internalConnPointCap = Mock.of<Capability>({ + type: 'tosca.capabilities.Node', + name: 'internal_connectionPoint', + capabilitySources : ['org.openecomp.resource.cp.extCP'], + uniqueId: 'capability.ddf1301e-866b-4fa3-bc4f-edbd81e532cd.internal_connectionPoint', + maxOccurrences: 'UNBOUNDED', + minOccurrences: '1' + }); + + const blockStoreAttachmentCap = Mock.of<Capability>({ + type: 'tosca.capabilities.Attachment', + name: 'attachment', + capabilitySources: ['tosca.nodes.BlockStorage'], + uniqueId: 'capability.ddf1301e-866b-4fa3-bc4f-edbd81e532cd.attachment', + maxOccurrences: 'UNBOUNDED', + minOccurrences: '1' + }); + + const bindingCap = Mock.of<Capability>({ + type: 'tosca.capabilities.network.Bindable', + name: 'binding', + capabilitySources: ['tosca.nodes.Compute'], + uniqueId: 'capability.ddf1301e-866b-4fa3-bc4f-edbd81e532cd.binding', + maxOccurrences: 'UNBOUNDED', + minOccurrences: '1', + }); + + const linkableCap = Mock.of<Capability>({ + type: 'tosca.capabilities.network.Linkable', + capabilitySources: ['org.openecomp.resource.vl.extVL'], + uniqueId: 'capability.ddf1301e-866b-4fa3-bc4f-edbd81e532cd.virtual_linkable', + maxOccurrences: 'UNBOUNDED', + minOccurrences: '1' + }); + + const nodeCompute = Mock.of<CompositionCiNodeVf>({ + name: 'Compute 0', + componentInstance: Mock.of<ComponentInstance>({ + componentName: 'Compute', + uniqueId : 'compute0', + requirements: Mock.of<RequirementsGroup>({ + 'tosca.capabilities.Node' : [ dependencyReq ], + 'tosca.capabilities.Attachment' : [ storeAttachmentReq ] + }), + capabilities: Mock.of<CapabilitiesGroup>({ + 'tosca.capabilities.network.Bindable' : [ bindingCap ], + 'tosca.capabilities.Node' : [ featureCap ] + }) + }) + }); + + const nodeBlockStorage = Mock.of<CompositionCiNodeVf>({ + name: 'BlockStorage 0', + componentInstance: Mock.of<ComponentInstance>({ + componentName: 'BlockStorage', + uniqueId : 'blockstorage0', + requirements: Mock.of<RequirementsGroup>({ + 'tosca.capabilities.Node' : [ dependencyReq ] + }), + capabilities: Mock.of<CapabilitiesGroup>({ + 'tosca.capabilities.Attachment' : [ blockStoreAttachmentCap ], + 'tosca.capabilities.Node' : [ featureCap ] + }) + }) + }); + + const nodeVl = Mock.of<CompositionCiNodeVl>({ + name: 'ExtVL 0', + componentInstance: Mock.of<ComponentInstance>({ + componentName: 'BlockStorage', + uniqueId : 'extvl0', + requirements: Mock.of<RequirementsGroup>({ + 'tosca.capabilities.Node' : [ dependencyReq ] + }), + capabilities: Mock.of<CapabilitiesGroup>({ + 'tosca.capabilities.network.Linkable' : [ linkableCap ], + 'tosca.capabilities.Node' : [ featureCap ] + }) + }) + }); + + const nodeCp = Mock.of<CompositionCiNodeCp>({ + name: 'ExtCP 0', + componentInstance: Mock.of<ComponentInstance>({ + componentName: 'ExtCP', + uniqueId : 'extcp0', + requirements: Mock.of<RequirementsGroup>({ + 'tosca.capabilities.network.Linkable' : [ virtualLinkReq ], + 'tosca.capabilities.network.Bindable' : [ bindableReq ] + }), + capabilities: Mock.of<CapabilitiesGroup>({ + 'tosca.capabilities.Node' : [ featureCap ] + }) + }) + }); + + let service: MatchCapabilitiesRequirementsUtils; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [], + providers: [MatchCapabilitiesRequirementsUtils] + }); + + service = TestBed.get(MatchCapabilitiesRequirementsUtils); + }); + + it('match capability requirements utils should be defined', () => { + console.log(JSON.stringify(service)); + expect(service).toBeDefined(); + }); + + describe('isMatch function ', () => { + + it('capability type not equal to requirement capability, match is false', () => { + const requirement = Mock.of<Requirement>({capability: 'tosca.capabilities.network.Linkable11'}); + const capability = Mock.of<Capability>({type: 'tosca.capabilities.network.Linkable'}); + expect(service.isMatch(requirement, capability)).toBeFalsy(); + }); + + it('capability type equal to requirement capability and requirement node not exist, match is true', () => { + const requirement = Mock.of<Requirement>({capability: 'tosca.capabilities.network.Linkable'}); + const capability = Mock.of<Capability>({type: 'tosca.capabilities.network.Linkable'}); + expect(service.isMatch(requirement, capability)).toBeTruthy(); + }); + + it('is match - capability type equal to requirement capability and requirement node exist and includes in capability sources, match is true', () => { + const requirement = Mock.of<Requirement>({capability: 'tosca.capabilities.network.Linkable', node: 'node1'}); + const capability = Mock.of<Capability>({ + type: 'tosca.capabilities.network.Linkable', + capabilitySources: ['node1', 'node2', 'node3'] + }); + expect(service.isMatch(requirement, capability)).toBeTruthy(); + }); + + it('no match - capability type equal to requirement capability and requirement node but not includes in capability sources, match is false', () => { + const requirement = Mock.of<Requirement>({capability: 'tosca.capabilities.network.Linkable', node: 'node4'}); + const capability = Mock.of<Capability>({ + type: 'tosca.capabilities.network.Linkable', + capabilitySources: ['node1', 'node2', 'node3'] + }); + expect(service.isMatch(requirement, capability)).toBeFalsy(); + }); + }); + + describe('hasUnfulfilledRequirementContainingMatch function ', () => { + + it('node have no componentInstance, return false', () => { + const node = Mock.of<CompositionCiNodeVf>({componentInstance: undefined}); + expect(service.hasUnfulfilledRequirementContainingMatch(node, [], {}, [])).toBeFalsy(); + }); + + it('node have componentInstance data but no unfulfilled requirements, return false', () => { + const node = Mock.of<CompositionCiNodeVf>({componentInstance: Mock.of<ComponentInstance>()}); + jest.spyOn(service, 'getUnfulfilledRequirements').mockReturnValue([]); + expect(service.hasUnfulfilledRequirementContainingMatch(node, [], {}, [])).toBeFalsy(); + }); + + it('node have componentInstance data and unfulfilled requirements but no match found, return false', () => { + const node = Mock.of<CompositionCiNodeVf>({componentInstance: Mock.of<ComponentInstance>()}); + jest.spyOn(service, 'getUnfulfilledRequirements').mockReturnValue([Mock.of<Requirement>(), Mock.of<Requirement>()]); + jest.spyOn(service, 'containsMatch').mockReturnValue(false); + expect(service.hasUnfulfilledRequirementContainingMatch(node, [], {}, [])).toBeFalsy(); + }); + + it('node have componentInstance data with unfulfilled requirements and match found, return true', () => { + const node = Mock.of<CompositionCiNodeVf>({componentInstance: Mock.of<ComponentInstance>()}); + jest.spyOn(service, 'getUnfulfilledRequirements').mockReturnValue([Mock.of<Requirement>(), Mock.of<Requirement>()]); + jest.spyOn(service, 'containsMatch').mockReturnValue(true); + expect(service.hasUnfulfilledRequirementContainingMatch(node, [], {}, [])).toBeTruthy(); + }); + }); + + describe('getMatches function ', () => { + let fromId: string; + let toId: string; + + beforeEach(() => { + fromId = 'from_id'; + toId = 'to_id'; + }); + + it('node have no unfulfilled requirements, return empty match array', () => { + jest.spyOn(service, 'getUnfulfilledRequirements').mockReturnValue([]); + expect(service.getMatches({}, {}, [], fromId, toId, true)).toHaveLength(0); + }); + + it('node have unfulfilled requirements but no capabilities, return empty match array', () => { + jest.spyOn(service, 'getUnfulfilledRequirements').mockReturnValue([Mock.of<Requirement>(), Mock.of<Requirement>()]); + expect(service.getMatches({}, {}, [], fromId, toId, true)).toHaveLength(0); + }); + + it('node have unfulfilled requirements and capabilities but no match found, return empty match array', () => { + jest.spyOn(service, 'getUnfulfilledRequirements').mockReturnValue([Mock.of<Requirement>(), Mock.of<Requirement>()]); + jest.spyOn(service, 'isMatch').mockReturnValue(false); + expect(service.getMatches({}, {}, [], fromId, toId, true)).toHaveLength(0); + }); + + it('node have 2 unfulfilled requirements and 2 capabilities and match found, return 4 matches', () => { + jest.spyOn(service, 'getUnfulfilledRequirements').mockReturnValue([Mock.of<Requirement>(), Mock.of<Requirement>()]); + const capabilities = {aaa: Mock.of<Capability>(), bbb: Mock.of<Capability>()}; + jest.spyOn(service, 'isMatch').mockReturnValue(true); + expect(service.getMatches({}, capabilities, [], fromId, toId, true)).toHaveLength(4); + }); + }); + + describe('Find matching nodes ===>', () => { + + it('should find matching nodes with component instance', () => { + const nodes = [ nodeBlockStorage, nodeCompute, nodeVl ]; + let matchingNodes: any; + + // Compute can connect to Block Store + matchingNodes = service.findMatchingNodesToComponentInstance(nodeCompute.componentInstance, nodes, []); + expect(matchingNodes).toHaveLength(1); + expect(matchingNodes).toContain(nodeBlockStorage); + + // Block Storage can connect to Compute + matchingNodes = service.findMatchingNodesToComponentInstance(nodeBlockStorage.componentInstance, nodes, []); + expect(matchingNodes).toHaveLength(1); + expect(matchingNodes).toContain(nodeCompute); + + // Vl has no matches + matchingNodes = service.findMatchingNodesToComponentInstance(nodeVl.componentInstance, nodes, []); + expect(matchingNodes).toHaveLength(0); + + // CP should be able to connect to VL and Compute + matchingNodes = service.findMatchingNodesToComponentInstance(nodeCp.componentInstance, nodes, []); + expect(matchingNodes).toHaveLength(2); + expect(matchingNodes).toContain(nodeCompute); + expect(matchingNodes).toContain(nodeVl); + }); + + it('try with empty list of nodes', () => { + const nodes = [ ]; + let matchingNodes: any; + + // Compute can connect to Block Store + matchingNodes = service.findMatchingNodesToComponentInstance(nodeCompute.componentInstance, nodes, []); + expect(matchingNodes).toHaveLength(0); + }); + + it('should detect fulfilled connection with compute node', () => { + const nodes = [ nodeBlockStorage, nodeCompute, nodeVl ]; + let matchingNodes: any; + const link = { + relation: { + fromNode: 'extcp0', + toNode: 'compute0', + relationships: [{ + relation: { + requirementOwnerId: 'extcp0', + requirement: 'virtualBinding', + relationship: { + type: 'tosca.relationships.network.BindsTo' + } + + } + }] + } + }; + + const links = [link]; + // CP should be able to connect to VL only since it already has a link with compute + matchingNodes = service.findMatchingNodesToComponentInstance(nodeCp.componentInstance, nodes, links as CompositionCiLinkBase[]); + expect(matchingNodes).toHaveLength(1); + expect(matchingNodes).toContain(nodeVl); + }); + }); +}); diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/utils/match-capability-requierment-utils.ts b/catalog-ui/src/app/ng2/pages/composition/graph/utils/match-capability-requierment-utils.ts new file mode 100644 index 0000000000..c3a1286a97 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/utils/match-capability-requierment-utils.ts @@ -0,0 +1,196 @@ +/*- + * ============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========================================================= + */ +import { Injectable } from '@angular/core'; +import { + CapabilitiesGroup, Capability, ComponentInstance, CompositionCiLinkBase, + CompositionCiNodeBase, Match, Requirement, RequirementsGroup +} from 'app/models'; +import * as _ from 'lodash'; + +/** + * Created by obarda on 1/1/2017. + */ +@Injectable() +export class MatchCapabilitiesRequirementsUtils { + + /** + * Shows + icon in corner of each node passed in + * @param filteredNodesData + * @param cy + */ + public highlightMatchingComponents(filteredNodesData, cy: Cy.Instance) { + _.each(filteredNodesData, (data: any) => { + const node = cy.getElementById(data.id); + cy.emit('showhandle', [node]); + }); + } + + /** + * Adds opacity to each node that cannot be linked to hovered node + * @param filteredNodesData + * @param nodesData + * @param cy + * @param hoveredNodeData + */ + public fadeNonMachingComponents(filteredNodesData, nodesData, cy: Cy.Instance, hoveredNodeData?) { + const fadeNodes = _.xorWith(nodesData, filteredNodesData, (node1, node2) => { + return node1.id === node2.id; + }); + if (hoveredNodeData) { + _.remove(fadeNodes, hoveredNodeData); + } + cy.batch(() => { + _.each(fadeNodes, (node) => { + cy.getElementById(node.id).style({'background-image-opacity': 0.4}); + }); + }); + } + + /** + * Resets all nodes to regular opacity + * @param cy + */ + public resetFadedNodes(cy: Cy.Instance) { + cy.batch(() => { + cy.nodes().style({'background-image-opacity': 1}); + }); + } + + public getMatchedRequirementsCapabilities(fromComponentInstance: ComponentInstance, + toComponentInstance: ComponentInstance, + links: CompositionCiLinkBase[]): Match[] { + const fromToMatches: Match[] = this.getMatches(fromComponentInstance.requirements, + toComponentInstance.capabilities, + links, + fromComponentInstance.uniqueId, + toComponentInstance.uniqueId, true); + const toFromMatches: Match[] = this.getMatches(toComponentInstance.requirements, + fromComponentInstance.capabilities, + links, + toComponentInstance.uniqueId, + fromComponentInstance.uniqueId, false); + + return fromToMatches.concat(toFromMatches); + } + + /***** REFACTORED FUNCTIONS START HERE *****/ + + public getMatches(requirements: RequirementsGroup, capabilities: CapabilitiesGroup, links: CompositionCiLinkBase[], + fromId: string, toId: string, isFromTo: boolean): Match[] { + const matches: Match[] = []; + const unfulfilledReqs = this.getUnfulfilledRequirements(fromId, requirements, links); + _.forEach(unfulfilledReqs, (req) => { + _.forEach(_.flatten(_.values(capabilities)), (capability: Capability) => { + if (this.isMatch(req, capability)) { + if (isFromTo) { + matches.push(new Match(req, capability, isFromTo, fromId, toId)); + } else { + matches.push(new Match(req, capability, isFromTo, toId, fromId)); + } + } + }); + }); + return matches; + } + + public getUnfulfilledRequirements = (fromNodeId: string, requirements: RequirementsGroup, links: CompositionCiLinkBase[]): Requirement[] => { + const requirementArray: Requirement[] = []; + _.forEach(_.flatten(_.values(requirements)), (requirement: Requirement) => { + const reqFulfilled = this.isRequirementFulfilled(fromNodeId, requirement, links); + if (requirement.name !== 'dependency' && requirement.parentName !== 'dependency' && !reqFulfilled) { + requirementArray.push(requirement); + } + }); + return requirementArray; + } + + /** + * Returns true if there is a match between the capabilities and requirements that are passed in + * @param requirements + * @param capabilities + * @returns {boolean} + */ + public containsMatch = (requirements: Requirement[], capabilities: CapabilitiesGroup): boolean => { + return _.some(requirements, (req: Requirement) => { + return _.some(_.flatten(_.values(capabilities)), (capability: Capability) => { + return this.isMatch(req, capability); + }); + }); + } + + public hasUnfulfilledRequirementContainingMatch = (node: CompositionCiNodeBase, componentRequirements: Requirement[], capabilities: CapabilitiesGroup, links: CompositionCiLinkBase[]) => { + if (node && node.componentInstance) { + // Check if node has unfulfilled requirement that can be filled by component (#2) + const nodeRequirements: Requirement[] = this.getUnfulfilledRequirements(node.componentInstance.uniqueId, node.componentInstance.requirements, links); + if (!nodeRequirements.length) { + return false; + } + if (this.containsMatch(nodeRequirements, capabilities)) { + return true; + } + } + } + + /** + * Returns array of nodes that can connect to the component. + * In order to connect, one of the following conditions must be met: + * 1. component has an unfulfilled requirement that matches a node's capabilities + * 2. node has an unfulfilled requirement that matches the component's capabilities + * 3. vl is passed in which has the capability to fulfill requirement from component and requirement on node. + */ + public findMatchingNodesToComponentInstance(componentInstance: ComponentInstance, nodeDataArray: CompositionCiNodeBase[], links: CompositionCiLinkBase[]): any[] { + return _.filter(nodeDataArray, (node: CompositionCiNodeBase) => { + const matchedRequirementsCapabilities = this.getMatchedRequirementsCapabilities(node.componentInstance, componentInstance, links); + return matchedRequirementsCapabilities && matchedRequirementsCapabilities.length > 0; + }); + } + + public isMatch(requirement: Requirement, capability: Capability): boolean { + if (capability.type === requirement.capability) { + if (requirement.node) { + if (_.includes(capability.capabilitySources, requirement.node)) { + return true; + } + } else { + return true; + } + } + return false; + } + + private isRequirementFulfilled(fromNodeId: string, requirement: any, links: CompositionCiLinkBase[]): boolean { + return _.some(links, { + relation: { + fromNode: fromNodeId, + relationships: [{ + relation: { + requirementOwnerId: requirement.ownerId, + requirement: requirement.name, + relationship: { + type: requirement.relationship + } + + } + }] + } + }); + } + +} diff --git a/catalog-ui/src/app/ng2/pages/composition/palette/__snapshots__/palette.component.spec.ts.snap b/catalog-ui/src/app/ng2/pages/composition/palette/__snapshots__/palette.component.spec.ts.snap new file mode 100644 index 0000000000..74517e1eb0 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/palette/__snapshots__/palette.component.spec.ts.snap @@ -0,0 +1,51 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`palette component should match current snapshot of palette component 1`] = ` +<composition-palette + buildPaletteByCategories={[Function Function]} + compositionPaletteService={[Function Object]} + eventListenerService={[Function Object]} + numberOfElements="0" + onDragStart={[Function Function]} + onDraggableMoved={[Function Function]} + onDrop={[Function Function]} + onMouseOut={[Function Function]} + onMouseOver={[Function Function]} + onSearchChanged={[Function Function]} + position={[Function Point]} +> + <div + class="composition-palette-component" + > + <div + class="palette-elements-count" + > + Elements + <span + class="palette-elements-count-value" + > + + </span> + </div> + <sdc-filter-bar + placeholder="Search..." + testid="searchAsset" + /> + <div + class="palette-elements-list" + > + <sdc-loader + name="palette-loader" + testid="palette-loader" + /> + + + </div> + </div><div + dnddropzone="" + id="draggable_element" + > + + </div> +</composition-palette> +`; diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-tabs.component.html b/catalog-ui/src/app/ng2/pages/composition/palette/palette-animation/palette-animation.component.html index 482de5eacf..efd619687c 100644 --- a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-tabs.component.html +++ b/catalog-ui/src/app/ng2/pages/composition/palette/palette-animation/palette-animation.component.html @@ -14,14 +14,7 @@ ~ limitations under the License. --> -<sdc-tabs> - <sdc-tab titleIcon="info-circle"> - <group-information-tab [group]="group" [isViewOnly]="isViewOnly" *ngIf="group"></group-information-tab> - </sdc-tab> - <sdc-tab titleIcon="inputs-o"> - <group-members-tab [group]="group" [topologyTemplate]="topologyTemplate" [isViewOnly]="isViewOnly" (isLoading)="setIsLoading($event)" *ngIf="group"></group-members-tab> - </sdc-tab> - <sdc-tab titleIcon="settings-o"> - <group-properties-tab [group]="group" [topologyTemplate]="topologyTemplate" [isViewOnly]="isViewOnly" *ngIf="group"></group-properties-tab> - </sdc-tab> -</sdc-tabs> +<div class="palette-animation-wrapper" [style.top]="from.y + 50 + 'px'" [style.left]="from.x + 'px'" [style.transform]="transformStyle" [class.hidden]="!visible" + (transitionend)="animationComplete()"> +<div class="medium small sprite-resource-icons sprite-{{iconName}}-icons {{iconName}}" ></div> +</div> diff --git a/catalog-ui/src/app/ng2/pages/composition/palette/palette-animation/palette-animation.component.less b/catalog-ui/src/app/ng2/pages/composition/palette/palette-animation/palette-animation.component.less new file mode 100644 index 0000000000..54f04189c0 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/palette/palette-animation/palette-animation.component.less @@ -0,0 +1,5 @@ +.palette-animation-wrapper{ + position: absolute; + z-index: 100; + transition: all 2s ease-in-out; +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/composition/palette/palette-animation/palette-animation.component.ts b/catalog-ui/src/app/ng2/pages/composition/palette/palette-animation/palette-animation.component.ts new file mode 100644 index 0000000000..a445c87f42 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/palette/palette-animation/palette-animation.component.ts @@ -0,0 +1,71 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +import {Component, Input } from '@angular/core'; +import {BrowserModule} from '@angular/platform-browser'; +import { setTimeout } from 'core-js/library/web/timers'; +import { EventListenerService } from 'app/services'; +import { GRAPH_EVENTS } from 'app/utils'; +import { Point } from 'app/models'; +import { ZoneInstanceType, ZoneInstance } from 'app/models/graph/zones/zone-instance'; + + + +@Component({ + selector: 'palette-animation', + templateUrl: './palette-animation.component.html', + styleUrls:['./palette-animation.component.less'], +}) + +export class PaletteAnimationComponent { + + @Input() from : Point; + @Input() to : Point; + @Input() type: ZoneInstanceType; + @Input() iconName : string; + @Input() zoneInstance : ZoneInstance; + + public animation; + private visible:boolean = false; + private transformStyle:string = ""; + + + constructor(private eventListenerService:EventListenerService) {} + + + ngOnDestroy(){ + this.zoneInstance.hidden = false; //if animation component is destroyed before animation is complete + } + + public runAnimation() { + this.visible = true; + let positionDiff:Point = new Point(this.to.x - this.from.x, this.to.y - this.from.y); + setTimeout(()=>{ + this.transformStyle = 'translate('+ positionDiff.x + 'px,' + positionDiff.y +'px)'; + }, 0); + }; + + public animationComplete = (e) => { + this.visible = false; + this.zoneInstance.hidden = false; + }; + + +} diff --git a/catalog-ui/src/app/ng2/pages/composition/palette/palette-animation/palette-animation.module.ts b/catalog-ui/src/app/ng2/pages/composition/palette/palette-animation/palette-animation.module.ts new file mode 100644 index 0000000000..8674571138 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/palette/palette-animation/palette-animation.module.ts @@ -0,0 +1,16 @@ +import { NgModule } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { PaletteAnimationComponent } from "./palette-animation.component"; + + +@NgModule({ + declarations: [ + PaletteAnimationComponent + ], + imports: [ CommonModule ], + exports: [ PaletteAnimationComponent ] +}) + +export class PaletteAnimationModule { + +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/composition/palette/palette-element/__snapshots__/palette-element.component.spec.ts.snap b/catalog-ui/src/app/ng2/pages/composition/palette/palette-element/__snapshots__/palette-element.component.spec.ts.snap new file mode 100644 index 0000000000..40df575519 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/palette/palette-element/__snapshots__/palette-element.component.spec.ts.snap @@ -0,0 +1,29 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`palette element component should match current snapshot of palette element component 1`] = ` +<palette-element> + <div + class="palette-element" + > + <sdc-element-icon + class="palette-element-icon" + /> + <div + class="palette-element-text" + > + <div + class="palette-element-name" + sdc-tooltip="" + > + + </div> + <span> + V. + </span> + <span> + + </span> + </div> + </div> +</palette-element> +`; diff --git a/catalog-ui/src/app/ng2/pages/composition/palette/palette-element/palette-element.component.html b/catalog-ui/src/app/ng2/pages/composition/palette/palette-element/palette-element.component.html new file mode 100644 index 0000000000..3a6be5d082 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/palette/palette-element/palette-element.component.html @@ -0,0 +1,11 @@ +<div class="palette-element" > + <sdc-element-icon class="palette-element-icon" [iconName]="paletteElement.icon" + [elementType]="paletteElement.componentSubType"[uncertified]="this.paletteElement.certifiedIconClass"></sdc-element-icon> + <div class="palette-element-text"> + <div class="palette-element-name" sdc-tooltip + tooltip-text='{{paletteElement.name | resourceName}}'>{{paletteElement.name | resourceName}} + </div> + <span> V.{{paletteElement.version}}</span> + <span>{{paletteElement.componentSubType}}</span> + </div> +</div>
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/composition/palette/palette-element/palette-element.component.less b/catalog-ui/src/app/ng2/pages/composition/palette/palette-element/palette-element.component.less new file mode 100644 index 0000000000..e9c3253fbd --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/palette/palette-element/palette-element.component.less @@ -0,0 +1,32 @@ +@import "./../../../../../../assets/styles/override"; +.palette-element { + cursor: pointer; + display: flex; + flex-direction: row; + max-height: 65px; + border-bottom: 1px solid @sdcui_color_silver; + padding: 10px; + align-items: center; + .palette-element-icon { + min-width: 45px; + text-align: center; + } + + .palette-element-text { + display: flex; + flex-direction: column; + font-size: 13px; + line-height: 15px; + padding-left: 10px; + font-family: OpenSans-Regular, sans-serif; + overflow: hidden; + + .palette-element-name { + color: @sdcui_color_dark-gray; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + } + } + +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/composition/palette/palette-element/palette-element.component.spec.ts b/catalog-ui/src/app/ng2/pages/composition/palette/palette-element/palette-element.component.spec.ts new file mode 100644 index 0000000000..64ed45ba9c --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/palette/palette-element/palette-element.component.spec.ts @@ -0,0 +1,30 @@ +import {async, ComponentFixture} from "@angular/core/testing"; +import {ConfigureFn, configureTests} from "../../../../../../jest/test-config.helper"; +import {NO_ERRORS_SCHEMA} from "@angular/core"; +import {PaletteElementComponent} from "./palette-element.component"; +import {ResourceNamePipe} from "../../../../pipes/resource-name.pipe"; + +describe('palette element component', () => { + + let fixture: ComponentFixture<PaletteElementComponent>; + + beforeEach( + async(() => { + const configure: ConfigureFn = testBed => { + testBed.configureTestingModule({ + declarations: [PaletteElementComponent, ResourceNamePipe], + imports: [], + schemas: [NO_ERRORS_SCHEMA] + }); + }; + + configureTests(configure).then(testBed => { + fixture = testBed.createComponent(PaletteElementComponent); + }); + }) + ); + + it('should match current snapshot of palette element component', () => { + expect(fixture).toMatchSnapshot(); + }); +});
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-information-tab.component.ts b/catalog-ui/src/app/ng2/pages/composition/palette/palette-element/palette-element.component.ts index 26602224da..9e9e5a29da 100644 --- a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-information-tab.component.ts +++ b/catalog-ui/src/app/ng2/pages/composition/palette/palette-element/palette-element.component.ts @@ -1,3 +1,6 @@ +/** + * Created by ob0695 on 6/28/2018. + */ /*- * ============LICENSE_START======================================================= * SDC @@ -18,22 +21,15 @@ * ============LICENSE_END========================================================= */ -import * as _ from "lodash"; -import { Component, Inject, Input, Output, EventEmitter } from "@angular/core"; -import { GroupInstance } from 'app/models/graph/zones/group-instance'; +import {Component, Input} from "@angular/core"; +import {LeftPaletteComponent} from "app/models/components/displayComponent"; @Component({ - selector: 'group-information-tab', - templateUrl: './group-information-tab.component.html', - styleUrls: ['./../base/base-tab.component.less'] + selector: 'palette-element', + templateUrl: './palette-element.component.html', + styleUrls: ['./palette-element.component.less'] }) -export class GroupInformationTabComponent { - - @Input() group: GroupInstance; - @Input() isViewOnly: boolean; - - constructor() { - - } +export class PaletteElementComponent { + @Input() paletteElement: LeftPaletteComponent; } diff --git a/catalog-ui/src/app/ng2/pages/composition/palette/palette-popup-panel/palette-popup-panel.component.html b/catalog-ui/src/app/ng2/pages/composition/palette/palette-popup-panel/palette-popup-panel.component.html new file mode 100644 index 0000000000..86847eb28a --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/palette/palette-popup-panel/palette-popup-panel.component.html @@ -0,0 +1,30 @@ +<!-- + ~ Copyright (C) 2018 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. + --> + +<div class="popup-panel" [ngClass]="{'hide':!isShowPanel}" [style.left]="popupPanelPosition.x + 'px'" [style.top]="popupPanelPosition.y + 'px'" + (mousedown)="addZoneInstance()" + (mouseenter)="onMouseEnter()" + (mouseleave)="onMouseLeave()"> + <div class="popup-panel-group"> + <div class="popup-panel-plus">+</div> + <div class="popup-panel-title">{{panelTitle}}</div> + </div> +</div> +<!--<popup-menu-list [menuItemsData]="getMenuItems()">--> + + + +<!--</popup-menu-list>-->
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/composition/palette/palette-popup-panel/palette-popup-panel.component.less b/catalog-ui/src/app/ng2/pages/composition/palette/palette-popup-panel/palette-popup-panel.component.less new file mode 100644 index 0000000000..24f0485e76 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/palette/palette-popup-panel/palette-popup-panel.component.less @@ -0,0 +1,37 @@ +.popup-panel { + position: absolute; + display: inline-block; + background-color: white; + border: solid 1px #d2d2d2; + border-top: solid 3px #13a7df; + left: 208px; top: 0px; + width: 140px; + height: 40px; + z-index: 10000; + + &:hover { + background-color: whitesmoke; + } + + .popup-panel-group { + padding-left: 8px; + padding-top: 8px; + cursor: pointer; + + .popup-panel-plus { + border-radius: 50%; + color: white; + background-color: #13a7df; + width: 20px; + text-align: center; + display: inline-block; + } + + .popup-panel-title { + padding-left: 10px; + display: inline-block; + } + + } + +} diff --git a/catalog-ui/src/app/ng2/pages/composition/palette/palette-popup-panel/palette-popup-panel.component.ts b/catalog-ui/src/app/ng2/pages/composition/palette/palette-popup-panel/palette-popup-panel.component.ts new file mode 100644 index 0000000000..5d98fc7f78 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/palette/palette-popup-panel/palette-popup-panel.component.ts @@ -0,0 +1,98 @@ +import {Component, OnInit} from '@angular/core'; +import {GRAPH_EVENTS, SdcElementType} from "app/utils"; +import {LeftPaletteComponent, Point} from "app/models"; +import {EventListenerService} from "app/services"; +import {LeftPaletteMetadataTypes} from "app/models/components/displayComponent"; + +@Component({ + selector: 'app-palette-popup-panel', + templateUrl: './palette-popup-panel.component.html', + styleUrls: [ './palette-popup-panel.component.less' ], +}) +export class PalettePopupPanelComponent implements OnInit { + + public panelTitle: string; + public isShowPanel: boolean; + private component: Component; + private displayComponent: LeftPaletteComponent; + private popupPanelPosition:Point = new Point(0,0); + + constructor(private eventListenerService: EventListenerService) { + this.isShowPanel = false; + } + + ngOnInit() { + this.registerObserverCallbacks(); + } + + public onMouseEnter() { + this.isShowPanel = true; + } + + public getMenuItems = () => { + return [{ + text: 'Delete', + iconName: 'trash-o', + iconType: 'common', + iconMode: 'secondary', + iconSize: 'small', + type: '', + action: () => this.addZoneInstance() + }]; + } + + public onMouseLeave() { + this.isShowPanel = false; + } + + public addZoneInstance(): void { + if(this.displayComponent) { + this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_ADD_ZONE_INSTANCE_FROM_PALETTE, this.component, this.displayComponent, this.popupPanelPosition); + this.hidePopupPanel(); + } + } + + private registerObserverCallbacks() { + + this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_PALETTE_COMPONENT_SHOW_POPUP_PANEL, + (displayComponent: LeftPaletteComponent, sectionElem: HTMLElement) => { + this.showPopupPanel(displayComponent, sectionElem); + }); + + this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_PALETTE_COMPONENT_HIDE_POPUP_PANEL, () => this.hidePopupPanel()); + } + + private getPopupPanelPosition (sectionElem: HTMLElement):Point { + let pos: ClientRect = sectionElem.getBoundingClientRect(); + let offsetX: number = -30; + const offsetY: number = pos.height / 2; + return new Point((pos.right + offsetX), (pos.top - offsetY + window.pageYOffset)); + }; + + private setPopupPanelTitle(component: LeftPaletteComponent): void { + if (component.componentSubType === SdcElementType.GROUP) { + this.panelTitle = "Add Group"; + return; + } + + if (component.componentSubType === SdcElementType.POLICY) { + this.panelTitle = "Add Policy"; + return; + } + } + + private showPopupPanel(displayComponent:LeftPaletteComponent, sectionElem: HTMLElement) { + if(!this.isShowPanel){ + this.displayComponent = displayComponent; + this.setPopupPanelTitle(displayComponent); + this.popupPanelPosition = this.getPopupPanelPosition(sectionElem); + this.isShowPanel = true; + } + }; + + private hidePopupPanel() { + if(this.isShowPanel){ + this.isShowPanel = false; + } + }; +} diff --git a/catalog-ui/src/app/ng2/pages/composition/palette/palette.component.html b/catalog-ui/src/app/ng2/pages/composition/palette/palette.component.html new file mode 100644 index 0000000000..7963dd18b7 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/palette/palette.component.html @@ -0,0 +1,41 @@ +<div class="composition-palette-component"> + <div class="palette-elements-count">Elements + <span class="palette-elements-count-value">{{numberOfElements}}</span> + </div> + + <sdc-filter-bar placeholder="Search..." (valueChange)="onSearchChanged($event)" testId="searchAsset"></sdc-filter-bar> + + <div class="palette-elements-list"> + <sdc-loader [global]="false" name="palette-loader" testId="palette-loader" [active]="this.isPaletteLoading" [class.inactive]="!this.isPaletteLoading"></sdc-loader> + <div *ngIf="numberOfElements === 0 && searchText" class="no-elements-found">No Elements Found</div> + <sdc-accordion *ngFor="let mapByCategory of paletteElements | keyValue; let first = first" [attr.data-tests-id]="'leftPalette.category.'+mapByCategory.key" [title]="mapByCategory.key" [css-class]="'palette-category'"> + <div *ngFor="let mapBySubCategory of mapByCategory.value | keyValue"> + <div class="palette-subcategory">{{mapBySubCategory.key}}</div> + <ng-container *ngIf="!(isViewOnly$ | async)"> + <div *ngFor="let paletteElement of mapBySubCategory.value" + [dndDraggable]="paletteElement" + [dndDisableIf]="paletteElement.componentSubType == 'GROUP' && paletteElement.componentSubType == 'POLICY'" + (dndStart)="onDragStart($event, paletteElement)" + (drag)="onDraggableMoved($event)" + [dndEffectAllowed]="'copyMove'" + (mouseenter)="onMouseOver($event, paletteElement)" + (mouseleave)="onMouseOut(paletteElement)" + [attr.data-tests-id]="paletteElement.name"> + <palette-element [paletteElement]="paletteElement"></palette-element> + </div> + </ng-container> + <ng-container *ngIf="(isViewOnly$ | async)"> + <div *ngFor="let paletteElement of mapBySubCategory.value" + [attr.data-tests-id]="paletteElement.name"> + <palette-element [paletteElement]="paletteElement"></palette-element> + </div> + </ng-container> + </div> + </sdc-accordion> + </div> +</div> + +<div id="draggable_element" dndDropzone (dndDrop)="onDrop($event)" [dndAllowExternal]="true"> + <sdc-element-icon *ngIf="paletteDraggedElement" [iconName]="paletteDraggedElement.icon" + [elementType]="paletteDraggedElement.componentSubType" [uncertified]="paletteDraggedElement.certifiedIconClass"></sdc-element-icon> +</div> diff --git a/catalog-ui/src/app/ng2/pages/composition/palette/palette.component.less b/catalog-ui/src/app/ng2/pages/composition/palette/palette.component.less new file mode 100644 index 0000000000..37461ba1c5 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/palette/palette.component.less @@ -0,0 +1,84 @@ +@import "./../../../../../assets/styles/override"; + +:host(composition-palette) { + display:flex; + flex: 0 0 244px; +} + +sdc-loader.inactive { + display:none; +} + +:host ::ng-deep .sdc-filter-bar .sdc-input { + margin-bottom:0px; +} +:host ::ng-deep .sdc-loader-wrapper { + position:static; +} + +.composition-palette-component { + background-color: @sdcui_color_white; + overflow-y: auto; + overflow-x: hidden; + display: flex; + flex-direction: column; + position:relative; + width: 244px; + box-shadow: 7px -3px 6px -8px @sdcui_color_gray; + + .palette-elements-count { + background-color: @sdcui_color_gray; + line-height: 40px; + padding: 0 17px; + color: @sdcui_color_white; + .palette-elements-count-value { + float: right; + } + } + + .palette-elements-list { + + .no-elements-found { + padding-left: 40px; + } + /deep/ .palette-category { + display: flex; + margin: 0px; + .sdc-accordion-header { + background-color: @sdcui_color_silver; + margin: 0px; + line-height: 40px; + padding: 0px 10px; + } + .sdc-accordion-body { + padding: 0px; + } + } + .palette-subcategory { + padding: 0 10px; + background-color: @sdcui_color_lighter-silver; + line-height: 35px; + } + } +} + +#draggable_element { + display: inline-block; + border-radius: 50%; + background: transparent; + position: absolute; + top: -9999px; + left: 0; + z-index: 100; +} + +.invalid-drag { + border: 7px solid @red-shadow; +} + +.valid-drag { + border: 7px solid @green-shadow; +} + +@green-shadow: rgba(29, 154, 149, 0.3); +@red-shadow: rgba(218, 31, 61, 0.3); diff --git a/catalog-ui/src/app/ng2/pages/composition/palette/palette.component.spec.ts b/catalog-ui/src/app/ng2/pages/composition/palette/palette.component.spec.ts new file mode 100644 index 0000000000..efa9cd3370 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/palette/palette.component.spec.ts @@ -0,0 +1,102 @@ +import {async, ComponentFixture, TestBed} from "@angular/core/testing"; +import {NO_ERRORS_SCHEMA} from "@angular/core"; +import {CompositionPaletteService} from "./services/palette.service"; +import {EventListenerService} from "../../../../services/event-listener-service"; +import {PaletteElementComponent} from "./palette-element/palette-element.component"; +import {PaletteComponent} from "./palette.component"; +import {ConfigureFn, configureTests} from "../../../../../jest/test-config.helper"; +import {GRAPH_EVENTS} from "../../../../utils/constants"; +import {KeyValuePipe} from "../../../pipes/key-value.pipe"; +import {ResourceNamePipe} from "../../../pipes/resource-name.pipe"; +import {LeftPaletteComponent} from "../../../../models/components/displayComponent"; +import {Observable} from "rxjs/Observable"; +import {leftPaletteElements} from "../../../../../jest/mocks/left-paeltte-elements.mock"; +import {NgxsModule, Select} from '@ngxs/store'; +import { WorkspaceState } from 'app/ng2/store/states/workspace.state'; + + +describe('palette component', () => { + + const mockedEvent = <MouseEvent>{ target: {} } + let fixture: ComponentFixture<PaletteComponent>; + let eventServiceMock: Partial<EventListenerService>; + let compositionPaletteMockService: Partial<CompositionPaletteService>; + + beforeEach( + async(() => { + eventServiceMock = { + notifyObservers: jest.fn() + } + compositionPaletteMockService = { + subscribeToLeftPaletteElements: jest.fn().mockImplementation(()=> Observable.of(leftPaletteElements)), + getLeftPaletteElements: jest.fn().mockImplementation(()=> leftPaletteElements) + } + const configure: ConfigureFn = testBed => { + testBed.configureTestingModule({ + declarations: [PaletteComponent, PaletteElementComponent, KeyValuePipe, ResourceNamePipe], + imports: [NgxsModule.forRoot([WorkspaceState])], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + {provide: CompositionPaletteService, useValue: compositionPaletteMockService}, + {provide: EventListenerService, useValue: eventServiceMock} + ], + }); + }; + + configureTests(configure).then(testBed => { + fixture = testBed.createComponent(PaletteComponent); + }); + }) + ); + + it('should match current snapshot of palette component', () => { + expect(fixture).toMatchSnapshot(); + }); + + it('should call on palette component hover in event', () => { + let paletteObject = <LeftPaletteComponent>{categoryType: 'COMPONENT'}; + fixture.componentInstance.onMouseOver(mockedEvent, paletteObject); + expect(eventServiceMock.notifyObservers).toHaveBeenCalledWith(GRAPH_EVENTS.ON_PALETTE_COMPONENT_HOVER_IN, paletteObject); + }); + + it('should call on palette component hover out event', () => { + let paletteObject = <LeftPaletteComponent>{categoryType: 'COMPONENT'}; + fixture.componentInstance.onMouseOut(paletteObject); + expect(eventServiceMock.notifyObservers).toHaveBeenCalledWith(GRAPH_EVENTS.ON_PALETTE_COMPONENT_HOVER_OUT); + }); + + it('should call show popup panel event', () => { + let paletteObject = <LeftPaletteComponent>{categoryType: 'GROUP'}; + fixture.componentInstance.onMouseOver(mockedEvent, paletteObject); + expect(eventServiceMock.notifyObservers).toHaveBeenCalledWith(GRAPH_EVENTS.ON_PALETTE_COMPONENT_SHOW_POPUP_PANEL, paletteObject, mockedEvent.target); + }); + + it('should call hide popup panel event', () => { + let paletteObject = <LeftPaletteComponent>{categoryType: 'GROUP'}; + fixture.componentInstance.onMouseOut(paletteObject); + expect(eventServiceMock.notifyObservers).toHaveBeenCalledWith(GRAPH_EVENTS.ON_PALETTE_COMPONENT_HIDE_POPUP_PANEL); + }); + + it('should build Palette By Categories without searchText', () => { + fixture.componentInstance.buildPaletteByCategories(); + expect(fixture.componentInstance.paletteElements["Generic"]["Network"].length).toBe(5); + expect(fixture.componentInstance.paletteElements["Generic"]["Network"][0].searchFilterTerms).toBe("extvirtualmachineinterfacecp external port for virtual machine interface extvirtualmachineinterfacecp 3.0"); + expect(fixture.componentInstance.paletteElements["Generic"]["Network"][1].searchFilterTerms).toBe("newservice2 asdfasdfa newservice2 0.3"); + + expect(fixture.componentInstance.paletteElements["Generic"]["Configuration"].length).toBe(1); + expect(fixture.componentInstance.paletteElements["Generic"]["Configuration"][0].systemName).toBe("Extvirtualmachineinterfacecp"); + }); + + it('should build Palette By Categories with searchText', () => { + fixture.componentInstance.buildPaletteByCategories("testVal"); + expect(fixture.componentInstance.paletteElements["Generic"]["Network"].length).toBe(1); + expect(fixture.componentInstance.paletteElements["Generic"]["Network"][0].searchFilterTerms).toBe("testVal and other values"); + }); + + it('should change numbers of elements', () => { + fixture.componentInstance.buildPaletteByCategories(); + expect(fixture.componentInstance.numberOfElements).toEqual(6); + fixture.componentInstance.buildPaletteByCategories("testVal"); + expect(fixture.componentInstance.numberOfElements).toEqual(1); + }); +});
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/composition/palette/palette.component.ts b/catalog-ui/src/app/ng2/pages/composition/palette/palette.component.ts new file mode 100644 index 0000000000..02d270b39a --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/palette/palette.component.ts @@ -0,0 +1,172 @@ +/** + * Created by ob0695 on 6/28/2018. + */ +/*- + * ============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========================================================= + */ + +import { Component, HostListener } from '@angular/core'; +import { Select } from '@ngxs/store'; +import { LeftPaletteComponent, LeftPaletteMetadataTypes } from 'app/models/components/displayComponent'; +import { Point } from 'app/models/graph/point'; +import { WorkspaceState } from 'app/ng2/store/states/workspace.state'; +import Dictionary = _.Dictionary; +import { EventListenerService } from 'app/services/event-listener-service'; +import { GRAPH_EVENTS } from 'app/utils/constants'; +import { DndDropEvent } from 'ngx-drag-drop/ngx-drag-drop'; +import { CompositionPaletteService } from './services/palette.service'; +import {PolicyMetadata} from "../../../../models/policy-metadata"; +import {GenericBrowserDomAdapter} from "@angular/platform-browser/src/browser/generic_browser_adapter"; + +@Component({ + selector: 'composition-palette', + templateUrl: './palette.component.html', + styleUrls: ['./palette.component.less'] +}) +export class PaletteComponent { + + constructor(private compositionPaletteService: CompositionPaletteService, private eventListenerService: EventListenerService) {} + + @Select(WorkspaceState.isViewOnly) isViewOnly$: boolean; + private paletteElements: Dictionary<Dictionary<LeftPaletteComponent[]>>; + public numberOfElements: number = 0; + public isPaletteLoading: boolean; + private paletteDraggedElement: LeftPaletteComponent; + public position: Point = new Point(); + + ngOnInit() { + this.isPaletteLoading = true; + + this.compositionPaletteService.subscribeToLeftPaletteElements((leftPaletteElementsResponse) => { + this.paletteElements = leftPaletteElementsResponse; + this.numberOfElements = this.countLeftPalleteElements(this.paletteElements); + this.isPaletteLoading = false; + }, () => { + this.isPaletteLoading = false; + }); + + } + + public buildPaletteByCategories = (searchText?: string) => { // create nested by category & subcategory, filtered by search parans + // Flat the object and run on its leaves + if (searchText) { + searchText = searchText.toLowerCase(); + const paletteElementsAfterSearch = {}; + this.paletteElements = this.compositionPaletteService.getLeftPaletteElements(); + for (const category in this.paletteElements) { + for (const subCategory in this.paletteElements[category]) { + const subCategoryToCheck = this.paletteElements[category][subCategory]; + const res = subCategoryToCheck.filter((item) => item.searchFilterTerms.toLowerCase().indexOf(searchText) >= 0) + if (res.length > 0) { + paletteElementsAfterSearch[category] = {}; + paletteElementsAfterSearch[category][subCategory] = res; + } + } + } + this.paletteElements = paletteElementsAfterSearch; + } else { + this.paletteElements = this.compositionPaletteService.getLeftPaletteElements(); + } + this.numberOfElements = this.countLeftPalleteElements(this.paletteElements); + } + + public onSearchChanged = (searchText: string) => { + + if (this.compositionPaletteService.getLeftPaletteElements()) { + this.buildPaletteByCategories(searchText); + } + } + + private countLeftPalleteElements(leftPalleteElements: Dictionary<Dictionary<LeftPaletteComponent[]>>) { + // Use _ & flat map + let counter = 0; + for (const category in leftPalleteElements) { + for (const subCategory in leftPalleteElements[category]) { + counter += leftPalleteElements[category][subCategory].length; + } + } + return counter; + } + + private isGroupOrPolicy(component: LeftPaletteComponent): boolean { + if (component && + (component.categoryType === LeftPaletteMetadataTypes.Group || + component.categoryType === LeftPaletteMetadataTypes.Policy)) { + return true; + } + return false; + } + @HostListener('document:dragover', ['$event']) + public onDrag(event) { + this.position.x = event.clientX; + this.position.y = event.clientY; + } + + //---------------------------------------Palette Events-----------------------------------------// + + public onDraggableMoved = (event:DragEvent) => { + let draggedElement = document.getElementById("draggable_element"); + draggedElement.style.top = (this.position.y - 80) + "px"; + draggedElement.style.left = (this.position.x - 30) + "px"; + this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_PALETTE_COMPONENT_DRAG_ACTION, this.position); + } + + public onDragStart = (event, draggedElement:LeftPaletteComponent) => { // Applying the dragged svg component to the draggable element + + this.paletteDraggedElement = draggedElement; + event.dataTransfer.dropEffect = "copy"; + let hiddenImg = document.createElement("span"); + event.dataTransfer.setDragImage(hiddenImg, 0, 0); + } + + + public onDrop = (event:DndDropEvent) => { + let draggedElement = document.getElementById("draggable_element"); + draggedElement.style.top = "-9999px"; + if(draggedElement.classList.contains('valid-drag')) { + if(!event.data){ + event.data = this.paletteDraggedElement; + } + this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_PALETTE_COMPONENT_DROP, event); + } else { + console.log("INVALID drop"); + } + this.paletteDraggedElement = undefined; + + } + + public onMouseOver = (sectionElem:MouseEvent, displayComponent:LeftPaletteComponent) => { + console.debug("On palette element MOUSE HOVER: ", displayComponent); + if (this.isGroupOrPolicy(displayComponent)) { + this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_PALETTE_COMPONENT_SHOW_POPUP_PANEL, displayComponent, sectionElem.target); + } else { + this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_PALETTE_COMPONENT_HOVER_IN, displayComponent); + } + }; + + public onMouseOut = (displayComponent:LeftPaletteComponent) => { + console.debug("On palette element MOUSE OUT: ", displayComponent); + if (this.isGroupOrPolicy(displayComponent)) { + this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_PALETTE_COMPONENT_HIDE_POPUP_PANEL); + } else { + this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_PALETTE_COMPONENT_HOVER_OUT); + } + }; + +} diff --git a/catalog-ui/src/app/ng2/pages/composition/palette/palette.module.ts b/catalog-ui/src/app/ng2/pages/composition/palette/palette.module.ts new file mode 100644 index 0000000000..aeb4c4c60b --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/palette/palette.module.ts @@ -0,0 +1,25 @@ +import { NgModule } from "@angular/core"; +import { CompositionPaletteService } from "./services/palette.service"; +import { PaletteComponent } from "./palette.component"; +import { SdcUiComponentsModule } from "onap-ui-angular"; +import { GlobalPipesModule } from "app/ng2/pipes/global-pipes.module"; +import { CommonModule } from "@angular/common"; +import { DndModule } from "ngx-drag-drop"; +import {PaletteElementComponent} from "./palette-element/palette-element.component"; +import {EventListenerService} from "app/services/event-listener-service"; +import {UiElementsModule} from "app/ng2/components/ui/ui-elements.module"; + +@NgModule({ + declarations: [PaletteComponent, PaletteElementComponent], + imports: [CommonModule, SdcUiComponentsModule, GlobalPipesModule, UiElementsModule, DndModule], + exports: [PaletteComponent], + entryComponents: [PaletteComponent], + providers: [CompositionPaletteService, EventListenerService] +}) +export class PaletteModule { + + constructor() { + + } + +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/composition/palette/services/palette.service.spec.ts b/catalog-ui/src/app/ng2/pages/composition/palette/services/palette.service.spec.ts new file mode 100644 index 0000000000..3a660c1de7 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/palette/services/palette.service.spec.ts @@ -0,0 +1,41 @@ +import {TestBed} from "@angular/core/testing"; +import {CompositionPaletteService} from "./palette.service"; +import {ISdcConfig, SdcConfigToken} from "../../../../config/sdc-config.config"; +import {WorkspaceService} from "../../../../pages/workspace/workspace.service"; +import { HttpClient } from "@angular/common/http"; +describe('palette component', () => { + + let service: CompositionPaletteService; + + let httpServiceMock: Partial<HttpClient> = { + get: jest.fn() + } + + let sdcConfigToken: Partial<ISdcConfig> = { + "api": { + "root": '' + } + } + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [], + providers: [CompositionPaletteService, + {provide: HttpClient, useValue: httpServiceMock}, + {provide: SdcConfigToken, useValue: sdcConfigToken}, + {provide: WorkspaceService, useValue{}} + ] + }); + + service = TestBed.get(CompositionPaletteService); + }); + + it('should create an instance', () => { + expect(service).toBeDefined(); + }); + + // it('should create an instance2', async () => { + // expect(await service.subscribeToLeftPaletteElements("resources")).toEqual([]); + // }); +}); + diff --git a/catalog-ui/src/app/ng2/pages/composition/palette/services/palette.service.ts b/catalog-ui/src/app/ng2/pages/composition/palette/services/palette.service.ts new file mode 100644 index 0000000000..7587c5206f --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/palette/services/palette.service.ts @@ -0,0 +1,98 @@ +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Inject, Injectable } from '@angular/core'; +import { LeftPaletteComponent, LeftPaletteMetadataTypes } from 'app/models/components/displayComponent'; +import { GroupMetadata } from 'app/models/group-metadata'; +import { PolicyMetadata } from 'app/models/policy-metadata'; +import { SdcConfigToken } from 'app/ng2/config/sdc-config.config'; +import { ISdcConfig } from 'app/ng2/config/sdc-config.config.factory'; +import { WorkspaceService } from 'app/ng2/pages/workspace/workspace.service'; +import 'rxjs/add/observable/forkJoin'; +import { Observable } from 'rxjs/Rx'; +import Dictionary = _.Dictionary; + + + +@Injectable() +export class CompositionPaletteService { + + protected baseUrl = ''; + + private leftPaletteComponents: Dictionary<Dictionary<LeftPaletteComponent[]>>; + private facadeUrl: string; + constructor(protected http: HttpClient, @Inject(SdcConfigToken) sdcConfig: ISdcConfig, private workspaceService: WorkspaceService) { + this.baseUrl = sdcConfig.api.root + sdcConfig.api.component_api_root; + this.facadeUrl = sdcConfig.api.uicache_root + sdcConfig.api.GET_uicache_left_palette; + + } + + public subscribeToLeftPaletteElements(next, error) { + + let params = new HttpParams(); + params = params.append('internalComponentType', this.workspaceService.getMetadataType()); + + const loadInstances = this.http.get(this.facadeUrl, {params}); + const loadGroups = this.http.get(this.baseUrl + 'groupTypes', {params}); + const loadPolicies = this.http.get(this.baseUrl + 'policyTypes', {params}); + + Observable.forkJoin( + loadInstances, loadGroups, loadPolicies + ).subscribe( ([resInstances, resGrouops, resPolicies]) => { + const combinedDictionary = this.combineResoponses(resInstances, resGrouops, resPolicies); + this.leftPaletteComponents = combinedDictionary; + next(this.leftPaletteComponents); + }); + } + + public getLeftPaletteElements = (): Dictionary<Dictionary<LeftPaletteComponent[]>> => { + return this.leftPaletteComponents; + } + + + public convertPoliciesOrGroups = (paletteListResult, type: string ) => { + const components: LeftPaletteComponent[] = []; + + if (type === 'Policies') { + _.forEach(paletteListResult, (policyMetadata: PolicyMetadata) => { + components.push(new LeftPaletteComponent(LeftPaletteMetadataTypes.Policy, policyMetadata)); + }); + return { + Policies: components + }; + } + + if (type === 'Groups') { + _.forEach(paletteListResult, (groupMetadata: GroupMetadata) => { + const item = new LeftPaletteComponent(LeftPaletteMetadataTypes.Group, groupMetadata); + components.push(item); + }); + return { + Groups: components + }; + } + + return {}; + } + + private combineResoponses(resInstances: object, resGrouops: object, resPolicies: object) { + const retValObject = {}; + // Generic will be the 1st category in the left Pallete + if (resInstances['Generic']) { + retValObject['Generic'] = resInstances['Generic']; + } + // Add all other categories + for (const category in resInstances) { + if (category === 'Generic') { + continue; + } + retValObject[category] = resInstances[category]; + } + + // Add Groups + retValObject["Groups"] = this.convertPoliciesOrGroups(resGrouops, 'Groups'); + + // Add policies + retValObject["Policies"] = this.convertPoliciesOrGroups(resPolicies, 'Policies'); + + return retValObject; + } +} diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/__snapshots__/composition-panel.component.spec.ts.snap b/catalog-ui/src/app/ng2/pages/composition/panel/__snapshots__/composition-panel.component.spec.ts.snap new file mode 100644 index 0000000000..5f10806315 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/panel/__snapshots__/composition-panel.component.spec.ts.snap @@ -0,0 +1,18 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`composition-panel component should match current snapshot of composition-panel component. 1`] = ` +<ng2-composition-panel + activatePreviousActiveTab={[Function Function]} + classes={[Function String]} + initTabs={[Function Function]} + isComponentInstanceSelected={[Function Function]} + isConfiguration={[Function Function]} + isPNF={[Function Function]} + selectedComponentIsServiceProxyInstance={[Function Function]} + setActive={[Function Function]} + store={[Function Store]} + toggleSidebarDisplay={[Function Function]} +> + +</ng2-composition-panel> +`; diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/composition-panel.component.html b/catalog-ui/src/app/ng2/pages/composition/panel/composition-panel.component.html new file mode 100644 index 0000000000..bd90b9a814 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/panel/composition-panel.component.html @@ -0,0 +1,21 @@ +<panel-wrapper-component *ngIf="compositionState$ | async as state"> <!-- HEADER --> + + <ng2-composition-panel-header [isViewOnly]="state.isViewOnly" + [selectedComponent]="state.selectedComponent"></ng2-composition-panel-header> + + <!-- TABS --> + <div class="component-details-panel-tabs"> + <sdc-loader [global]="false" name="panel" testId="panel-loader" [active]="state.panelLoading"></sdc-loader> + <sdc-tabs (selectedTab)="setActive($event)" [iconsSize]="'large'"> + <sdc-tab *ngFor="let tab of tabs" [titleIcon]="tab.titleIcon" [active]="tab.isActive" + [tooltipText]="tab.tooltipText"> + <panel-tab [isActive]="tab.isActive" [component]="selectedComponent" + [componentType]="state.selectedComponentType" [isViewOnly]="isViewOnly$ | async" + [input]="tab.input" [panelTabType]="tab.component"></panel-tab> + </sdc-tab> + </sdc-tabs> + </div> + +</panel-wrapper-component> + + diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/composition-panel.component.less b/catalog-ui/src/app/ng2/pages/composition/panel/composition-panel.component.less new file mode 100644 index 0000000000..776ef68944 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/panel/composition-panel.component.less @@ -0,0 +1,27 @@ +@import '../../../../../assets/styles/variables'; +@import '../../../../../assets/styles/mixins_old'; + +:host ::ng-deep .sdc-loader-wrapper { + position:static; +} + +.component-details-panel-tabs { + flex: 1; + display:flex; + overflow:hidden; + } + +.component-details-panel-tabs /deep/ sdc-tabs { + display:flex; + flex-direction:column; + + /deep/ sdc-tab { + display: flex; + flex-direction: column; + overflow-y: auto; + } + .svg-icon-wrapper.label-placement-left .svg-icon-label { + margin-right: 0; + } +} + diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/composition-panel.component.spec.ts b/catalog-ui/src/app/ng2/pages/composition/panel/composition-panel.component.spec.ts new file mode 100644 index 0000000000..25a0c728a8 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/panel/composition-panel.component.spec.ts @@ -0,0 +1,228 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { async, ComponentFixture } from '@angular/core/testing'; +import { NgxsModule, Store } from '@ngxs/store'; +import { Observable } from 'rxjs'; +import { Mock } from 'ts-mockery'; +import { ConfigureFn, configureTests } from '../../../../../jest/test-config.helper'; +import { Service } from '../../../../models/components/service'; +import { Resource } from '../../../../models/components/resource'; +import { GroupInstance } from '../../../../models/graph/zones/group-instance'; +import { PolicyInstance } from '../../../../models/graph/zones/policy-instance'; +import { ArtifactGroupType, ResourceType } from '../../../../utils/constants'; +import { WorkspaceState } from '../../../store/states/workspace.state'; +import { CompositionPanelComponent } from './composition-panel.component'; +import { ArtifactsTabComponent } from './panel-tabs/artifacts-tab/artifacts-tab.component'; +import { GroupMembersTabComponent } from './panel-tabs/group-members-tab/group-members-tab.component'; +import { GroupOrPolicyPropertiesTab } from './panel-tabs/group-or-policy-properties-tab/group-or-policy-properties-tab.component'; +import { InfoTabComponent } from './panel-tabs/info-tab/info-tab.component'; +import { PolicyTargetsTabComponent } from './panel-tabs/policy-targets-tab/policy-targets-tab.component'; +import { PropertiesTabComponent } from './panel-tabs/properties-tab/properties-tab.component'; +import { ReqAndCapabilitiesTabComponent } from './panel-tabs/req-capabilities-tab/req-capabilities-tab.component'; + +describe('composition-panel component', () => { + + let fixture: ComponentFixture<CompositionPanelComponent>; + let store: Store; + + const tabs = { + infoTab : {titleIcon: 'info-circle', component: InfoTabComponent, input: {}, isActive: true, tooltipText: 'Information'}, + policyProperties: { + titleIcon: 'settings-o', component: GroupOrPolicyPropertiesTab, input: {type: 'policy'}, isActive: false, tooltipText: 'Properties' + }, + policyTargets: {titleIcon: 'inputs-o', component: PolicyTargetsTabComponent, input: {}, isActive: false, tooltipText: 'Targets'}, + groupMembers: {titleIcon: 'inputs-o', component: GroupMembersTabComponent, input: {}, isActive: false, tooltipText: 'Members'}, + groupProperties: { + titleIcon: 'settings-o', component: GroupOrPolicyPropertiesTab, input: {type: 'group'}, isActive: false, tooltipText: 'Properties' + }, + deploymentArtifacts: { + titleIcon: 'deployment-artifacts-o', component: ArtifactsTabComponent, + input: { type: ArtifactGroupType.DEPLOYMENT}, isActive: false, tooltipText: 'Deployment Artifacts' + }, + apiArtifacts: { + titleIcon: 'api-o', component: ArtifactsTabComponent, + input: { type: ArtifactGroupType.SERVICE_API}, isActive: false, tooltipText: 'API Artifacts' + }, + infoArtifacts: { + titleIcon: 'info-square-o', component: ArtifactsTabComponent, + input: { type: ArtifactGroupType.INFORMATION}, isActive: false, tooltipText: 'Information Artifacts' + }, + properties: { + titleIcon: 'settings-o', component: PropertiesTabComponent, + input: {title: 'Properties and Attributes'}, isActive: false, tooltipText: 'Properties' + }, + reqAndCapabilities : { + titleIcon: 'req-capabilities-o', component: ReqAndCapabilitiesTabComponent, input: {}, + isActive: false, tooltipText: 'Requirements and Capabilities' + }, + inputs: {titleIcon: 'inputs-o', component: PropertiesTabComponent, input: {title: 'Inputs'}, isActive: false, tooltipText: 'Inputs'}, + settings: {titleIcon: 'settings-o', component: PropertiesTabComponent, input: {}, isActive: false, tooltipText: 'Settings'}, + }; + + beforeEach( + async(() => { + + const configure: ConfigureFn = (testBed) => { + testBed.configureTestingModule({ + declarations: [CompositionPanelComponent], + imports: [NgxsModule.forRoot([WorkspaceState])], + schemas: [NO_ERRORS_SCHEMA], + providers: [], + }); + }; + + configureTests(configure).then((testBed) => { + fixture = testBed.createComponent(CompositionPanelComponent); + store = testBed.get(Store); + }); + }) + ); + + it('When PolicyInstance Selected => Expect (info, policyTargets and policyProperties) tabs appear', () => { + + const testInstance = new PolicyInstance(); + + fixture.componentInstance.initTabs(testInstance); + + expect (fixture.componentInstance.tabs.length).toBe(3); + expect (fixture.componentInstance.tabs[0]).toEqual(tabs.infoTab); + expect (fixture.componentInstance.tabs[1]).toEqual(tabs.policyTargets); + expect (fixture.componentInstance.tabs[2]).toEqual(tabs.policyProperties); + }); + + it('should match current snapshot of composition-panel component.', () => { + expect(fixture).toMatchSnapshot(); + }); + + it('When Topology Template is Service and no instance is selected Expect (info, deployment, inputs, info and api)', () => { + + const selectedComponent: Service = new Service(null, null); + selectedComponent.isResource = jest.fn(() => false); + selectedComponent.isService = jest.fn(() => true ); + + fixture.componentInstance.store.select = jest.fn(() => Observable.of(selectedComponent)); + + // const pnfMock = Mock.of<Service>({ isResource : () => false }); + fixture.componentInstance.topologyTemplate = selectedComponent; + + // Call ngOnInit + fixture.componentInstance.ngOnInit(); + + // Expect that + expect (fixture.componentInstance.tabs.length).toBe(5); + expect (fixture.componentInstance.tabs[0]).toEqual(tabs.infoTab); + expect (fixture.componentInstance.tabs[1]).toEqual(tabs.deploymentArtifacts); + expect (fixture.componentInstance.tabs[2]).toEqual(tabs.inputs); + expect (fixture.componentInstance.tabs[3]).toEqual(tabs.infoArtifacts); + expect (fixture.componentInstance.tabs[4]).toEqual(tabs.apiArtifacts); + + }); + + it('When Topology Template is Resource and no instance is selected Expect (info, deployment, inputs, info and api)', () => { + + const selectedComponent: Service = new Service(null, null); + selectedComponent.isResource = jest.fn(() => true); + selectedComponent.isService = jest.fn(() => false ); + + fixture.componentInstance.store.select = jest.fn(() => Observable.of(selectedComponent)); + + fixture.componentInstance.topologyTemplate = selectedComponent; + + // Call ngOnInit + fixture.componentInstance.ngOnInit(); + + // Expect that + expect (fixture.componentInstance.tabs.length).toBe(5); + expect (fixture.componentInstance.tabs[0]).toEqual(tabs.infoTab); + expect (fixture.componentInstance.tabs[1]).toEqual(tabs.deploymentArtifacts); + expect (fixture.componentInstance.tabs[2]).toEqual(tabs.properties); + expect (fixture.componentInstance.tabs[3]).toEqual(tabs.infoArtifacts); + expect (fixture.componentInstance.tabs[4]).toEqual(tabs.reqAndCapabilities); + + }); + + it('When Topology Template is Service and proxyService instance is selected ' + + 'Expect (info, deployment, inputs, info and api)', () => { + + const selectedComponent: Service = new Service(null, null); + selectedComponent.isResource = jest.fn(() => false); + selectedComponent.isService = jest.fn(() => true ); + + fixture.componentInstance.store.select = jest.fn(() => Observable.of(selectedComponent)); + fixture.componentInstance.selectedComponentIsServiceProxyInstance = jest.fn(() => true); + + // const pnfMock = Mock.of<Service>({ isResource : () => false }); + fixture.componentInstance.topologyTemplate = selectedComponent; + + // Call ngOnInit + fixture.componentInstance.ngOnInit(); + + // Expect that + expect (fixture.componentInstance.tabs.length).toBe(5); + expect (fixture.componentInstance.tabs[0]).toEqual(tabs.infoTab); + expect (fixture.componentInstance.tabs[1]).toEqual(tabs.properties); + expect (fixture.componentInstance.tabs[2]).toEqual(tabs.reqAndCapabilities); + + }); + + it('When Topology Template is Resource and VL is selected ' + + 'Expect (info, deployment, inputs, info and api)', () => { + + const topologyTemplate: Resource = new Resource(null, null); + topologyTemplate.isResource = jest.fn(() => true); + topologyTemplate.isService = jest.fn(() => false ); + + const vlMock = Mock.of<Resource>({ resourceType : 'VL', isResource : () => true, isService : () => false }); + fixture.componentInstance.store.select = jest.fn(() => Observable.of(vlMock)); + + fixture.componentInstance.topologyTemplate = topologyTemplate; + + // Call ngOnInit + fixture.componentInstance.ngOnInit(); + + // Expect that + expect (fixture.componentInstance.tabs.length).toBe(5); + expect (fixture.componentInstance.tabs[0]).toEqual(tabs.infoTab); + expect (fixture.componentInstance.tabs[1]).toEqual(tabs.deploymentArtifacts); + expect (fixture.componentInstance.tabs[2]).toEqual(tabs.properties); + expect (fixture.componentInstance.tabs[3]).toEqual(tabs.infoArtifacts); + expect (fixture.componentInstance.tabs[4]).toEqual(tabs.reqAndCapabilities); + + }); + + it('When Topology Template is Service and VL is selected ' + + 'Expect (info, deployment, inputs, info and api)', () => { + + const topologyTemplate: Service = new Service(null, null); + topologyTemplate.isResource = jest.fn(() => true); + topologyTemplate.isService = jest.fn(() => false ); + + const vlMock = Mock.of<Resource>({ resourceType : 'VL', isResource : () => true, isService : () => false }); + fixture.componentInstance.store.select = jest.fn(() => Observable.of(vlMock)); + + fixture.componentInstance.topologyTemplate = topologyTemplate; + + // Call ngOnInit + fixture.componentInstance.ngOnInit(); + + // Expect that + expect (fixture.componentInstance.tabs.length).toBe(5); + expect (fixture.componentInstance.tabs[0]).toEqual(tabs.infoTab); + expect (fixture.componentInstance.tabs[1]).toEqual(tabs.deploymentArtifacts); + expect (fixture.componentInstance.tabs[2]).toEqual(tabs.properties); + expect (fixture.componentInstance.tabs[3]).toEqual(tabs.infoArtifacts); + expect (fixture.componentInstance.tabs[4]).toEqual(tabs.reqAndCapabilities); + + }); + + it('When GroupInstance Selected => Expect (info, groupMembers and groupProperties) tabs appear.', () => { + + const testInstance = new GroupInstance(); + fixture.componentInstance.initTabs(testInstance); + + expect (fixture.componentInstance.tabs.length).toBe(3); + expect (fixture.componentInstance.tabs[0]).toEqual(tabs.infoTab); + expect (fixture.componentInstance.tabs[1]).toEqual(tabs.groupMembers); + expect (fixture.componentInstance.tabs[2]).toEqual(tabs.groupProperties); + }); + +}); diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/composition-panel.component.ts b/catalog-ui/src/app/ng2/pages/composition/panel/composition-panel.component.ts new file mode 100644 index 0000000000..c5ea41bcd1 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/panel/composition-panel.component.ts @@ -0,0 +1,171 @@ +/*- + * ============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========================================================= + */ + +import { Component, HostBinding, Input } from '@angular/core'; +import { Select, Store } from '@ngxs/store'; +import { Component as TopologyTemplate, ComponentInstance, FullComponentInstance, GroupInstance, PolicyInstance, Resource, Service } from 'app/models'; +import { ArtifactsTabComponent } from 'app/ng2/pages/composition/panel/panel-tabs/artifacts-tab/artifacts-tab.component'; +import { GroupMembersTabComponent } from 'app/ng2/pages/composition/panel/panel-tabs/group-members-tab/group-members-tab.component'; +import { GroupOrPolicyPropertiesTab } from 'app/ng2/pages/composition/panel/panel-tabs/group-or-policy-properties-tab/group-or-policy-properties-tab.component'; +import { InfoTabComponent } from 'app/ng2/pages/composition/panel/panel-tabs/info-tab/info-tab.component'; +import { PolicyTargetsTabComponent } from 'app/ng2/pages/composition/panel/panel-tabs/policy-targets-tab/policy-targets-tab.component'; +import { PropertiesTabComponent } from 'app/ng2/pages/composition/panel/panel-tabs/properties-tab/properties-tab.component'; +import { ReqAndCapabilitiesTabComponent } from 'app/ng2/pages/composition/panel/panel-tabs/req-capabilities-tab/req-capabilities-tab.component'; +import { ComponentType, ResourceType } from 'app/utils'; +import * as _ from 'lodash'; +import { Subscription } from 'rxjs'; +import { Observable } from 'rxjs/Observable'; +import { ArtifactGroupType, COMPONENT_FIELDS } from '../../../../utils/constants'; +import { WorkspaceState } from '../../../store/states/workspace.state'; +import { OnSidebarOpenOrCloseAction } from '../common/store/graph.actions'; +import { CompositionStateModel, GraphState } from '../common/store/graph.state'; +import { ServiceConsumptionTabComponent } from './panel-tabs/service-consumption-tab/service-consumption-tab.component'; +import { ServiceDependenciesTabComponent } from './panel-tabs/service-dependencies-tab/service-dependencies-tab.component'; + +const tabs = { + infoTab : {titleIcon: 'info-circle', component: InfoTabComponent, input: {}, isActive: true, tooltipText: 'Information'}, + policyProperties: {titleIcon: 'settings-o', component: GroupOrPolicyPropertiesTab, input: {type: 'policy'}, isActive: false, tooltipText: 'Properties'}, + policyTargets: {titleIcon: 'inputs-o', component: PolicyTargetsTabComponent, input: {}, isActive: false, tooltipText: 'Targets'}, + groupMembers: {titleIcon: 'inputs-o', component: GroupMembersTabComponent, input: {}, isActive: false, tooltipText: 'Members'}, + groupProperties: {titleIcon: 'settings-o', component: GroupOrPolicyPropertiesTab, input: {type: 'group'}, isActive: false, tooltipText: 'Properties'}, + deploymentArtifacts: {titleIcon: 'deployment-artifacts-o', component: ArtifactsTabComponent, input: { type: ArtifactGroupType.DEPLOYMENT}, isActive: false, tooltipText: 'Deployment Artifacts'}, + apiArtifacts: {titleIcon: 'api-o', component: ArtifactsTabComponent, input: { type: ArtifactGroupType.SERVICE_API}, isActive: false, tooltipText: 'API Artifacts'}, + infoArtifacts: {titleIcon: 'info-square-o', component: ArtifactsTabComponent, input: { type: ArtifactGroupType.INFORMATION}, isActive: false, tooltipText: 'Information Artifacts'}, + properties: {titleIcon: 'settings-o', component: PropertiesTabComponent, input: {title: 'Properties and Attributes'}, isActive: false, tooltipText: 'Properties'}, + reqAndCapabilities : { titleIcon: 'req-capabilities-o', component: ReqAndCapabilitiesTabComponent, input: {}, isActive: false, tooltipText: 'Requirements and Capabilities'}, + inputs: {titleIcon: 'inputs-o', component: PropertiesTabComponent, input: {title: 'Inputs'}, isActive: false, tooltipText: 'Inputs'}, + settings: {titleIcon: 'settings-o', component: PropertiesTabComponent, input: {}, isActive: false, tooltipText: 'Settings'}, + consumption: {titleIcon: 'api-o', component: ServiceConsumptionTabComponent, input: {title: 'OPERATION CONSUMPTION'}, isActive: false, tooltipText: 'Service Consumption'}, + dependencies: {titleIcon: 'archive', component: ServiceDependenciesTabComponent, input: {title: 'SERVICE DEPENDENCIES'}, isActive: false, tooltipText: 'Service Dependencies'} +}; + +@Component({ + selector: 'ng2-composition-panel', + templateUrl: './composition-panel.component.html', + styleUrls: ['./composition-panel.component.less', './panel-tabs/panel-tabs.less'], +}) +export class CompositionPanelComponent { + + @Input() topologyTemplate: TopologyTemplate; + @HostBinding('class') classes = 'component-details-panel'; + @Select(GraphState) compositionState$: Observable<CompositionStateModel>; + @Select(GraphState.withSidebar) withSidebar$: boolean; + @Select(WorkspaceState.isViewOnly) isViewOnly$: boolean; + tabs: any[]; + subscription: Subscription; + + private selectedComponent; + + constructor(public store: Store) { + } + + ngOnInit() { + this.subscription = this.store.select(GraphState.getSelectedComponent).subscribe((component) => { + this.selectedComponent = component; + this.initTabs(component); + this.activatePreviousActiveTab(); + }); + } + + ngOnDestroy() { + if (this.subscription) { + this.subscription.unsubscribe(); + } + } + + public setActive = (tabToSelect) => { + this.tabs.map((tab) => tab.isActive = (tab.titleIcon === tabToSelect.titleIcon) ? true : false); + } + + public activatePreviousActiveTab = () => { // sets the info tab to active if no other tab selected + + this.setActive(this.tabs.find((tab) => tab.isActive) || tabs.infoTab); + + } + + private initTabs = (component) => { + this.tabs = []; + + // Information + this.tabs.push(tabs.infoTab); + + if (component instanceof PolicyInstance) { + this.tabs.push(tabs.policyTargets); + this.tabs.push(tabs.policyProperties); + return; + } + + if (component instanceof GroupInstance) { + this.tabs.push(tabs.groupMembers); + this.tabs.push(tabs.groupProperties); + return; + } + + // Deployment artifacts + if (!this.isPNF() && !this.isConfiguration() && !this.selectedComponentIsServiceProxyInstance()) { + this.tabs.push(tabs.deploymentArtifacts); + } + + // Properties or Inputs + if (component.isResource() || this.selectedComponentIsServiceProxyInstance()) { + this.tabs.push(tabs.properties); + } else { + this.tabs.push(tabs.inputs); + } + + if (!this.isConfiguration() && !this.selectedComponentIsServiceProxyInstance()) { + this.tabs.push(tabs.infoArtifacts); + } + + if (!(component.isService()) || this.selectedComponentIsServiceProxyInstance()) { + this.tabs.push(tabs.reqAndCapabilities); + } + + if (component.isService() && !this.selectedComponentIsServiceProxyInstance()) { + this.tabs.push(tabs.apiArtifacts); + } + if (component.isService() && this.selectedComponentIsServiceProxyInstance()) { + this.tabs.push(tabs.consumption); + this.tabs.push(tabs.dependencies); + } + + } + + private toggleSidebarDisplay = () => { + // this.withSidebar = !this.withSidebar; + this.store.dispatch(new OnSidebarOpenOrCloseAction()); + } + + private isPNF = (): boolean => { + return this.topologyTemplate.isResource() && (this.topologyTemplate as Resource).resourceType === ResourceType.PNF; + } + + private isConfiguration = (): boolean => { + return this.topologyTemplate.isResource() && (this.topologyTemplate as Resource).resourceType === ResourceType.CONFIGURATION; + } + + private isComponentInstanceSelected = (): boolean => { + return this.selectedComponent instanceof FullComponentInstance; + } + + private selectedComponentIsServiceProxyInstance = (): boolean => { + return this.isComponentInstanceSelected() && this.selectedComponent.isServiceProxy(); + } +} diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/composition-panel.module.ts b/catalog-ui/src/app/ng2/pages/composition/panel/composition-panel.module.ts new file mode 100644 index 0000000000..0fd1e51fa5 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/panel/composition-panel.module.ts @@ -0,0 +1,106 @@ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ +import { NgModule } from "@angular/core"; +import { FormsModule } from "@angular/forms"; +import { BrowserModule } from "@angular/platform-browser"; +import { CompositionPanelComponent } from "./composition-panel.component"; +import { CompositionPanelHeaderModule } from "app/ng2/pages/composition/panel/panel-header/panel-header.module"; +import { SdcUiComponentsModule, SdcUiServices } from "onap-ui-angular"; +// import { SdcUiServices } from "onap-ui-angular/"; +import { UiElementsModule } from 'app/ng2/components/ui/ui-elements.module'; +import { AddElementsModule } from "../../../components/ui/modal/add-elements/add-elements.module"; +import { TranslateModule } from "app/ng2/shared/translator/translate.module"; +import { InfoTabComponent } from './panel-tabs/info-tab/info-tab.component'; +import { PanelTabComponent } from "app/ng2/pages/composition/panel/panel-tabs/panel-tab.component"; +import { ArtifactsTabComponent } from "app/ng2/pages/composition/panel/panel-tabs/artifacts-tab/artifacts-tab.component"; +import { PropertiesTabComponent } from "app/ng2/pages/composition/panel/panel-tabs/properties-tab/properties-tab.component"; +import { ReqAndCapabilitiesTabComponent } from "app/ng2/pages/composition/panel/panel-tabs/req-capabilities-tab/req-capabilities-tab.component"; +import { RequirementListComponent } from "app/ng2/pages/composition/panel/panel-tabs/req-capabilities-tab/requirement-list/requirement-list.component"; +import { PolicyTargetsTabComponent } from "app/ng2/pages/composition/panel/panel-tabs/policy-targets-tab/policy-targets-tab.component"; +import { GroupMembersTabComponent } from "app/ng2/pages/composition/panel/panel-tabs/group-members-tab/group-members-tab.component"; +import { GroupOrPolicyPropertiesTab } from "app/ng2/pages/composition/panel/panel-tabs/group-or-policy-properties-tab/group-or-policy-properties-tab.component"; +import { GlobalPipesModule } from "app/ng2/pipes/global-pipes.module"; +import {ModalModule} from "../../../components/ui/modal/modal.module"; +import {EnvParamsComponent} from "../../../components/forms/env-params/env-params.component"; +import {ModalsModule} from "../../../components/modals/modals.module"; +// import {EnvParamsModule} from "../../../components/forms/env-params/env-params.module"; +import { NgxDatatableModule } from "@swimlane/ngx-datatable"; +import {EnvParamsModule} from "../../../components/forms/env-params/env-params.module"; +import { ServiceConsumptionTabComponent } from "./panel-tabs/service-consumption-tab/service-consumption-tab.component"; +import { ServiceDependenciesTabComponent } from "./panel-tabs/service-dependencies-tab/service-dependencies-tab.component"; +import { ServiceDependenciesModule } from "../../../components/logic/service-dependencies/service-dependencies.module"; +import { ServiceConsumptionModule } from "../../../components/logic/service-consumption/service-consumption.module"; + + + +@NgModule({ + declarations: [ + CompositionPanelComponent, + PolicyTargetsTabComponent, + GroupOrPolicyPropertiesTab, + GroupMembersTabComponent, + InfoTabComponent, + PanelTabComponent, + ArtifactsTabComponent, + PropertiesTabComponent, + ReqAndCapabilitiesTabComponent, + ServiceConsumptionTabComponent, + ServiceDependenciesTabComponent, + RequirementListComponent, + EnvParamsComponent + ], + imports: [ + GlobalPipesModule, + BrowserModule, + FormsModule, + CompositionPanelHeaderModule, + SdcUiComponentsModule, + UiElementsModule, + AddElementsModule, + TranslateModule, + NgxDatatableModule, + ServiceDependenciesModule, + ServiceConsumptionModule + // EnvParamsModule + ], + entryComponents: [ + CompositionPanelComponent, + PolicyTargetsTabComponent, + GroupOrPolicyPropertiesTab, + GroupMembersTabComponent, + InfoTabComponent, + ArtifactsTabComponent, + PropertiesTabComponent, + ReqAndCapabilitiesTabComponent, + ServiceConsumptionTabComponent, + ServiceDependenciesTabComponent, + RequirementListComponent, + PanelTabComponent, + EnvParamsComponent + ], + exports: [ + CompositionPanelComponent + // EnvParamsModule + ], + providers: [SdcUiServices.ModalService] +}) +export class CompositionPanelModule { + +} diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/edit-name-modal/edit-name-modal.component.html b/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/edit-name-modal/edit-name-modal.component.html new file mode 100644 index 0000000000..75ee2d520f --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/edit-name-modal/edit-name-modal.component.html @@ -0,0 +1,28 @@ +<!-- + ~ Copyright (C) 2018 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. + --> + +<div class="name-update-container"> + <sdc-input #updateNameInput + label="Instance Name" + required="true" + [maxLength]="50" + [(value)]="name" + testId="instanceName"></sdc-input> + <sdc-validation [validateElement]="updateNameInput" (validityChanged)="validityChanged($event)"> + <sdc-required-validator message="Name is required."></sdc-required-validator> + <sdc-regex-validator message="Special characters not allowed." [pattern]="pattern"></sdc-regex-validator> + </sdc-validation> +</div>
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/edit-name-modal/edit-name-modal.component.less b/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/edit-name-modal/edit-name-modal.component.less new file mode 100644 index 0000000000..b958ca17b7 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/edit-name-modal/edit-name-modal.component.less @@ -0,0 +1,3 @@ +.name-update-container { + min-height: 90px; +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/edit-name-modal/edit-name-modal.component.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/edit-name-modal/edit-name-modal.component.ts new file mode 100644 index 0000000000..9c4aab206e --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/edit-name-modal/edit-name-modal.component.ts @@ -0,0 +1,25 @@ +import { Component, Input } from "@angular/core"; + +@Component({ + selector: 'edit-name-modal', + templateUrl: './edit-name-modal.component.html', + styleUrls: ['./edit-name-modal.component.less'] +}) +export class EditNameModalComponent { + + @Input() name:String; + @Input() validityChangedCallback: Function; + + private pattern:string = "^[\\s\\w\&_.:-]{1,1024}$" + constructor(){ + } + + private validityChanged = (value):void => { + if(this.validityChangedCallback) { + this.validityChangedCallback(value); + } + } + + + +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/panel-header.component.html b/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/panel-header.component.html index 67c82389cc..d9c56198ea 100644 --- a/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/panel-header.component.html +++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/panel-header.component.html @@ -1,30 +1,23 @@ -<!-- - ~ Copyright (C) 2018 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. - --> - -<div class="component-details-panel-header" data-tests-id="w-sdc-designer-sidebar-head"> - +<div *ngIf="selectedComponent" class="component-details-panel-header" data-tests-id="w-sdc-designer-sidebar-head"> <div class="icon"> - <div class="large {{iconClassName}}"> - <div [ngClass]="{'non-certified': nonCertified}" tooltip="Not certified"></div> - </div> + <div *ngIf="iconClassName; else svgIcon" class="large {{iconClassName}}"></div> + <ng-template #svgIcon> + <sdc-element-icon + [elementType]="selectedComponent.componentType === 'RESOURCE' ? selectedComponent.resourceType: (selectedComponent.originType || selectedComponent.componentType)" + [iconName]="selectedComponent.icon" + [uncertified]="!isTopologyTemplateSelected && selectedComponent.lifecycleState && 'CERTIFIED' !== selectedComponent.lifecycleState"></sdc-element-icon> + </ng-template> </div> - <div class="title" data-tests-id="selectedCompTitle" tooltip="​{{name}}">{{name}}</div> + <div class="title" data-tests-id="selectedCompTitle" tooltip="​{{selectedComponent.name}}"> + {{selectedComponent.name}} + </div> + + <svg-icon-label *ngIf="!isViewOnly && !isTopologyTemplateSelected && !selectedComponent.archived" name="edit-file-o" + clickable="true" size="small" class="rename-instance" data-tests-id="renameInstance" + (click)="renameInstance()"></svg-icon-label> + <svg-icon-label *ngIf="!isViewOnly && !isTopologyTemplateSelected && !selectedComponent.archived" name="trash-o" + clickable="true" size="small" class="delete-instance" data-tests-id="deleteInstance" + (click)="deleteInstance()"></svg-icon-label> - <svg-icon-label *ngIf="!isViewOnly" name="edit-file-o" clickable="true" size="small" class="rename-instance" data-tests-id="renameInstance" (click)="renameInstance()"></svg-icon-label> - <svg-icon-label *ngIf="!isViewOnly" name="trash-o" clickable="true" size="small" class="delete-instance" data-tests-id="deleteInstance" (click)="deleteInstance()"></svg-icon-label> - </div>
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/panel-header.component.less b/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/panel-header.component.less index 9bbc765761..6685f74009 100644 --- a/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/panel-header.component.less +++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/panel-header.component.less @@ -7,6 +7,7 @@ .icon { margin: 0 20px; + display:flex; } .title { @@ -31,4 +32,17 @@ cursor: pointer; } + + .non-certified { + position: absolute; + background-image: url('../../../../../../assets/styles/images/sprites/sprite-global-old.png'); + background-position: -157px -3386px; width: 15px; height: 15px; + + &.smaller-icon { + left: 35px; + bottom: -14px; + } + } + + }
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/panel-header.component.spec.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/panel-header.component.spec.ts new file mode 100644 index 0000000000..76e84a2323 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/panel-header.component.spec.ts @@ -0,0 +1,123 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed, async } from '@angular/core/testing'; +import { CompositionService } from 'app/ng2/pages/composition/composition.service'; +import { EventListenerService } from '../../../../../services/event-listener-service'; +import { ComponentInstanceServiceNg2 } from 'app/ng2/services/component-instance-services/component-instance.service'; +import { WorkspaceService } from 'app/ng2/pages/workspace/workspace.service'; +import { GroupsService } from 'app/services-ng2'; +import { PoliciesService } from 'app/services-ng2'; +import { CompositionPanelHeaderComponent } from './panel-header.component'; +import {SdcUiServices} from 'onap-ui-angular'; +import { Capability, Requirement, RequirementsGroup, CapabilitiesGroup, ComponentInstance, Component, FullComponentInstance, PolicyInstance, GroupInstance } from "app/models"; +import { of, Observable } from "rxjs"; + +describe('CompositionPanelHeaderComponent', () => { + let component: CompositionPanelHeaderComponent; + let fixture: ComponentFixture<CompositionPanelHeaderComponent>; + const componentInstanceServiceNg2Stub = { + updateComponentInstance: jest.fn() + }; + const valueEditModalInstance = { + innerModalContent : { + instance: { name : "VF Test" } + }, + buttons: [{id: 'saveButton', text: 'OK', size: 'xsm', callback: jest.fn(), closeModal: false}], + closeModal : jest.fn() + }; + + beforeEach( + () => { + const compositionServiceStub = {}; + const eventListenerServiceStub = {}; + + const workspaceServiceStub = { + metadata: { + componentType: "SERVICE", + uniqueId: "123" + } + }; + const groupsServiceStub = { + updateName: jest.fn() + }; + const policiesServiceStub = { + updateName: jest.fn() + }; + + TestBed.configureTestingModule({ + schemas: [NO_ERRORS_SCHEMA], + declarations: [CompositionPanelHeaderComponent], + providers: [ + { provide: CompositionService, useValue: compositionServiceStub }, + { provide: EventListenerService, useValue: eventListenerServiceStub }, + { + provide: ComponentInstanceServiceNg2, + useValue: componentInstanceServiceNg2Stub + }, + { provide: WorkspaceService, useValue: workspaceServiceStub }, + { provide: GroupsService, useValue: groupsServiceStub }, + { provide: PoliciesService, useValue: policiesServiceStub }, + { provide: SdcUiServices.ModalService, useValue: {}} + ] + }); + fixture = TestBed.createComponent(CompositionPanelHeaderComponent); + component = fixture.componentInstance; + } + ); + + it('can load instance', () => { + expect(component).toBeTruthy(); + }); + + it('should close the modal without saving if the name has not changed', () => { + component.selectedComponent = <FullComponentInstance>{name: "VF Test"}; + component.valueEditModalInstance = valueEditModalInstance; + + component.saveInstanceName(); + expect(component.componentInstanceService.updateComponentInstance).not.toHaveBeenCalled(); + expect(component.valueEditModalInstance.closeModal).toHaveBeenCalled(); + }); + + it('after editing instance name, capabilities/requirements should be updated with new name', () => { + const newName = "New VF NAME"; + component.selectedComponent = new FullComponentInstance(<ComponentInstance>{ + name: "VF Test", + requirements: <RequirementsGroup>{"key": [<Requirement>{ownerName: "VF Test"}, <Requirement>{ownerName: "VF Test"}]}, + capabilities: new CapabilitiesGroup() + }, <Component>{}); + component.selectedComponent.capabilities['key'] = [<Capability>{ownerName: "VF Test"}]; + component.valueEditModalInstance = valueEditModalInstance; + component.valueEditModalInstance.innerModalContent.instance.name = newName; + jest.spyOn(component.componentInstanceService, 'updateComponentInstance').mockReturnValue(of(<ComponentInstance>{name: newName})); + component.saveInstanceName(); + + expect(component.selectedComponent.name).toBe(newName); + expect(component.selectedComponent.requirements['key'][0].ownerName).toEqual(newName); + expect(component.selectedComponent.requirements['key'][1].ownerName).toEqual(newName); + expect(component.selectedComponent.capabilities['key'][0].ownerName).toEqual(newName); + }); + + it('if update fails, name is reverted to old value', () => { + component.selectedComponent = new GroupInstance(<GroupInstance>{name: "GROUP NAME"}); + component.valueEditModalInstance = valueEditModalInstance; + jest.spyOn(component.groupService, 'updateName').mockReturnValue(Observable.throw(new Error('Error'))); + component.saveInstanceName(); + expect(component.selectedComponent.name).toEqual("GROUP NAME"); + }); + + it('policy instance uses policies service for update name', () => { + component.selectedComponent = new PolicyInstance(<PolicyInstance>{name: "Policy OLD NAME"}); + component.valueEditModalInstance = valueEditModalInstance; + jest.spyOn(component.policiesService, 'updateName').mockReturnValue(of(true)); + component.saveInstanceName(); + expect(component.policiesService.updateName).toHaveBeenCalledTimes(1); + }); + + it('group instance uses groups service for update name', () => { + component.selectedComponent = new GroupInstance(<GroupInstance>{name: "GROUP NAME"}); + component.valueEditModalInstance = valueEditModalInstance; + jest.spyOn(component.groupService, 'updateName').mockReturnValue(of(true)); + component.saveInstanceName(); + expect(component.groupService.updateName).toHaveBeenCalledTimes(1); + }); + +}); diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/panel-header.component.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/panel-header.component.ts index ab659a3b8f..90a98147e9 100644 --- a/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/panel-header.component.ts +++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/panel-header.component.ts @@ -18,64 +18,70 @@ * ============LICENSE_END========================================================= */ -import { Component, Input, AfterViewInit, SimpleChanges, OnInit, OnChanges } from "@angular/core"; -import { SdcUiComponents } from "sdc-ui/lib/angular"; -import { IModalConfig } from 'sdc-ui/lib/angular/modals/models/modal-config'; -import { ZoneInstanceType } from 'app/models/graph/zones/zone-instance'; -import { ValueEditComponent } from './../../../../components/ui/forms/value-edit/value-edit.component'; -import { Component as TopologyTemplate, ComponentInstance, IAppMenu } from "app/models"; -import { PoliciesService } from '../../../../services/policies.service'; -import { GroupsService } from '../../../../services/groups.service'; -import {IZoneService} from "../../../../../models/graph/zones/zone"; -import { EventListenerService, LoaderService } from "../../../../../services"; -import { GRAPH_EVENTS, EVENTS } from "../../../../../utils"; +import { Component, Input, OnInit } from "@angular/core"; +import { SdcUiComponents, SdcUiCommon, SdcUiServices } from "onap-ui-angular"; +import { EditNameModalComponent } from "app/ng2/pages/composition/panel/panel-header/edit-name-modal/edit-name-modal.component"; +import {Component as TopologyTemplate, FullComponentInstance, GroupInstance, PolicyInstance, Requirement, Capability, ComponentInstance} from "app/models"; +import { Select } from "@ngxs/store"; +import { Observable } from "rxjs/Observable"; +import { Subscription } from "rxjs"; +import {GRAPH_EVENTS} from "../../../../../utils/constants"; +import { CompositionService } from "app/ng2/pages/composition/composition.service"; +import {EventListenerService} from "../../../../../services/event-listener-service"; +import { ComponentInstanceServiceNg2 } from "app/ng2/services/component-instance-services/component-instance.service"; +import { WorkspaceService } from "app/ng2/pages/workspace/workspace.service"; +import { GroupsService, PoliciesService } from "app/services-ng2"; import { UIZoneInstanceObject } from "../../../../../models/ui-models/ui-zone-instance-object"; -import { ModalButtonComponent } from "sdc-ui/lib/angular/components"; +import {SelectedComponentType} from "../../common/store/graph.actions"; +import * as _ from 'lodash'; +import {GraphState} from "../../common/store/graph.state"; + @Component({ selector: 'ng2-composition-panel-header', templateUrl: './panel-header.component.html', styleUrls: ['./panel-header.component.less'] }) -export class CompositionPanelHeaderComponent implements OnInit, OnChanges { - - @Input() topologyTemplate: TopologyTemplate; - @Input() selectedZoneInstanceType: ZoneInstanceType; - @Input() selectedZoneInstanceId: string; - @Input() name: string; - @Input() nonCertified: boolean; +export class CompositionPanelHeaderComponent implements OnInit { @Input() isViewOnly: boolean; - @Input() isLoading: boolean; + @Input() selectedComponent: FullComponentInstance | TopologyTemplate | GroupInstance | PolicyInstance; + @Select(GraphState.getSelectedComponentType) selectedComponentType$:Observable<SelectedComponentType>; + - constructor(private groupsService:GroupsService, private policiesService: PoliciesService, - private modalService:SdcUiComponents.ModalService, private eventListenerService:EventListenerService) { } + constructor(private modalService: SdcUiServices.ModalService, + private groupService: GroupsService, + private policiesService: PoliciesService, + private eventListenerService: EventListenerService, + private compositionService: CompositionService, + private workspaceService: WorkspaceService, + private componentInstanceService: ComponentInstanceServiceNg2) { } - private service:IZoneService; private iconClassName: string; + private valueEditModalInstance: SdcUiComponents.ModalComponent; + private isTopologyTemplateSelected: boolean; + private componentTypeSubscription: Subscription; ngOnInit(): void { - this.init(); - } + this.componentTypeSubscription = this.selectedComponentType$.subscribe((newComponentType) => { - ngOnChanges (changes:SimpleChanges):void { - if(changes.selectedZoneInstanceId){ - this.init(); - } + this.initClasses(newComponentType); + this.isTopologyTemplateSelected = (newComponentType === SelectedComponentType.TOPOLOGY_TEMPLATE) ? true : false; + }); } ngOnDestroy() { - - + if(this.componentTypeSubscription) { + this.componentTypeSubscription.unsubscribe(); + } } - private init = (): void => { - if (this.selectedZoneInstanceType === ZoneInstanceType.POLICY) { + + private initClasses = (componentType:SelectedComponentType): void => { + if (componentType === SelectedComponentType.POLICY) { this.iconClassName = "sprite-policy-icons policy"; - this.service = this.policiesService; - } else if (this.selectedZoneInstanceType === ZoneInstanceType.GROUP) { + } else if (componentType === SelectedComponentType.GROUP) { this.iconClassName = "sprite-group-icons group"; - this.service = this.groupsService; } else { - this.iconClassName = "sprite-resource-icons defaulticon"; + this.iconClassName = undefined; } } @@ -83,53 +89,95 @@ export class CompositionPanelHeaderComponent implements OnInit, OnChanges { const modalConfig = { title: "Edit Name", size: "sm", - type: "custom", + type: SdcUiCommon.ModalType.custom, testId: "renameInstanceModal", buttons: [ {id: 'saveButton', text: 'OK', size: 'xsm', callback: this.saveInstanceName, closeModal: false}, - {id: 'cancelButton', text: 'Cancel', size: 'sm', closeModal: true} - ] as ModalButtonComponent[] - } as IModalConfig; - this.modalService.openCustomModal(modalConfig, ValueEditComponent, {name: this.name, validityChangedCallback: this.enableOrDisableSaveButton}); + {id: 'cancelButton', text: 'Cancel', size: 'sm', closeModal: true} + ] as SdcUiCommon.IModalButtonComponent[] + } as SdcUiCommon.IModalConfig; + this.valueEditModalInstance = this.modalService.openCustomModal(modalConfig, EditNameModalComponent, {name: this.selectedComponent.name, validityChangedCallback: this.enableOrDisableSaveButton}); }; private enableOrDisableSaveButton = (shouldEnable: boolean): void => { - let saveButton: ModalButtonComponent = this.modalService.getCurrentInstance().getButtonById('saveButton'); + let saveButton: SdcUiComponents.ModalButtonComponent = this.valueEditModalInstance.getButtonById('saveButton'); saveButton.disabled = !shouldEnable; } private saveInstanceName = ():void => { - let currentModal = this.modalService.getCurrentInstance(); - let nameFromModal:string = currentModal.innerModalContent.instance.name; - - if(nameFromModal != this.name){ - currentModal.buttons[0].disabled = true; - this.service.updateName(this.topologyTemplate.componentType, this.topologyTemplate.uniqueId, this.selectedZoneInstanceId, nameFromModal).subscribe((success)=>{ - this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_ZONE_INSTANCE_NAME_CHANGED, nameFromModal); - this.modalService.closeModal(); - }, (error)=> { - currentModal.buttons[0].disabled = false; - }); - } else { - this.modalService.closeModal(); + let nameFromModal:string = this.valueEditModalInstance.innerModalContent.instance.name; + + if(nameFromModal != this.selectedComponent.name){ + let oldName = this.selectedComponent.name; + this.selectedComponent.name = nameFromModal; + this.valueEditModalInstance.buttons[0].disabled = true; + + let onFailed = (error) => { + this.selectedComponent.name = oldName; + this.valueEditModalInstance.buttons[0].disabled = false; + }; + + if(this.selectedComponent instanceof FullComponentInstance){ + let onSuccess = (componentInstance:ComponentInstance) => { + //update requirements and capabilities owner name + _.forEach((<FullComponentInstance>this.selectedComponent).requirements, (requirementsArray:Array<Requirement>) => { + _.forEach(requirementsArray, (requirement:Requirement):void => { + requirement.ownerName = componentInstance.name; + }); + }); + + _.forEach((<FullComponentInstance>this.selectedComponent).capabilities, (capabilitiesArray:Array<Capability>) => { + _.forEach(capabilitiesArray, (capability:Capability):void => { + capability.ownerName = componentInstance.name; + }); + }); + this.valueEditModalInstance.closeModal(); + this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_COMPONENT_INSTANCE_NAME_CHANGED, this.selectedComponent); + }; + + this.componentInstanceService.updateComponentInstance(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId, new ComponentInstance(this.selectedComponent)) + .subscribe(onSuccess, onFailed); + } else if (this.selectedComponent instanceof PolicyInstance) { + this.policiesService.updateName(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId, this.selectedComponent.uniqueId, nameFromModal).subscribe((success)=>{ + this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_POLICY_INSTANCE_UPDATE, this.selectedComponent); + this.valueEditModalInstance.closeModal(); + }, onFailed); + } else if (this.selectedComponent instanceof GroupInstance){ + this.groupService.updateName(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId, this.selectedComponent.uniqueId, nameFromModal).subscribe((success)=>{ + this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_GROUP_INSTANCE_UPDATE, this.selectedComponent); + this.valueEditModalInstance.closeModal(); + }, onFailed); + } + } else { + this.valueEditModalInstance.closeModal(); } }; - + private deleteInstance = (): void => { let title:string = "Delete Confirmation"; - let message:string = "Are you sure you would like to delete "+ this.name + "?"; - this.modalService.openAlertModal(title, message, "OK", this.deleteInstanceConfirmed, "deleteInstanceModal"); + let message:string = "Are you sure you would like to delete "+ this.selectedComponent.name + "?"; + const okButton = {testId: "OK", text: "OK", type: SdcUiCommon.ButtonType.warning, callback: this.deleteInstanceConfirmed, closeModal: true} as SdcUiComponents.ModalButtonComponent; + this.modalService.openWarningModal(title, message, "delete-modal", [okButton]); }; - private deleteInstanceConfirmed = () => { - this.eventListenerService.notifyObservers(EVENTS.SHOW_LOADER_EVENT + 'composition-graph'); - this.service.deleteZoneInstance(this.topologyTemplate.componentType, this.topologyTemplate.uniqueId, this.selectedZoneInstanceId).finally(()=> { - this.eventListenerService.notifyObservers(EVENTS.HIDE_LOADER_EVENT + 'composition-graph'); - }).subscribe(()=> { - let deletedItem:UIZoneInstanceObject = new UIZoneInstanceObject(this.selectedZoneInstanceId, this.selectedZoneInstanceType, this.name); - this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_DELETE_ZONE_INSTANCE, deletedItem); - }); - }; + private deleteInstanceConfirmed: Function = () => { + if(this.selectedComponent instanceof FullComponentInstance){ + this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_DELETE_COMPONENT_INSTANCE , this.selectedComponent.uniqueId); + } + else if(this.selectedComponent instanceof PolicyInstance){ + this.policiesService.deletePolicy(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId, this.selectedComponent.uniqueId).subscribe((success)=>{ + this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_DELETE_ZONE_INSTANCE , + new UIZoneInstanceObject(this.selectedComponent.uniqueId, 1)); + }, (err) => {}); + + } + else if(this.selectedComponent instanceof GroupInstance){ + this.groupService.deleteGroup(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId, this.selectedComponent.uniqueId).subscribe((success)=>{ + this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_DELETE_ZONE_INSTANCE , + new UIZoneInstanceObject(this.selectedComponent.uniqueId, 0)); + }, (err) => {}); + } + }; } diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/panel-header.module.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/panel-header.module.ts index bde0a14669..a11bc99fee 100644 --- a/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/panel-header.module.ts +++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-header/panel-header.module.ts @@ -18,29 +18,26 @@ * ============LICENSE_END========================================================= */ import { NgModule } from "@angular/core"; -import { HttpModule } from "@angular/http"; import { FormsModule } from "@angular/forms"; import { BrowserModule } from "@angular/platform-browser"; import { CompositionPanelHeaderComponent } from "./panel-header.component"; import { UiElementsModule } from './../../../../components/ui/ui-elements.module'; -import { ValueEditComponent } from './../../../../components/ui/forms/value-edit/value-edit.component'; -import { SdcUiComponentsModule } from "sdc-ui/lib/angular"; -import { ModalFormsModule } from "app/ng2/components/ui/forms/modal-forms.module"; +import { SdcUiComponentsModule } from "onap-ui-angular"; +import { EditNameModalComponent } from "app/ng2/pages/composition/panel/panel-header/edit-name-modal/edit-name-modal.component"; @NgModule({ declarations: [ - CompositionPanelHeaderComponent + CompositionPanelHeaderComponent, + EditNameModalComponent ], imports: [ BrowserModule, FormsModule, - HttpModule, UiElementsModule, - SdcUiComponentsModule, - ModalFormsModule + SdcUiComponentsModule ], entryComponents: [ - CompositionPanelHeaderComponent, ValueEditComponent + CompositionPanelHeaderComponent, EditNameModalComponent ], exports: [ CompositionPanelHeaderComponent diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/artifacts-tab/__snapshots__/artifact-tab.component.spec.ts.snap b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/artifacts-tab/__snapshots__/artifact-tab.component.spec.ts.snap new file mode 100644 index 0000000000..c143e8106b --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/artifacts-tab/__snapshots__/artifact-tab.component.spec.ts.snap @@ -0,0 +1,50 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`artifact-tab component should match current snapshot of artifact-tab component 1`] = ` +<artifacts-tab + addOrUpdate={[Function Function]} + allowDeleteAndUpdateArtifact={[Function Function]} + artifactService={[Function Object]} + componentInstanceService="undefined" + compositionService={[Function Object]} + delete={[Function Function]} + getEnvArtifact={[Function Function]} + getTitle={[Function Function]} + heatToEnv={[Function Map]} + isLicenseArtifact={[Function Function]} + loadArtifacts={[Function Function]} + store={[Function Store]} + topologyTemplateService="undefined" + updateEnvParams={[Function Function]} + viewEnvParams={[Function Function]} + workspaceService="undefined" +> + <div + class="w-sdc-designer-sidebar-tab-content artifacts" + > + <div + class="w-sdc-designer-sidebar-section" + > + <ng2-expand-collapse + state="0" + > + <header + sdc-tooltip="" + > + + </header> + <content + class="artifacts-container" + > + <div + class="w-sdc-designer-sidebar-section-content" + > + + </div> + + </content> + </ng2-expand-collapse> + </div> + </div> +</artifacts-tab> +`; diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/artifacts-tab/artifact-tab.component.spec.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/artifacts-tab/artifact-tab.component.spec.ts new file mode 100644 index 0000000000..258f2295ab --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/artifacts-tab/artifact-tab.component.spec.ts @@ -0,0 +1,303 @@ +import { async, ComponentFixture } from '@angular/core/testing'; +import { ConfigureFn, configureTests } from '../../../../../../../jest/test-config.helper'; +import { NgxsModule, Store } from '@ngxs/store'; +import { WorkspaceState } from '../../../../../store/states/workspace.state'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ArtifactsTabComponent } from './artifacts-tab.component'; +import { CompositionService } from '../../../composition.service'; +import { WorkspaceService } from '../../../../workspace/workspace.service'; +import { ComponentInstanceServiceNg2 } from '../../../../../services/component-instance-services/component-instance.service'; +import { TopologyTemplateService } from '../../../../../services/component-services/topology-template.service'; +import { ArtifactsService } from '../../../../../components/forms/artifacts-form/artifacts.service'; +import { ArtifactModel } from '../../../../../../models/artifacts'; +import { ArtifactType } from '../../../../../../utils/constants'; +import { FullComponentInstance } from '../../../../../../models/componentsInstances/fullComponentInstance'; +import { ComponentInstance } from '../../../../../../models/componentsInstances/componentInstance'; +import { Component } from '../../../../../../models/components/component'; +import { GetInstanceArtifactsByTypeAction } from '../../../../../store/actions/instance-artifacts.actions'; +import { Observable } from 'rxjs'; + + +describe('artifact-tab component', () => { + + let fixture: ComponentFixture<ArtifactsTabComponent>; + let compositionMockService: Partial<CompositionService>; + const workspaceMockService: Partial<WorkspaceService>; + const componentInstanceMockService: Partial<ComponentInstanceServiceNg2>; + const topologyTemplateMockService: Partial<TopologyTemplateService>; + let artifactsServiceMockService: Partial<ArtifactsService>; + let store: Store; + + beforeEach( + async(() => { + compositionMockService = { + updateInstance: jest.fn() + } + + artifactsServiceMockService = { + deleteArtifact: jest.fn(), + openUpdateEnvParams: jest.fn(), + openArtifactModal: jest.fn() + } + + const configure: ConfigureFn = (testBed) => { + testBed.configureTestingModule({ + declarations: [ArtifactsTabComponent], + imports: [NgxsModule.forRoot([WorkspaceState])], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + {provide: CompositionService, useValue: compositionMockService}, + {provide: WorkspaceService, useValue: workspaceMockService}, + {provide: ComponentInstanceServiceNg2, useValue: componentInstanceMockService}, + {provide: TopologyTemplateService, useValue: topologyTemplateMockService}, + {provide: ArtifactsService, useValue: artifactsServiceMockService} + ], + }); + }; + + configureTests(configure).then((testBed) => { + fixture = testBed.createComponent(ArtifactsTabComponent); + store = testBed.get(Store); + }); + }) + ); + + it ('on delete -> deleteArtifact is being called from artifactService', () => { + const artifact = new ArtifactModel(); + const topologyTemplateType: string = undefined; + const topologyTemplateId: string = undefined; + + fixture.componentInstance.delete(artifact); + expect(artifactsServiceMockService.deleteArtifact).toHaveBeenCalledWith(topologyTemplateType, topologyTemplateId, artifact); + }); + + it('should match current snapshot of artifact-tab component', () => { + expect(fixture).toMatchSnapshot(); + }); + + + it ('should get API Artifacts as Title', () => { + const artifactType = ArtifactType.SERVICE_API; + + const res = fixture.componentInstance.getTitle(artifactType); + expect(res).toBe('API Artifacts'); + }); + + + it ('should get Deployment Artifacts as Title', () => { + const artifactType = ArtifactType.DEPLOYMENT; + + const res = fixture.componentInstance.getTitle(artifactType); + expect(res).toBe('Deployment Artifacts'); + }); + + it ('should get Informational Artifacts as Title', () => { + const artifactType = ArtifactType.INFORMATION; + + const res = fixture.componentInstance.getTitle(artifactType); + expect(res).toBe('Informational Artifacts'); + }); + + it ('should get SomeString as Title - This is the default case (return the last val)', () => { + // So the last value will be "SomeString" + fixture.componentInstance.getTitle('SomeString'); + + const res = fixture.componentInstance.getTitle('SomeString'); + expect(res).toBe('SomeString Artifacts'); + }); + + + it ('should return isLicenseArtifact false', () => { + const artifact = new ArtifactModel(); + const componentInstance = new ComponentInstance(); + const component = new Component(); + fixture.componentInstance.component = new FullComponentInstance(componentInstance, component); + + let res = fixture.componentInstance.isLicenseArtifact(artifact); + expect(res).toBe(false); + }); + + it ('should return isLicenseArtifact true', () => { + const artifact = new ArtifactModel(); + const componentInstance = new ComponentInstance(); + const component = new Component(); + fixture.componentInstance.component = new FullComponentInstance(componentInstance, component); + fixture.componentInstance.component.isResource = jest.fn(() => true); + fixture.componentInstance.component.isCsarComponent = true; + + artifact.artifactType = ArtifactType.VENDOR_LICENSE; + const res = fixture.componentInstance.isLicenseArtifact(artifact); + expect(res).toBe(true); + }); + + it ('should verify getEnvArtifact with match', () => { + const artifact = new ArtifactModel(); + artifact.uniqueId = 'matchUniqueID'; + + const testItem1 = new ArtifactModel(); + testItem1.generatedFromId = 'matchUniqueID'; + + const testItem2 = new ArtifactModel(); + testItem2.generatedFromId = '123456'; + + const artifacts: ArtifactModel[] = [testItem1, testItem2]; + + const res = fixture.componentInstance.getEnvArtifact(artifact, artifacts); + expect(res.generatedFromId).toBe('matchUniqueID'); + }); + + it ('should verify getEnvArtifact with no match', () => { + const artifact = new ArtifactModel(); + artifact.uniqueId = 'matchUniqueID'; + + const testItem1 = new ArtifactModel(); + testItem1.generatedFromId = '654321'; + + const testItem2 = new ArtifactModel(); + testItem2.generatedFromId = '123456'; + + const artifacts: ArtifactModel[] = [testItem1, testItem2]; + + const res = fixture.componentInstance.getEnvArtifact(artifact, artifacts); + expect(res).toBe(undefined); + }); + + it ('on updateEnvParams -> openUpdateEnvParams is being called from artifactService when isComponentInstanceSelected = true', () => { + const artifact = new ArtifactModel(); + artifact.envArtifact = new ArtifactModel(); + + const topologyTemplateType: string = undefined; + const topologyTemplateId: string = undefined; + + const component = new Component(); + component.uniqueId = 'id'; + + const isComponentInstanceSelected = true; + + fixture.componentInstance.component = component; + fixture.componentInstance.isComponentInstanceSelected = isComponentInstanceSelected; + fixture.componentInstance.updateEnvParams(artifact); + + expect(artifactsServiceMockService.openUpdateEnvParams).toHaveBeenCalledWith(topologyTemplateType, topologyTemplateId, undefined, component.uniqueId); + }); + + it ('on updateEnvParams -> openUpdateEnvParams is being called from artifactService when isComponentInstanceSelected = false', () => { + const artifact = new ArtifactModel(); + + const topologyTemplateType: string = undefined + const topologyTemplateId: string = undefined; + + const component = new Component(); + + const isComponentInstanceSelected = false; + + fixture.componentInstance.component = component; + fixture.componentInstance.isComponentInstanceSelected = isComponentInstanceSelected; + fixture.componentInstance.updateEnvParams(artifact); + + expect(artifactsServiceMockService.openUpdateEnvParams).toHaveBeenCalledWith(topologyTemplateType, topologyTemplateId, artifact); + }); + + it ('on addOrUpdate -> openArtifactModal is being called from artifactService when isComponentInstanceSelected = true', () => { + const artifact = new ArtifactModel(); + + const topologyTemplateType: string = 'testType'; + const topologyTemplateId: string = 'testID'; + const type: string = 'testType'; + const isViewOnly: boolean = false; + + const component = new Component(); + component.uniqueId = 'id'; + + const isComponentInstanceSelected = true; + + fixture.componentInstance.component = component; + fixture.componentInstance.type = type; + fixture.componentInstance.topologyTemplateId = topologyTemplateId; + fixture.componentInstance.topologyTemplateType = topologyTemplateType; + fixture.componentInstance.isComponentInstanceSelected = isComponentInstanceSelected; + fixture.componentInstance.isViewOnly = isViewOnly; + fixture.componentInstance.addOrUpdate(artifact); + + + expect(artifactsServiceMockService.openArtifactModal).toHaveBeenCalledWith(topologyTemplateId, topologyTemplateType, artifact, type, isViewOnly, component.uniqueId); + }); + + it ('on addOrUpdate -> openArtifactModal is being called from artifactService when isComponentInstanceSelected = false', () => { + const artifact = new ArtifactModel(); + + const topologyTemplateType: string = 'testType'; + const topologyTemplateId: string = 'testID'; + const type: string = 'testType'; + const isViewOnly: boolean = false; + + const isComponentInstanceSelected = false; + + fixture.componentInstance.type = type; + fixture.componentInstance.isComponentInstanceSelected = isComponentInstanceSelected; + fixture.componentInstance.topologyTemplateId = topologyTemplateId; + fixture.componentInstance.topologyTemplateType = topologyTemplateType; + fixture.componentInstance.isViewOnly = isViewOnly; + fixture.componentInstance.addOrUpdate(artifact); + + expect(artifactsServiceMockService.openArtifactModal).toHaveBeenCalledWith(topologyTemplateId, topologyTemplateType, artifact, type, isViewOnly); + }); + + + it ('verify allowDeleteAndUpdateArtifact return false since isViewOnly=true', () => { + const artifact = new ArtifactModel(); + fixture.componentInstance.isViewOnly = true; + + const res = fixture.componentInstance.allowDeleteAndUpdateArtifact(artifact); + expect(res).toBe(false) + }); + + it ('verify allowDeleteAndUpdateArtifact return artifact.isFromCsar since isViewOnly=false && artifactGroupType = DEPLOYMENT', () => { + const artifact = new ArtifactModel(); + artifact.artifactGroupType = ArtifactType.DEPLOYMENT; + artifact.isFromCsar = false; + + fixture.componentInstance.isViewOnly = false; + + const res = fixture.componentInstance.allowDeleteAndUpdateArtifact(artifact); + expect(res).toBe(!artifact.isFromCsar); + }); + + it ('verify allowDeleteAndUpdateArtifact return !artifact.isHEAT() && !artifact.isThirdParty() &&' + + ' !this.isLicenseArtifact(artifact) since isViewOnly=false && artifactGroupType != DEPLOYMENT', () => { + const artifact = new ArtifactModel(); + artifact.artifactGroupType = 'NOT_DEPLOYMENT'; + artifact.isHEAT = () => false; + artifact.isThirdParty = () => false; + + fixture.componentInstance.isLicenseArtifact = jest.fn(() => false); + + fixture.componentInstance.isViewOnly = false; + + const res = fixture.componentInstance.allowDeleteAndUpdateArtifact(artifact); + expect(res).toBe(true ) + }); + + it('verify action on loadArtifacts in case isComponentInstanceSelected = true', () => { + fixture.componentInstance.isComponentInstanceSelected = true; + fixture.componentInstance.topologyTemplateType = 'topologyTemplateType'; + fixture.componentInstance.topologyTemplateId = 'topologyTemplateId'; + const component = new Component(); + component.uniqueId = 'uniqueId'; + fixture.componentInstance.component = component; + fixture.componentInstance.type = 'type'; + + const action = new GetInstanceArtifactsByTypeAction(({ + componentType: 'topologyTemplateType', + componentId: 'topologyTemplateId', + instanceId: 'uniqueId', + artifactType: 'type' + })) + + fixture.componentInstance.store.dispatch = jest.fn(() => Observable.of(true)); + fixture.componentInstance.loadArtifacts(); + + expect(store.dispatch).toBeCalledWith(action); + + }); +}); diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/artifacts-tab/artifacts-tab.component.html b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/artifacts-tab/artifacts-tab.component.html new file mode 100644 index 0000000000..264444b674 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/artifacts-tab/artifacts-tab.component.html @@ -0,0 +1,119 @@ +<div class="w-sdc-designer-sidebar-tab-content artifacts"> + <div class="w-sdc-designer-sidebar-section"> + <ng2-expand-collapse state="0"> + <header sdc-tooltip tooltip-text="{{title}}">{{title}}</header> + <content class="artifacts-container"> + <div class="w-sdc-designer-sidebar-section-content"> + <div class="i-sdc-designer-sidebar-section-content-item" *ngFor="let artifact of artifacts$ | async"> + <div class="i-sdc-designer-sidebar-section-content-item-artifact" + *ngIf="(!isComponentInstanceSelected || artifact.esId) && 'HEAT_ENV' !== artifact.artifactType" + attr.data-tests-id="'artifact-item-' + artifact.artifactDisplayName"> + <span *ngIf="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" + [class.heat]="artifact.isHEAT() && artifact.heatParameters?.length"> + <div *ngIf="artifact.artifactName" + class="i-sdc-designer-sidebar-section-content-item-artifact-filename" + attr.data-tests-id="artifactName-{{artifact.artifactDisplayName}}" + sdc-tooltip tooltip-text="{{artifact.artifactName}}">{{artifact.artifactName}} + </div> + <div class="artifact-buttons-container upper-buttons"> + + + <svg-icon + *ngIf="!isViewOnly && !artifact.isFromCsar && artifact.artifactName" + name="trash-o" clickable="true" size="medium" mode="info" + class="artifact-button" testId="delete_{{artifact.artifactDisplayName}}" + (click)="delete(artifact)"></svg-icon> + + <!--Display env parameters edit button for Instance --> + <svg-icon + *ngIf="!isViewOnly && artifact.isHEAT() && isComponentInstanceSelected && artifact.heatParameters?.length" + name="indesign_status" clickable="true" size="medium" mode="info" + class="artifact-button" + testId="edit-parameters-of-{{artifact.artifactDisplayName}}" + (click)="updateEnvParams(artifact)" + tooltip="Edit ENV Params" + ></svg-icon> + + <!--Display env parameters VIEW button for Instance --> + <svg-icon + *ngIf="isViewOnly && artifact.isHEAT() && isComponentInstanceSelected && artifact.heatParameters?.length" + name="inputs-o" clickable="true" size="medium" mode="info" + class="artifact-button" + testId="view-parameters-of-{{artifact.artifactDisplayName}}" + (click)="viewEnvParams(artifact)" + tooltip="View ENV Params" + ></svg-icon> + + <!--Display env parameters edit button for VF --> + <svg-icon + *ngIf = "!isViewOnly && !isComponentInstanceSelected && artifact.heatParameters?.length" + name="indesign_status" clickable="true" size="medium" mode="info" + class="artifact-button" + testId="edit-parameters-of-{{artifact.artifactDisplayName}}" + (click)="updateEnvParams(artifact)"></svg-icon> + + + <download-artifact *ngIf="artifact.esId && 'deployment' != type" + class="artifact-button" + [artifact]="artifact" [componentType]="component.componentType" + [componentId]="component.uniqueId" + testId="download_{{artifact.artifactDisplayName}}" + [isInstance]="isComponentInstanceSelected"></download-artifact> + <download-artifact *ngIf="artifact.esId && 'deployment' == type" + class="artifact-button" + [artifact]="artifact" [componentType]="component.componentType" + [componentId]="component.uniqueId" + [isInstance]="isComponentInstanceSelected" + testId="download_{{artifact.artifactDisplayName}}" + [showLoader]="artifact.isHEAT()"></download-artifact> + + <button *ngIf="!isViewOnly && !artifact.esId && type==='deployment' && !isComponentInstanceSelected && !artifact.isThirdParty()" + class="artifact-button attach sprite e-sdc-small-icon-upload" + (click)="addOrUpdate(artifact)" type="button" + attr.data-tests-id="add_Artifact"></button> + </div> + <div> + <span class="i-sdc-designer-sidebar-section-content-item-artifact-details-name" + attr.data-tests-id="artifact_Display_Name-{{artifact.artifactDisplayName}}" + [ngClass]="{'hand enabled': artifact.allowDeleteAndUpdate}" + (click)="artifact.allowDeleteAndUpdate && addOrUpdate(artifact)" + sdc-tooltip tooltip-text="{{artifact.artifactDisplayName}}">{{artifact.artifactDisplayName}}</span> + <div class="i-sdc-designer-sidebar-section-content-item-artifact-heat-env" + *ngIf="artifact.heatParameters?.length"> + <span attr.data-tests-id="heat_env_{{artifact.artifactDisplayName}}">{{artifact.artifactDisplayName}} (ENV)</span> + <div class="artifact-buttons-container"> + <svg-icon *ngIf="!isViewOnly && envArtifactOf(artifact)" + name="edit-o" clickable="true" size="medium" + mode="info" class="artifact-button edit-pencil" + testId="edit_{{artifact.artifactDisplayName}}" + (click)="addOrUpdate(envArtifactOf(artifact))"></svg-icon> + + <download-artifact [artifact]="envArtifactOf(artifact)" + class="artifact-button" + [componentType]="component.componentType" + [componentId]="component.uniqueId" + [isInstance]="isComponentInstanceSelected" + testId="download_env_{{artifact.artifactDisplayName}}"></download-artifact> + </div> + </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" + *ngIf="artifact.description">Description:</span>{{artifact.description}} + </div> + </div> + </div> + </div> + </div> + <div class="w-sdc-designer-sidebar-section-footer" + *ngIf="!isViewOnly && type!=='api' && (!isComponentInstanceSelected || isVfOrPnf() && (type !== 'deployment') || isComplex)"> + <sdc-button testId="add_Artifact_Button" size="large" type="primary" text="Add Artifact" + (click)="addOrUpdate({})"></sdc-button> + </div> + </content> + </ng2-expand-collapse> + </div> +</div> diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/artifacts-tab/artifacts-tab.component.less b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/artifacts-tab/artifacts-tab.component.less new file mode 100644 index 0000000000..fef199dd97 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/artifacts-tab/artifacts-tab.component.less @@ -0,0 +1,169 @@ +@import '../../../../../../../assets/styles/override'; + + +.artifacts /deep/ .expand-collapse-content { + padding: 10px 0px; + + &.collapsed { + padding: 0 0; + } +} + +.i-sdc-designer-sidebar-section-content-item-artifact { + + &:not(:hover) .artifact-button { + display:none; + } + .artifact-buttons-container { + display: inline-flex; + flex-direction: row-reverse; + position: absolute; + right:0; + + &.upper-buttons { + margin-top: 8px; + } + + .artifact-button { + cursor:pointer; + padding-right:5px; + + &.edit-pencil { + margin-top: 10px; + } + } + } +} + +.w-sdc-designer-sidebar-section-footer { + padding: 20px; + display: flex; + justify-content: center; + +} + + +.w-sdc-designer-sidebar-tab-content.artifacts { + + .i-sdc-designer-sidebar-section-content-item-artifact.hand { + cursor: pointer; + } + + .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 { + + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width:220px; + display: inline-block; + //text-transform: capitalize; + &.enabled { + &:hover { + color: @sdcui_color_dark-blue; + } + } + + } + + .i-sdc-designer-sidebar-section-content-item-artifact-heat-env { + color: @sdcui_color_dark-gray; + margin-top: 6px; + line-height: 42px; + padding-top: 10px; + border-top:1px solid #c8cdd1; + .enabled { + &:hover { + cursor: pointer; + color: @sdcui_color_dark-blue; + } + } + } + + .i-sdc-designer-sidebar-section-content-item-artifact-filename { + color: @sdcui_color_dark-gray; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 225px; + display: inline-block; + font-weight: bold; + &.enabled { + &:hover { + color: @sdcui_color_dark-blue; + } + } + } + + + .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 { + color: @sdcui_color_dark-gray; + } + + + .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; + background-color: white; + transition: all .3s; + + .i-sdc-designer-sidebar-section-content-item-button { + display: block; + + } + + } + } +} diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/artifacts-tab/artifacts-tab.component.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/artifacts-tab/artifacts-tab.component.ts new file mode 100644 index 0000000000..53a6c267e2 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/artifacts-tab/artifacts-tab.component.ts @@ -0,0 +1,204 @@ +import { Component, Input } from '@angular/core'; +import { Store } from '@ngxs/store'; +import { ArtifactModel, Component as TopologyTemplate, FullComponentInstance, Resource } from 'app/models'; +import { WorkspaceService } from 'app/ng2/pages/workspace/workspace.service'; +import { ResourceNamePipe } from 'app/ng2/pipes/resource-name.pipe'; +import { ComponentInstanceServiceNg2 } from 'app/ng2/services/component-instance-services/component-instance.service'; +import { TopologyTemplateService } from 'app/ng2/services/component-services/topology-template.service'; +import { ArtifactType } from 'app/utils'; +import * as _ from 'lodash'; +import { SdcUiServices } from 'onap-ui-angular'; +import { Observable } from 'rxjs/Observable'; +import { map } from 'rxjs/operators'; +import { ArtifactsService } from '../../../../../components/forms/artifacts-form/artifacts.service'; +import { GetArtifactsByTypeAction } from '../../../../../store/actions/artifacts.action'; +import { GetInstanceArtifactsByTypeAction } from '../../../../../store/actions/instance-artifacts.actions'; +import { ArtifactsState } from '../../../../../store/states/artifacts.state'; +import { InstanceArtifactsState } from '../../../../../store/states/instance-artifacts.state'; +import { SelectedComponentType, TogglePanelLoadingAction } from '../../../common/store/graph.actions'; +import { CompositionService } from '../../../composition.service'; + +@Component({ + selector: 'artifacts-tab', + styleUrls: ['./artifacts-tab.component.less'], + templateUrl: './artifacts-tab.component.html', + providers: [SdcUiServices.ModalService] +}) + +export class ArtifactsTabComponent { + + @Input() component: FullComponentInstance | TopologyTemplate; + @Input() isViewOnly: boolean; + @Input() input: any; + @Input() componentType: SelectedComponentType; + + public title: string; + public type: string; + public isComponentInstanceSelected: boolean; + public artifacts$: Observable<ArtifactModel[]>; + private topologyTemplateType: string; + private topologyTemplateId: string; + private heatToEnv: Map<string, ArtifactModel>; + private resourceType: string; + private isComplex: boolean; + + constructor(private store: Store, + private compositionService: CompositionService, + private workspaceService: WorkspaceService, + private componentInstanceService: ComponentInstanceServiceNg2, + private topologyTemplateService: TopologyTemplateService, + private artifactService: ArtifactsService) { + this.heatToEnv = new Map(); + } + + ngOnInit() { + this.topologyTemplateType = this.workspaceService.metadata.componentType; + this.topologyTemplateId = this.workspaceService.metadata.uniqueId; + this.type = this.input.type; + this.title = this.getTitle(this.type); + this.isComponentInstanceSelected = this.componentType === SelectedComponentType.COMPONENT_INSTANCE; + this.resourceType = this.component['resourceType']; + this.isComplex = this.component.isComplex(); + this.loadArtifacts(); + } + + public addOrUpdate = (artifact: ArtifactModel): void => { + if (this.isComponentInstanceSelected) { + this.artifactService.openArtifactModal(this.topologyTemplateId, this.topologyTemplateType, artifact, this.type, this.isViewOnly, this.component.uniqueId); + } else { + this.artifactService.openArtifactModal(this.topologyTemplateId, this.topologyTemplateType, artifact, this.type, this.isViewOnly); + } + } + + public updateEnvParams = (artifact: ArtifactModel) => { + if (this.isComponentInstanceSelected) { + this.artifactService.openUpdateEnvParams(this.topologyTemplateType, this.topologyTemplateId, this.heatToEnv.get(artifact.uniqueId), this.component.uniqueId); + } else { + this.artifactService.openUpdateEnvParams(this.topologyTemplateType, this.topologyTemplateId, artifact); + } + } + + public viewEnvParams = (artifact: ArtifactModel) => { + if (this.isComponentInstanceSelected) { + this.artifactService.openViewEnvParams(this.topologyTemplateType, this.topologyTemplateId, this.heatToEnv.get(artifact.uniqueId), this.component.uniqueId); + } else { + this.artifactService.openViewEnvParams(this.topologyTemplateType, this.topologyTemplateId, artifact); + } + } + + public getEnvArtifact = (heatArtifact: ArtifactModel, artifacts: ArtifactModel[]): ArtifactModel => { + const envArtifact = _.find(artifacts, (item: ArtifactModel) => { + return item.generatedFromId === heatArtifact.uniqueId; + }); + if (envArtifact && heatArtifact) { + envArtifact.artifactDisplayName = heatArtifact.artifactDisplayName; + envArtifact.timeout = heatArtifact.timeout; + } + return envArtifact; + } + + public delete = (artifact: ArtifactModel): void => { + if (this.isComponentInstanceSelected) { + this.artifactService.deleteArtifact(this.topologyTemplateType, this.topologyTemplateId, artifact, this.component.uniqueId); + } else { + this.artifactService.deleteArtifact(this.topologyTemplateType, this.topologyTemplateId, artifact); + } + } + + public isVfOrPnf(): boolean { + if (this.component.isResource()){ + if (this.resourceType) { + return this.resourceType === 'VF' || this.resourceType == 'PNF'; + } + return false; + } + + return false; + } + + private envArtifactOf(artifact: ArtifactModel): ArtifactModel { + return this.heatToEnv.get(artifact.uniqueId); + } + + private isLicenseArtifact = (artifact: ArtifactModel): boolean => { + let isLicense: boolean = false; + if (this.component.isResource && (this.component as Resource).isCsarComponent) { + if (ArtifactType.VENDOR_LICENSE === artifact.artifactType || ArtifactType.VF_LICENSE === artifact.artifactType) { + isLicense = true; + } + } + + return isLicense; + } + + private getTitle = (artifactType: string): string => { + switch (artifactType) { + case ArtifactType.SERVICE_API: + return 'API Artifacts'; + case ArtifactType.DEPLOYMENT: + return 'Deployment Artifacts'; + case ArtifactType.INFORMATION: + return 'Informational Artifacts'; + default: + return ResourceNamePipe.getDisplayName(artifactType) + ' Artifacts'; + } + } + + private loadArtifacts = (forceLoad?: boolean): void => { + + this.store.dispatch(new TogglePanelLoadingAction({isLoading: true})); + + let action; + if (this.isComponentInstanceSelected) { + action = new GetInstanceArtifactsByTypeAction(({ + componentType: this.topologyTemplateType, + componentId: this.topologyTemplateId, + instanceId: this.component.uniqueId, + artifactType: this.type + })); + } else { + action = new GetArtifactsByTypeAction({ + componentType: this.topologyTemplateType, + componentId: this.topologyTemplateId, + artifactType: this.type + }); + } + this.store.dispatch(action).subscribe(() => { + const stateSelector = this.isComponentInstanceSelected ? InstanceArtifactsState.getArtifactsByType : ArtifactsState.getArtifactsByType; + this.artifacts$ = this.store.select(stateSelector).pipe(map((filterFn) => filterFn(this.type))).pipe(map((artifacts) => { + _.forEach(artifacts, (artifact: ArtifactModel): void => { + const envArtifact = this.getEnvArtifact(artifact, artifacts); // Extract the env artifact (if exist) of the HEAT artifact + if (envArtifact) { + // Set a mapping between HEAT to HEAT_ENV + this.heatToEnv.set(artifact.uniqueId, envArtifact); + } + }); + return _.orderBy(artifacts, ['mandatory', 'artifactDisplayName'], ['desc', 'asc']); + })); + + this.artifacts$.subscribe((artifacts) => { + _.forEach(artifacts, (artifact: ArtifactModel) => { + artifact.allowDeleteAndUpdate = this.allowDeleteAndUpdateArtifact(artifact); + }); + if (this.component instanceof FullComponentInstance) { + this.compositionService.updateInstance(this.component); + } + }); + this.store.dispatch(new TogglePanelLoadingAction({isLoading: false})); + }, () => { + this.store.dispatch(new TogglePanelLoadingAction({isLoading: false})); + }); + } + + private allowDeleteAndUpdateArtifact = (artifact: ArtifactModel): boolean => { + if (!this.isViewOnly) { + if (artifact.artifactGroupType === ArtifactType.DEPLOYMENT) { + return !artifact.isFromCsar; + } else { + + return (!artifact.isHEAT() && !artifact.isThirdParty() && !this.isLicenseArtifact(artifact)); + } + } + return false; + } +} diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-members-tab.component.html b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/group-members-tab/group-members-tab.component.html index 6585ad2da9..8c5c9c7663 100644 --- a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-members-tab.component.html +++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/group-members-tab/group-members-tab.component.html @@ -14,7 +14,7 @@ ~ limitations under the License. --> -<div class="w-sdc-designer-sidebar-section-title" tooltip="Members">Members +<h1 class="w-sdc-designer-sidebar-section-title" tooltip="Members">Members <svg-icon-label *ngIf="!isViewOnly" class="add-members-btn" name="plus-circle-o" @@ -24,7 +24,7 @@ labelPlacement="right" (click)="openAddMembersModal()"> </svg-icon-label> -</div> +</h1> <div class="expand-collapse-content"> <ul> <li *ngFor="let member of members; let i = index" class="component-details-panel-large-item" @@ -40,7 +40,7 @@ </li> </ul> - <div *ngIf="members.length===0" class="component-details-panel-tab-no-data"> + <div *ngIf="!members || members.length===0" class="component-details-panel-tab-no-data"> <div class="component-details-panel-tab-no-data-title">No data to display yet</div> <div class="component-details-panel-tab-no-data-content">Add members to group to see members</div> </div> diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/group-members-tab/group-members-tab.component.spec.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/group-members-tab/group-members-tab.component.spec.ts new file mode 100644 index 0000000000..43f6aac2c7 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/group-members-tab/group-members-tab.component.spec.ts @@ -0,0 +1,127 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { async, ComponentFixture } from '@angular/core/testing'; +import { SdcUiCommon, SdcUiComponents, SdcUiServices } from 'onap-ui-angular'; +import { Observable } from 'rxjs/Rx'; +import { Mock } from 'ts-mockery'; +import { ConfigureFn, configureTests } from '../../../../../../../jest/test-config.helper'; +import { ComponentMetadata } from '../../../../../../models/component-metadata'; +import { GroupInstance } from '../../../../../../models/graph/zones/group-instance'; +import { EventListenerService } from '../../../../../../services/event-listener-service'; +import { GroupsService } from '../../../../../services/groups.service'; +import { TranslateService } from '../../../../../shared/translator/translate.service'; +import { WorkspaceService } from '../../../../workspace/workspace.service'; +import { CompositionService } from '../../../composition.service'; +import { GroupMembersTabComponent } from './group-members-tab.component'; + +describe('group members tab component', () => { + + let fixture: ComponentFixture<GroupMembersTabComponent>; + + // Mocks + let workspaceServiceMock: Partial<WorkspaceService>; + let eventsListenerServiceMock: Partial<EventListenerService>; + let groupServiceMock: Partial<GroupsService>; + let loaderServiceMock: Partial<SdcUiServices.LoaderService>; + let compositionServiceMock: Partial<CompositionService>; + let modalServiceMock: Partial<SdcUiServices.ModalService>; + + const membersToAdd = [ + {uniqueId: '1', name: 'inst1'}, + {uniqueId: '2', name: 'inst2'}, + ]; + + beforeEach( + async(() => { + + eventsListenerServiceMock = {}; + + groupServiceMock = Mock.of<GroupsService>( + { + updateMembers: jest.fn().mockImplementation((compType, uid, groupUniqueId, updatedMembers) => { + if (updatedMembers === undefined) { + return Observable.throwError('error'); + } else { + return Observable.of(updatedMembers); + } + } + )}); + + compositionServiceMock = { + getComponentInstances: jest.fn().mockImplementation( () => { + return [{uniqueId: '1', name: 'inst1'}, + {uniqueId: '2', name: 'inst2'}, + {uniqueId: '3', name: 'inst3'}, + {uniqueId: '4', name: 'inst4'}, + {uniqueId: '5', name: 'inst5'} + ]; + } + ) + }; + + workspaceServiceMock = { + metadata: Mock.of<ComponentMetadata>() + }; + + const addMemberModalInstance = { + innerModalContent: { instance: { existingElements: membersToAdd }}, + closeModal: jest.fn() + }; + + modalServiceMock = { + openInfoModal: jest.fn(), + openCustomModal: jest.fn().mockImplementation(() => addMemberModalInstance) + }; + + loaderServiceMock = { + activate: jest.fn(), + deactivate: jest.fn() + }; + + const groupInstanceMock = Mock.of<GroupInstance>(); + + const configure: ConfigureFn = (testBed) => { + testBed.configureTestingModule({ + declarations: [GroupMembersTabComponent], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + {provide: TranslateService, useValue: { translate: jest.fn() }}, + {provide: GroupsService, useValue: groupServiceMock}, + {provide: SdcUiServices.ModalService, useValue: modalServiceMock }, + {provide: EventListenerService, useValue: eventsListenerServiceMock }, + {provide: CompositionService, useValue: compositionServiceMock }, + {provide: WorkspaceService, useValue: workspaceServiceMock}, + {provide: SdcUiServices.LoaderService, useValue: loaderServiceMock } + ], + }); + }; + + configureTests(configure).then((testBed) => { + fixture = testBed.createComponent(GroupMembersTabComponent); + fixture.componentInstance.group = groupInstanceMock; + }); + }) + ); + + it('test that initially all members are available for adding', () => { + const testedComponent = fixture.componentInstance; + + // No members are currently in the group, all 5 members should be returned + const optionalMembersToAdd = testedComponent.getOptionalsMembersToAdd(); + expect(optionalMembersToAdd).toHaveLength(5); + }); + + it('test list of available instances to add does not include existing members', () => { + const testedComponent = fixture.componentInstance; + + // Mock the group instance to return the members that we are about to add + testedComponent.group.getMembersAsUiObject = jest.fn().mockImplementation( () => membersToAdd); + + // The opened modal shall return 2 members to be added + testedComponent.openAddMembersModal(); + testedComponent.addMembers(); // Shall add 2 members (1,2) + + // Now the getOptionalsMembersToAdd shall return 3 which are the members that were no added yet + const optionalMembersToAdd = testedComponent.getOptionalsMembersToAdd(); + expect(optionalMembersToAdd).toHaveLength(3); + }); +}); diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/group-members-tab/group-members-tab.component.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/group-members-tab/group-members-tab.component.ts new file mode 100644 index 0000000000..7f1222367d --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/group-members-tab/group-members-tab.component.ts @@ -0,0 +1,158 @@ +/*- + * ============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========================================================= + */ + +import { Component, HostBinding, Input, OnDestroy, OnInit } from '@angular/core'; +import { Select } from '@ngxs/store'; +import { GroupInstance } from 'app/models/graph/zones/group-instance'; +import { CompositionService } from 'app/ng2/pages/composition/composition.service'; +import { WorkspaceService } from 'app/ng2/pages/workspace/workspace.service'; +import { EventListenerService } from 'app/services/event-listener-service'; +import { GRAPH_EVENTS } from 'app/utils'; +import * as _ from 'lodash'; +import { SdcUiCommon, SdcUiComponents, SdcUiServices } from 'onap-ui-angular'; +import { Observable, Subscription } from 'rxjs'; +import { tap } from 'rxjs/operators'; +import { ComponentInstance } from '../../../../../../models/componentsInstances/componentInstance'; +import { MemberUiObject } from '../../../../../../models/ui-models/ui-member-object'; +import { AddElementsComponent } from '../../../../../components/ui/modal/add-elements/add-elements.component'; +import {GraphState} from "../../../common/store/graph.state"; +import { GroupsService } from '../../../../../services/groups.service'; +import { TranslateService } from '../../../../../shared/translator/translate.service'; + +@Component({ + selector: 'group-members-tab', + templateUrl: './group-members-tab.component.html', + styleUrls: ['./../policy-targets-tab/policy-targets-tab.component.less'] +}) + +export class GroupMembersTabComponent implements OnInit, OnDestroy { + + @Input() group: GroupInstance; + @Input() isViewOnly: boolean; + @Select(GraphState.getSelectedComponent) group$: Observable<GroupInstance>; + @HostBinding('class') classes = 'component-details-panel-tab-group-members'; + + private members: MemberUiObject[]; + private addMemberModalInstance: SdcUiComponents.ModalComponent; + private subscription: Subscription; + + constructor( + private translateService: TranslateService, + private groupsService: GroupsService, + private modalService: SdcUiServices.ModalService, + private eventListenerService: EventListenerService, + private compositionService: CompositionService, + private workspaceService: WorkspaceService, + private loaderService: SdcUiServices.LoaderService + ) { + } + + ngOnInit() { + this.subscription = this.group$.pipe( + tap((group) => { + this.group = group; + this.members = this.group.getMembersAsUiObject(this.compositionService.componentInstances); + })).subscribe(); + } + + ngOnDestroy() { + if (this.subscription) { + this.subscription.unsubscribe(); + } + } + + deleteMember = (member: MemberUiObject): void => { + this.loaderService.activate(); + this.groupsService.deleteGroupMember( + this.workspaceService.metadata.componentType, + this.workspaceService.metadata.uniqueId, + this.group, + member.uniqueId).subscribe( + (updatedMembers: string[]) => { + this.group.members = updatedMembers; + this.initMembers(); + this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_GROUP_INSTANCE_UPDATE, this.group); + }, + () => console.log('Error deleting member!'), + () => this.loaderService.deactivate() + ); + } + + addMembers = (): void => { + // TODO refactor sdc-ui modal in order to return the data + const membersToAdd: MemberUiObject[] = this.addMemberModalInstance.innerModalContent.instance.existingElements; + if (membersToAdd.length > 0) { + this.addMemberModalInstance.closeModal(); + this.loaderService.activate(); + const locallyUpdatedMembers: MemberUiObject[] = _.union(this.members, membersToAdd); + this.groupsService.updateMembers( + this.workspaceService.metadata.componentType, + this.workspaceService.metadata.uniqueId, + this.group.uniqueId, + locallyUpdatedMembers).subscribe( + (updatedMembers: string[]) => { + this.group.members = updatedMembers; + this.initMembers(); + this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_GROUP_INSTANCE_UPDATE, this.group); + }, + () => { + console.log('Error updating members!'); + }, () => + this.loaderService.deactivate() + ); + } + } + + getOptionalsMembersToAdd(): MemberUiObject[] { + const optionalsMembersToAdd: MemberUiObject[] = []; + // adding all instances as optional members to add if not already exist + _.forEach(this.compositionService.getComponentInstances(), (instance: ComponentInstance) => { + if (!_.some(this.members, (member: MemberUiObject) => { + return member.uniqueId === instance.uniqueId; + })) { + optionalsMembersToAdd.push(new MemberUiObject(instance.uniqueId, instance.name)); + } + }); + return optionalsMembersToAdd; + } + + openAddMembersModal(): void { + const addMembersModalConfig = { + title: this.group.name + ' ADD MEMBERS', + size: 'md', + type: SdcUiCommon.ModalType.custom, + testId: 'addMembersModal', + buttons: [ + {text: 'ADD MEMBERS', size: 'medium', callback: this.addMembers, closeModal: false}, + {text: 'CANCEL', size: 'sm', type: 'secondary', closeModal: true} + ] + } as SdcUiCommon.IModalConfig; + const optionalsMembersToAdd = this.getOptionalsMembersToAdd(); + this.addMemberModalInstance = this.modalService.openCustomModal(addMembersModalConfig, AddElementsComponent, { + elementsToAdd: optionalsMembersToAdd, + elementName: 'member' + }); + } + + private initMembers = (groupInstance?: GroupInstance) => { + this.group = groupInstance ? groupInstance : this.group; + this.members = this.group.getMembersAsUiObject(this.compositionService.getComponentInstances()); + } +} diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-properties-tab.component.html b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/group-or-policy-properties-tab/group-or-policy-properties-tab.component.html index fe1f6b4f0d..c57f99786c 100644 --- a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-properties-tab.component.html +++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/group-or-policy-properties-tab/group-or-policy-properties-tab.component.html @@ -18,7 +18,7 @@ <header tooltip="Properties">Properties</header> <content> <ul> - <li *ngFor="let property of properties; let i = index" + <li *ngFor="let property of component.properties; let i = index" class="i-sdc-designer-sidebar-section-content-item-property-and-attribute" data-tests-id="propertyRow"> <div class="i-sdc-designer-sidebar-section-content-item-property-and-attribute-label hand" [attr.data-tests-id]="'propertyName_'+property.name" @@ -32,7 +32,7 @@ </li> </ul> - <div *ngIf="properties.length===0" class="component-details-panel-tab-no-data"> + <div *ngIf="!component.properties || component.properties.length===0" class="component-details-panel-tab-no-data"> <div class="component-details-panel-tab-no-data-title">No properties to display</div> </div> </content> diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-properties-tab.component.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/group-or-policy-properties-tab/group-or-policy-properties-tab.component.ts index 5862135df2..24ae8b2833 100644 --- a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-properties-tab.component.ts +++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/group-or-policy-properties-tab/group-or-policy-properties-tab.component.ts @@ -19,44 +19,32 @@ */ import * as _ from "lodash"; -import { Component, Inject, Input, Output, EventEmitter, OnChanges, SimpleChanges } from "@angular/core"; +import { Component, Inject, Input} from "@angular/core"; import { TranslateService } from './../../../../../shared/translator/translate.service'; import { PolicyInstance } from 'app/models/graph/zones/policy-instance'; -import { PropertyBEModel } from 'app/models'; import { PropertyModel } from './../../../../../../models/properties'; import { ModalsHandler } from "app/utils"; -import { Component as TopologyTemplate, ComponentInstance, IAppMenu } from "app/models"; +import { Component as TopologyTemplate, GroupInstance } from "app/models"; @Component({ - selector: 'policy-properties-tab', - templateUrl: './policy-properties-tab.component.html', - styleUrls: ['./../base/base-tab.component.less', 'policy-properties-tab.component.less'], - host: {'class': 'component-details-panel-tab-policy-properties'} + selector: 'group-or-policy-properties-tab', + templateUrl: './group-or-policy-properties-tab.component.html', + styleUrls: ['./../properties-tab/properties-tab.component.less'], }) -export class PolicyPropertiesTabComponent implements OnChanges { +export class GroupOrPolicyPropertiesTab { - @Input() policy:PolicyInstance; + @Input() component: GroupInstance | PolicyInstance; @Input() topologyTemplate:TopologyTemplate; @Input() isViewOnly: boolean; + @Input() input: {type: string}; - private properties:Array<PropertyModel>; constructor(private translateService:TranslateService, private ModalsHandler:ModalsHandler) { } - ngOnChanges(changes: SimpleChanges): void { - console.log("PolicyPropertiesTabComponent: ngAfterViewInit: "); - console.log("policy: " + this.policy); - this.properties = []; - this.initProperties(); - } - - initProperties = ():void => { - this.properties= this.policy.properties; - } editProperty = (property?:PropertyModel):void => { - this.ModalsHandler.openEditPropertyModal((property ? property : new PropertyModel()), this.topologyTemplate, this.properties, false, 'policy', this.policy.uniqueId).then((updatedProperty:PropertyModel) => { + this.ModalsHandler.openEditPropertyModal((property ? property : new PropertyModel()), this.topologyTemplate, this.component.properties, false, this.input.type, this.component.uniqueId).then((updatedProperty:PropertyModel) => { console.log("ok"); }); } diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-information-tab.component.html b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-information-tab.component.html deleted file mode 100644 index 953b57bda1..0000000000 --- a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-information-tab.component.html +++ /dev/null @@ -1,47 +0,0 @@ -<!-- - ~ Copyright (C) 2018 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. - --> - - -<ng2-expand-collapse state="0"> - - <header tooltip="General Information">General Info</header> - - <content> - <!-- CATEGORY --> - <div class="component-details-panel-item"> - <span class="name" [innerHTML]="'GENERAL_LABEL_CATEGORY' | translate"></span> - <span class="value" data-tests-id="rightTab_category" tooltip="Group">Group</span> - </div> - - <!-- SUB CATEGORY --> - <div class="component-details-panel-item"> - <span class="name" [innerHTML]="'GENERAL_LABEL_SUB_CATEGORY' | translate"></span> - <span class="value" data-tests-id="rightTab_subCategory" tooltip="Group">Group</span> - </div> - - <!-- VERSION --> - <div class="component-details-panel-item"> - <span class="name" [innerHTML]="'GENERAL_LABEL_VERSION' | translate"></span> - <span class="value" data-tests-id="rightTab_version" tooltip="{{group.version}}">{{group.version}}</span> - </div> - - <!-- DESCRIPTION --> - <div class="component-details-panel-item description"> - <span class="name" [innerHTML]="'GENERAL_LABEL_DESCRIPTION' | translate"></span> - <span class="value" ellipsis="group.description" max-chars="55" data-tests-id="rightTab_description">{{group.description}}</span> - </div> - </content> -</ng2-expand-collapse> diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-members-tab.component.less b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-members-tab.component.less deleted file mode 100644 index 1006e864fa..0000000000 --- a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-members-tab.component.less +++ /dev/null @@ -1,13 +0,0 @@ -/deep/ -.component-details-panel-tab-group-members { - .component-details-panel-large-item { - display: flex; - flex-direction: row; - justify-content: space-between; - } - - .w-sdc-designer-sidebar-section-title { - display: flex; - justify-content: space-between; - } -}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-members-tab.component.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-members-tab.component.ts deleted file mode 100644 index 148f2133e8..0000000000 --- a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-members-tab.component.ts +++ /dev/null @@ -1,133 +0,0 @@ -/*- - * ============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========================================================= - */ - -import * as _ from "lodash"; -import { Component, Input, Output, EventEmitter, OnChanges, HostBinding } from "@angular/core"; -import { TranslateService } from './../../../../../shared/translator/translate.service'; -import { Component as TopologyTemplate } from "app/models"; -import { GroupInstance } from "app/models/graph/zones/group-instance"; -import { GroupsService } from "../../../../../services/groups.service"; -import { SimpleChanges } from "@angular/core/src/metadata/lifecycle_hooks"; -import { MemberUiObject } from "../../../../../../models/ui-models/ui-member-object"; -import { IModalConfig } from "sdc-ui/lib/angular/modals/models/modal-config"; -import { AddElementsComponent } from "../../../../../components/ui/modal/add-elements/add-elements.component"; -import { GRAPH_EVENTS } from 'app/utils'; -import { EventListenerService } from 'app/services/event-listener-service'; -import { ComponentInstance } from "../../../../../../models/componentsInstances/componentInstance"; -import { SdcUiComponents } from "sdc-ui/lib/angular"; - -@Component({ - selector: 'group-members-tab', - templateUrl: './group-members-tab.component.html', - styleUrls: ['./../base/base-tab.component.less', 'group-members-tab.component.less'] -}) - -export class GroupMembersTabComponent implements OnChanges { - - - private members: Array<MemberUiObject>; - - @Input() group: GroupInstance; - @Input() topologyTemplate: TopologyTemplate; - @Input() isViewOnly: boolean; - @Output() isLoading: EventEmitter<boolean> = new EventEmitter<boolean>(); - @HostBinding('class') classes = 'component-details-panel-tab-group-members'; - - constructor(private translateService: TranslateService, - private groupsService: GroupsService, - private modalService: SdcUiComponents.ModalService, - private eventListenerService: EventListenerService - ) { - this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_GROUP_INSTANCE_UPDATE, this.initMembers) - } - - ngOnChanges(changes:SimpleChanges):void { - this.initMembers(); - } - - deleteMember = (member: MemberUiObject):void => { - this.isLoading.emit(true); - this.groupsService.deleteGroupMember(this.topologyTemplate.componentType, this.topologyTemplate.uniqueId, this.group, member.uniqueId).subscribe( - (updatedMembers:Array<string>) => { - this.group.members = updatedMembers; - this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_GROUP_INSTANCE_UPDATE, this.group); - }, - error => console.log("Error deleting member!"), - () => this.isLoading.emit(false) - ); - } - - private initMembers = (groupInstance?: GroupInstance) => { - this.group = groupInstance ? groupInstance : this.group; - this.members = this.group.getMembersAsUiObject(this.topologyTemplate.componentInstances); - } - - addMembers = ():void => { - var membersToAdd:Array<MemberUiObject> = this.modalService.getCurrentInstance().innerModalContent.instance.existingElements; //TODO refactor sdc-ui modal in order to return the data - if(membersToAdd.length > 0) { - this.modalService.closeModal(); - this.isLoading.emit(true); - var updatedMembers: Array<MemberUiObject> = _.union(this.members, membersToAdd); - this.groupsService.updateMembers(this.topologyTemplate.componentType, this.topologyTemplate.uniqueId, this.group.uniqueId, updatedMembers).subscribe( - (updatedMembers:Array<string>) => { - this.group.members = updatedMembers; - this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_GROUP_INSTANCE_UPDATE, this.group); - }, - error => { - console.log("Error updating members!"); - }, () => - this.isLoading.emit(false) - ); - } - } - - getOptionalsMembersToAdd():Array<MemberUiObject> { - - let optionalsMembersToAdd:Array<MemberUiObject> = []; - - // adding all instances as optional members to add if not already exist - _.forEach(this.topologyTemplate.componentInstances, (instance:ComponentInstance) => { - if (!_.some(this.members, (member:MemberUiObject) => { - return member.uniqueId === instance.uniqueId - })) { - optionalsMembersToAdd.push(new MemberUiObject(instance.uniqueId, instance.name)); - } - }); - return optionalsMembersToAdd; - } - - openAddMembersModal():void { - let addMembersModalConfig:IModalConfig = { - title: this.group.name + " ADD MEMBERS", - size: "md", - type: "custom", - testId: "addMembersModal", - buttons: [ - {text: 'ADD MEMBERS', size: 'xsm', callback: this.addMembers, closeModal: false}, - {text: 'CANCEL', size: 'sm', type: "secondary", closeModal: true} - ] - }; - var optionalsMembersToAdd = this.getOptionalsMembersToAdd(); - this.modalService.openCustomModal(addMembersModalConfig, AddElementsComponent, { - elementsToAdd: optionalsMembersToAdd, - elementName: "member" - }); - } -} diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-properties-tab.component.html b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-properties-tab.component.html deleted file mode 100644 index fe1f6b4f0d..0000000000 --- a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-properties-tab.component.html +++ /dev/null @@ -1,39 +0,0 @@ -<!-- - ~ Copyright (C) 2018 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. - --> - -<ng2-expand-collapse state="0"> - <header tooltip="Properties">Properties</header> - <content> - <ul> - <li *ngFor="let property of properties; let i = index" - class="i-sdc-designer-sidebar-section-content-item-property-and-attribute" data-tests-id="propertyRow"> - <div class="i-sdc-designer-sidebar-section-content-item-property-and-attribute-label hand" - [attr.data-tests-id]="'propertyName_'+property.name" - tooltip="{{property.name}}" - (click)="!isViewOnly && editProperty(property)">{{property.name}} - </div> - <div class="i-sdc-designer-sidebar-section-content-item-property-value" - [attr.data-tests-id]="'value_'+property.name" - tooltip="{{property.value || property.defaultValue}}">{{property.value || property.defaultValue}} - </div> - </li> - </ul> - - <div *ngIf="properties.length===0" class="component-details-panel-tab-no-data"> - <div class="component-details-panel-tab-no-data-title">No properties to display</div> - </div> - </content> -</ng2-expand-collapse> diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-properties-tab.component.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-properties-tab.component.ts deleted file mode 100644 index 69079347c4..0000000000 --- a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-properties-tab.component.ts +++ /dev/null @@ -1,64 +0,0 @@ -/*- - * ============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========================================================= - */ - -import * as _ from "lodash"; -import { Component, Inject, Input, Output, EventEmitter, OnChanges, SimpleChanges } from "@angular/core"; -import { TranslateService } from './../../../../../shared/translator/translate.service'; -import { GroupInstance } from 'app/models/graph/zones/group-instance'; -import { PropertyBEModel } from 'app/models'; -import { PropertyModel } from './../../../../../../models/properties'; -import { ModalsHandler } from "app/utils"; -import { Component as TopologyTemplate, ComponentInstance, IAppMenu } from "app/models"; - -@Component({ - selector: 'group-properties-tab', - templateUrl: './group-properties-tab.component.html', - styleUrls: ['./../base/base-tab.component.less', 'group-properties-tab.component.less'], - host: {'class': 'component-details-panel-tab-group-properties'} -}) -export class GroupPropertiesTabComponent implements OnChanges { - - @Input() group:GroupInstance; - @Input() topologyTemplate:TopologyTemplate; - @Input() isViewOnly: boolean; - - private properties:Array<PropertyModel>; - - constructor(private translateService:TranslateService, private ModalsHandler:ModalsHandler) { - } - - ngOnChanges(changes: SimpleChanges): void { - console.log("GroupPropertiesTabComponent: ngAfterViewInit: "); - console.log("group: " + JSON.stringify(this.group)); - this.properties = []; - this.initProperties(); - } - - initProperties = ():void => { - this.properties= this.group.properties; - } - - editProperty = (property?:PropertyModel):void => { - this.ModalsHandler.openEditPropertyModal((property ? property : new PropertyModel()), this.topologyTemplate, this.properties, false, 'group', this.group.uniqueId).then((updatedProperty:PropertyModel) => { - console.log("ok"); - }); - } - -} diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-tabs.component.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-tabs.component.ts deleted file mode 100644 index 975d5c6153..0000000000 --- a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-tabs.component.ts +++ /dev/null @@ -1,67 +0,0 @@ -/*- - * ============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========================================================= - */ - -import * as _ from "lodash"; -import { Component, Inject, Input, Output, EventEmitter, SimpleChanges, OnChanges } from "@angular/core"; -import { TranslateService } from './../../../../../shared/translator/translate.service'; -import { Component as TopologyTemplate, ComponentInstance, IAppMenu } from "app/models"; -import { GroupsService } from '../../../../../services/groups.service'; -import { GroupInstance } from "app/models/graph/zones/group-instance"; - -@Component({ - selector: 'group-tabs', - templateUrl: './group-tabs.component.html' -}) -export class GroupTabsComponent implements OnChanges { - - @Input() topologyTemplate:TopologyTemplate; - @Input() selectedZoneInstanceType:string; - @Input() selectedZoneInstanceId:string; - @Input() isViewOnly: boolean; - @Output() isLoading: EventEmitter<boolean> = new EventEmitter<boolean>(); - - private group:GroupInstance; - - constructor(private translateService:TranslateService, - private groupsService:GroupsService - ) { - } - - ngOnChanges(changes: SimpleChanges): void { - this.initGroup(); - } - - private initGroup = ():void => { - this.isLoading.emit(true); - this.groupsService.getSpecificGroup(this.topologyTemplate.componentType, this.topologyTemplate.uniqueId, this.selectedZoneInstanceId).subscribe( - group => { - this.group = group; - console.log(JSON.stringify(group)); - }, - error => console.log("Error getting group!"), - () => this.isLoading.emit(false) - ); - } - - private setIsLoading = (value) :void => { - this.isLoading.emit(value); - } - -} diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-tabs.module.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-tabs.module.ts deleted file mode 100644 index 50797f862c..0000000000 --- a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-tabs.module.ts +++ /dev/null @@ -1,71 +0,0 @@ -/*- - * ============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========================================================= - */ -import { NgModule } from "@angular/core"; -import { HttpModule } from "@angular/http"; -import { FormsModule } from "@angular/forms"; -import { BrowserModule } from "@angular/platform-browser"; -import { UiElementsModule } from 'app/ng2/components/ui/ui-elements.module'; -import { ExpandCollapseComponent } from 'app/ng2/components/ui/expand-collapse/expand-collapse.component'; -import { PoliciesService } from "../../../../../services/policies.service"; -import { GroupInformationTabComponent } from './group-information-tab.component'; -import { TooltipModule } from './../../../../../components/ui/tooltip/tooltip.module'; -import { GroupTabsComponent } from "./group-tabs.component"; -import { SdcUiComponentsModule } from "sdc-ui/lib/angular"; -import { GroupMembersTabComponent } from './group-members-tab.component'; -import { TranslateModule } from './../../../../../shared/translator/translate.module'; -import { GroupPropertiesTabComponent } from "./group-properties-tab.component"; - -@NgModule({ - declarations: [ - GroupInformationTabComponent, - GroupMembersTabComponent, - GroupTabsComponent, - GroupPropertiesTabComponent - ], - imports: [ - BrowserModule, - FormsModule, - HttpModule, - TooltipModule, - UiElementsModule, - SdcUiComponentsModule, - TranslateModule - ], - entryComponents: [ - GroupInformationTabComponent, - GroupMembersTabComponent, - GroupTabsComponent, - GroupPropertiesTabComponent, - ExpandCollapseComponent - ], - exports: [ - TooltipModule, - GroupInformationTabComponent, - GroupMembersTabComponent, - GroupTabsComponent, - GroupPropertiesTabComponent - ], - providers: [ - PoliciesService - ] -}) -export class GroupTabsModule { - -} diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/info-tab/__snapshots__/info-tab.component.spec.ts.snap b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/info-tab/__snapshots__/info-tab.component.spec.ts.snap new file mode 100644 index 0000000000..fdd0dcf75c --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/info-tab/__snapshots__/info-tab.component.spec.ts.snap @@ -0,0 +1,66 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`InfoTabComponent can load instance 1`] = ` +<panel-info-tab + componentInstanceService={[Function Object]} + compositionPaletteService={[Function Object]} + compositionService={[Function Object]} + eventListenerService={[Function Object]} + flatLeftPaletteElementsFromService={[Function Function]} + getPathNamesVersionChangeModal={[Function Function]} + initEditResourceVersion={[Function Function]} + modalService={[Function Object]} + onChangeVersion={[Function Function]} + sdcMenu={[Function Object]} + serviceService={[Function Object]} + store={[Function Object]} + versioning={[Function Function]} + workspaceService={[Function Object]} +> + <ng2-expand-collapse + state="0" + > + <header + tooltip="General Information" + > + General Info + </header> + <content + class="general-info-container" + > + + + <div + class="component-details-panel-item" + > + <span + class="name" + /> + + + </div> + + + + + + + + + + + + <div + class="component-details-panel-item description" + > + <span + class="name" + /> + <chars-ellipsis /> + </div> + + + </content> + </ng2-expand-collapse> +</panel-info-tab> +`; diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/info-tab/info-tab.component.html b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/info-tab/info-tab.component.html new file mode 100644 index 0000000000..71545f8143 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/info-tab/info-tab.component.html @@ -0,0 +1,174 @@ +<!-- + ~ Copyright (C) 2018 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. + --> + +<ng2-expand-collapse state="0"> + <header tooltip="General Information">General Info</header> + <content class="general-info-container"> + <!-- TYPE --> + <div class="component-details-panel-item" *ngIf="component.componentType"> + <span class="name" [innerHTML]="'Type:'"></span> + <span class="value" data-tests-id="rightTab_componentType" tooltip="{{component.componentType}}">{{component.componentType}}</span> + </div> + + <!-- RESOURCE TYPE--> + <div class="component-details-panel-item" *ngIf="component.resourceType"> + <span class="name" [innerHTML]="'Resource Type:'"></span> + <span class="value" data-tests-id="rightTab_resourceType" tooltip="{{component.resourceType}}">{{component.resourceType}}</span> + </div> + + <!-- VERSION --> + <div class="component-details-panel-item" > + <span class="name" [innerHTML]="'GENERAL_LABEL_VERSION' | translate"></span> + <span class="value" *ngIf="!isComponentSelectedFlag" data-tests-id="rightTab_version" tooltip="{{component.version}}">{{component.version}}</span> + <ng-container *ngIf="isComponentSelectedFlag"> + <select #versionDropdown (change)="onChangeVersion(versionDropdown)" [ngModel]="component.getComponentUid()" data-tests-id="changeVersion"> + <option *ngFor="let version of versions" value="{{version.value}}" + [disabled]="isDisabledFlag" [class.minor]="(component.componentVersion)%1" + >{{version.label}}</option> + </select> + </ng-container> + </div> + + <!-- CATEGORY --> + <ng-container *ngIf="component.categories && component.categories[0]"> + <div class="component-details-panel-item"> + <span class="name" [innerHTML]="'GENERAL_LABEL_CATEGORY' | translate"></span> + <span class="value" data-tests-id="rightTab_category" tooltip="{{component.categories[0].name}}">{{component.categories[0].name}}</span> + </div> + + <!-- SUB CATEGORY --> + <div class="component-details-panel-item" *ngIf="component.categories[0].subcategories && component.categories[0].subcategories[0]"> + <span class="name" [innerHTML]="'GENERAL_LABEL_SUB_CATEGORY' | translate"></span> + <span class="value" data-tests-id="rightTab_subCategory" tooltip="{{component.categories[0].subcategories[0].name}}">{{component.categories[0].subcategories[0].name}}</span> + </div> + </ng-container> + + <!-- CREATION DATE --> + <div class="component-details-panel-item" *ngIf="component.creationDate"> + <span class="name" [innerHTML]="'Creation Date:'"></span> + <span class="value" data-tests-id="rightTab_version" tooltip="{{component.creationDate | date: 'MM/dd/yyyy'}}">{{component.creationDate | date: 'MM/dd/yyyy'}}</span> + </div> + + <!-- AUTHOR --> + <div class="component-details-panel-item" *ngIf="component.creatorFullName"> + <span class="name" [innerHTML]="'Author:'"></span> + <span class="value" data-tests-id="rightTab_author" tooltip="{{component.creatorFullName}}">{{component.creatorFullName}}</span> + </div> + + <!-- Vendor Name data-ng-if="selectedComponent.isResource()"--> + <div class="component-details-panel-item" *ngIf="component.vendorName"> + <span class="name" [innerHTML]="'Vendor Name:'"></span> + <span class="value" data-tests-id="rightTab_vendorName" tooltip="{{component.vendorName}}">{{component.vendorName}}</span> + </div> + + <!-- Vendor Release data-ng-if="selectedComponent.isResource()"--> + <div class="component-details-panel-item" *ngIf="component.vendorRelease"> + <span class="name" [innerHTML]="'Vendor Release:'"></span> + <span class="value" data-tests-id="rightTab_vendorRelease" tooltip="{{component.vendorRelease}}">{{component.vendorRelease}}</span> + </div> + + <!-- Vendor Release data-ng-if="selectedComponent.isResource()"--> + <div class="component-details-panel-item" *ngIf="component.resourceVendorModelNumber"> + <span class="name" [innerHTML]="'GENERAL_LABEL_RESOURCE_MODEL_NUMBER' | translate"></span> + <span class="value" data-tests-id="rightTab_resourceVendorModelNumber" tooltip="{{component.resourceVendorModelNumber}}">{{component.resourceVendorModelNumber}}</span> + </div> + + <!-- Service Type data-ng-if="selectedComponent.isService()"--> + <div class="component-details-panel-item" *ngIf="component.serviceType"> + <span class="name" [innerHTML]="'GENERAL_LABEL_SERVICE_TYPE' | translate"></span> + <span class="value" data-tests-id="rightTab_serviceType" tooltip="{{component.serviceType}}">{{component.serviceType}}</span> + </div> + + <!-- Service Role data-ng-if="selectedComponent.isService()"--> + <div class="component-details-panel-item" *ngIf="component.serviceRole"> + <span class="name" [innerHTML]="'GENERAL_LABEL_SERVICE_ROLE' | translate"></span> + <span class="value" data-tests-id="rightTab_serviceRole" tooltip="{{component.serviceRole}}">{{component.serviceRole}}</span> + </div> + + <!-- Contact ID --> + <div class="component-details-panel-item" *ngIf="component.contactId"> + <span class="name" [innerHTML]="'GENERAL_LABEL_CONTACT_ID' | translate"></span> + <span class="value" data-tests-id="rightTab_contactId" tooltip="{{component.contactId}}">{{component.contactId}}</span> + </div> + + <!-- Service Name data-ng-if="isComponentInstanceSelected() && currentComponent.selectedInstance.isServiceProxy()"--> + <div class="component-details-panel-item" *ngIf="component.sourceModelName"> + <span class="name" [innerHTML]="'GENERAL_LABEL_SOURCE_SERVICE_NAME' | translate"></span> + <span class="value" data-tests-id="rightTab_sourceModelName" tooltip="{{component.sourceModelName}}">{{component.sourceModelName}}</span> + </div> + + <!-- Customization UUID data-ng-if="isViewMode() && currentComponent.isService() && selectedComponent.isResource()"--> + <div class="component-details-panel-item" *ngIf="component.customizationUUID"> + <span class="name" [innerHTML]="'GENERAL_LABEL_RESOURCE_CUSTOMIZATION_UUID' | translate"></span> + <span class="value" data-tests-id="rightTab_customizationModuleUUID" tooltip="{{component.customizationUUID}}">{{component.customizationUUID}}</span> + </div> + + <!-- DESCRIPTION --> + <div class="component-details-panel-item description"> + <span class="name" [innerHTML]="'GENERAL_LABEL_DESCRIPTION' | translate"></span> + <chars-ellipsis [text]="component.description" [maxChars]="55" [testId]="'rightTab_description'"></chars-ellipsis> + </div> + + + <!--TODO: move to separate component!--> + <ng-container *ngIf="componentType == 'POLICY'"> + <!-- TYPE --> + <div class="component-details-panel-item policy-item"> + <span class="name" [innerHTML]="'GENERAL_LABEL_TYPE' | translate"></span> + <span class="value" data-tests-id="rightTab_componentType" tooltip="{{component.policyTypeUid}}">{{component.policyTypeUid}}</span> + </div> + + <!-- CATEGORY --> + <div class="component-details-panel-item policy-item"> + <span class="name" [innerHTML]="'GENERAL_LABEL_CATEGORY' | translate"></span> + <span class="value" data-tests-id="rightTab_category" tooltip="Policy">Policy</span> + </div> + + <!-- SUB CATEGORY --> + <div class="component-details-panel-item policy-item"> + <span class="name" [innerHTML]="'GENERAL_LABEL_SUB_CATEGORY' | translate"></span> + <span class="value" data-tests-id="rightTab_subCategory" tooltip="Policy">Policy</span> + </div> + </ng-container> + + <!--TODO: move to separate component!--> + <ng-container *ngIf="componentType == 'GROUP'"> + <!-- CATEGORY --> + <div class="component-details-panel-item group-item"> + <span class="name" [innerHTML]="'GENERAL_LABEL_CATEGORY' | translate"></span> + <span class="value" data-tests-id="rightTab_category" tooltip="Group">Group</span> + </div> + + <!-- SUB CATEGORY --> + <div class="component-details-panel-item group-item"> + <span class="name" [innerHTML]="'GENERAL_LABEL_SUB_CATEGORY' | translate"></span> + <span class="value" data-tests-id="rightTab_subCategory" tooltip="Group">Group</span> + </div> + + </ng-container> + + </content> +</ng2-expand-collapse> + +<ng2-expand-collapse *ngIf="component.tags || isComponentInstanceSelected()"> + <header tooltip="Tags">Tags</header> + <content class="tags-container"> + <span *ngIf="component.tags?.indexOf(component.name)===-1" class="i-sdc-designer-sidebar-section-content-item-tag" + data-tests-id="rightTab_tag" tooltip="{{component.name}}">{{component.name}}</span> + <span class="i-sdc-designer-sidebar-section-content-item-tag" *ngFor="let tag of component.tags" + data-tests-id="rightTab_tag" tooltip="{{tag}}">{{tag}}</span> + </content> +</ng2-expand-collapse> diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/info-tab/info-tab.component.less b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/info-tab/info-tab.component.less new file mode 100644 index 0000000000..c8da4e3e68 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/info-tab/info-tab.component.less @@ -0,0 +1,51 @@ +@import '../../../../../../../assets/styles/variables'; + +.general-info-container { + display: flex; + flex-direction: column; + padding: 10px 20px; +} + +.component-details-panel-item { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + margin-bottom: 5px; + order:1; + + .name { font-family: OpenSans-Semibold, sans-serif; } + .value { padding-left: 10px; } + + + &.description { + margin-top: 28px; + white-space: normal; + word-wrap: break-word; + overflow: ellipsis; + + .value { + padding-left: 0; + max-width: none; + font-weight: normal; + font-family: @font-opensans-regular; + } + } + + &.group-item, &.policy-item { + order:0; + } +} + +.tags-container { + display: flex; + flex-wrap: wrap; + padding: 10px 20px; + + .i-sdc-designer-sidebar-section-content-item-tag { + padding: 5px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + user-select: all; + } +} diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/info-tab/info-tab.component.spec.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/info-tab/info-tab.component.spec.ts new file mode 100644 index 0000000000..6915d651f1 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/info-tab/info-tab.component.spec.ts @@ -0,0 +1,98 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { Store } from '@ngxs/store'; +import { CompositionPaletteService } from '../../../../../pages/composition/palette/services/palette.service'; +import { IAppMenu, SdcMenuToken } from '../../../../../../../app/ng2/config/sdc-menu.config'; +import { CompositionService } from '../../../../../pages/composition/composition.service'; +import { ServiceServiceNg2 } from '../../../../../../../app/services-ng2'; +import { WorkspaceService } from '../../../../../../../app/ng2/pages/workspace/workspace.service'; +import { ComponentInstanceServiceNg2 } from '../../../../../../../app/ng2/services/component-instance-services/component-instance.service'; +import { EventListenerService } from '../../../../../../../app/services'; +import { InfoTabComponent } from './info-tab.component'; +import { ConfigureFn, configureTests } from "../../../../../../../jest/test-config.helper"; +import { Observable } from "rxjs"; +import { leftPaletteElements } from "../../../../../../../jest/mocks/left-paeltte-elements.mock"; +import { TranslatePipe } from "../../../../../shared/translator/translate.pipe"; +import { HttpClientModule } from "@angular/common/http"; +import { TranslateModule } from "../../../../../../../app/ng2/shared/translator/translate.module"; +import _ from "lodash"; +import { TranslateService } from "../../../../../shared/translator/translate.service"; +import { SdcUiServices } from "onap-ui-angular"; +import { Component as TopologyTemplate, FullComponentInstance, ComponentInstance } from '../../../../../../../app/models'; + + +describe('InfoTabComponent', () => { + // let comp: InfoTabComponent; + let fixture: ComponentFixture<InfoTabComponent>; + + // let eventServiceMock: Partial<EventListenerService>; + let storeStub:Partial<Store>; + let compositionPaletteServiceStub:Partial<CompositionPaletteService>; + let iAppMenuStub:Partial<IAppMenu>; + let compositionServiceStub:Partial<CompositionService>; + let serviceServiceNg2Stub:Partial<ServiceServiceNg2>; + let workspaceServiceStub:Partial<WorkspaceService>; + let componentInstanceServiceNg2Stub:Partial<ComponentInstanceServiceNg2>; + let eventListenerServiceStub:Partial<EventListenerService>; + + beforeEach( + async(() => { + storeStub = {}; + iAppMenuStub = {}; + eventListenerServiceStub = { + notifyObservers: jest.fn() + } + compositionPaletteServiceStub = { + getLeftPaletteElements: jest.fn().mockImplementation(()=> Observable.of(leftPaletteElements)) + } + const configure: ConfigureFn = testBed => { + testBed.configureTestingModule({ + imports: [ ], + declarations: [ InfoTabComponent, TranslatePipe ], + schemas: [ NO_ERRORS_SCHEMA ], + providers: [ + { provide: Store, useValue: {} }, + { provide: CompositionPaletteService, useValue: compositionPaletteServiceStub }, + { provide: SdcMenuToken, useValue: {} }, + { provide: CompositionService, useValue: {} }, + { provide: SdcUiServices.ModalService, useValue: {}}, + { provide: ServiceServiceNg2, useValue: {} }, + { provide: WorkspaceService, useValue: {} }, + { provide: ComponentInstanceServiceNg2, useValue: {} }, + { provide: EventListenerService, useValue: eventListenerServiceStub }, + { provide: TranslateService, useValue: {}} + ] + }); + }; + + configureTests(configure).then(testBed => { + fixture = testBed.createComponent(InfoTabComponent); + let comp = fixture.componentInstance; + + }); + }) + ); + + + it('can load instance', () => { + expect(fixture).toMatchSnapshot(); + }); + + describe('Version dropdown', () => { + it('is undefined for topologyTemplate', () => { + fixture.componentInstance.component = <TopologyTemplate>{}; + fixture.componentInstance.initEditResourceVersion(fixture.componentInstance.component, fixture.componentInstance.flatLeftPaletteElementsFromService(leftPaletteElements)); + expect(fixture.componentInstance.versions).toBe(undefined); + }); + it('does not contain the highest minor version if it is checked out', () => { + fixture.componentInstance.component = new ComponentInstance(); + fixture.componentInstance.component.allVersions = + {'1.0': "9c829122-af05-4bc9-b537-5d84f4c8ae25", '1.1': "930d56cb-868d-4e35-bd0f-e737d2fdb171"}; + fixture.componentInstance.component.version = "1.0"; + fixture.componentInstance.component.uuid = "a8cf015e-e4e5-4d4b-a01e-8624e8d36095"; + fixture.componentInstance.initEditResourceVersion(fixture.componentInstance.component, fixture.componentInstance.flatLeftPaletteElementsFromService(leftPaletteElements)); + expect(fixture.componentInstance.versions).toHaveLength(1); + }); + }); + +}); diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/info-tab/info-tab.component.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/info-tab/info-tab.component.ts new file mode 100644 index 0000000000..45f31e7b35 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/info-tab/info-tab.component.ts @@ -0,0 +1,189 @@ +import { Component, OnInit, Input, Inject, OnDestroy } from '@angular/core'; +import { + PolicyInstance, + GroupInstance, + Component as TopologyTemplate, + ComponentInstance, + LeftPaletteComponent, + FullComponentInstance +} from "app/models"; +import {Store} from "@ngxs/store"; +import { EVENTS, GRAPH_EVENTS } from 'app/utils'; +import {IDropDownOption} from "onap-ui-angular/dist/form-elements/dropdown/dropdown-models"; +import { CompositionPaletteService } from "app/ng2/pages/composition/palette/services/palette.service"; +import { SdcUiCommon, SdcUiComponents, SdcUiServices } from "onap-ui-angular"; +import { SdcMenuToken, IAppMenu } from "app/ng2/config/sdc-menu.config"; +import { CompositionService } from "app/ng2/pages/composition/composition.service"; +import { ServiceServiceNg2 } from "app/services-ng2"; +import { WorkspaceService } from "app/ng2/pages/workspace/workspace.service"; +import { ComponentInstanceServiceNg2 } from "app/ng2/services/component-instance-services/component-instance.service"; +import { EventListenerService } from "app/services"; +import * as _ from 'lodash'; +import {SelectedComponentType, TogglePanelLoadingAction} from "../../../common/store/graph.actions"; +import Dictionary = _.Dictionary; + + +@Component({ + selector: 'panel-info-tab', + templateUrl: './info-tab.component.html', + styleUrls: ['./info-tab.component.less'], + // providers: [SdcUiServices.ModalService] +}) +export class InfoTabComponent implements OnInit, OnDestroy { + + @Input() isViewOnly: boolean; + @Input() componentType: SelectedComponentType; + @Input() component: TopologyTemplate | PolicyInstance | GroupInstance | ComponentInstance; + public versions: IDropDownOption[]; + private leftPalletElements: LeftPaletteComponent[]; + private isDisabledFlag: boolean; + private isComponentSelectedFlag: boolean; + + constructor(private store: Store, + private compositionPaletteService: CompositionPaletteService, + private compositionService: CompositionService, + private workspaceService: WorkspaceService, + private modalService: SdcUiServices.ModalService, + private componentInstanceService: ComponentInstanceServiceNg2, + private serviceService: ServiceServiceNg2, + private eventListenerService: EventListenerService, + @Inject(SdcMenuToken) public sdcMenu:IAppMenu) { + } + + ngOnInit() { + this.leftPalletElements = this.flatLeftPaletteElementsFromService(this.compositionPaletteService.getLeftPaletteElements()); + this.initEditResourceVersion(this.component, this.leftPalletElements); + this.eventListenerService.registerObserverCallback(EVENTS.ON_CHECKOUT, (comp) => { + this.component = comp; + }); + this.isComponentSelectedFlag = this.isComponentInstanceSelected(); + this.isDisabledFlag = this.isDisabled(); + + } + + ngOnDestroy() { + this.eventListenerService.unRegisterObserver(EVENTS.ON_CHECKOUT); + } + + flatLeftPaletteElementsFromService = (leftPalleteElementsFromService: Dictionary<Dictionary<LeftPaletteComponent[]>>): LeftPaletteComponent[] => { + let retValArr = []; + for (const category in leftPalleteElementsFromService) { + for (const subCategory in leftPalleteElementsFromService[category]) { + retValArr = retValArr.concat(leftPalleteElementsFromService[category][subCategory].slice(0)); + } + } + return retValArr; + } + + private isComponentInstanceSelected () { + return this.componentType === SelectedComponentType.COMPONENT_INSTANCE; + } + + private versioning: Function = (versionNumber: string): string => { + let version: Array<string> = versionNumber && versionNumber.split('.'); + return '00000000'.slice(version[0].length) + version[0] + '.' + '00000000'.slice(version[1].length) + version[1]; + }; + + + private onChangeVersion = (versionDropdown) => { + let newVersionValue = versionDropdown.value; + versionDropdown.value = (<FullComponentInstance>this.component).getComponentUid(); + + this.store.dispatch(new TogglePanelLoadingAction({isLoading: true})); + + // let service = <Service>this.$scope.currentComponent; + if(this.component instanceof FullComponentInstance) { + + let onCancel = (error:any) => { + this.store.dispatch(new TogglePanelLoadingAction({isLoading: false})); + if (error) { + console.log(error); + } + }; + + let onUpdate = () => { + //this function will update the instance version than the function call getComponent to update the current component and return the new instance version + this.componentInstanceService.changeResourceInstanceVersion(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId, this.component.uniqueId, newVersionValue) + .subscribe((component) => { + this.store.dispatch(new TogglePanelLoadingAction({isLoading: false})); + this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_VERSION_CHANGED, component); + }, onCancel); + }; + + if (this.component.isService() || this.component.isServiceProxy()) { + this.serviceService.checkComponentInstanceVersionChange(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId, + this.component.uniqueId, newVersionValue).subscribe((pathsToDelete:string[]) => { + if (pathsToDelete && pathsToDelete.length) { + this.store.dispatch(new TogglePanelLoadingAction({isLoading: false})); + + + const {title, message} = this.sdcMenu.alertMessages['upgradeInstance']; + let pathNames:string = this.getPathNamesVersionChangeModal(pathsToDelete); + let onOk: Function = () => { + this.store.dispatch(new TogglePanelLoadingAction({isLoading: true})); + + onUpdate(); + }; + const okButton = {testId: "OK", text: "OK", type: SdcUiCommon.ButtonType.info, callback: onOk, closeModal: true} as SdcUiComponents.ModalButtonComponent; + const cancelButton = {testId: "Cancel", text: "Cancel", type: SdcUiCommon.ButtonType.secondary, callback: <Function>onCancel, closeModal: true} as SdcUiComponents.ModalButtonComponent; + const modal = this.modalService.openInfoModal(title, message.format([pathNames]), 'confirm-modal', [okButton, cancelButton]); + modal.getCloseButton().onClick(onCancel); + } else { + onUpdate(); + } + }, onCancel); + } else { + onUpdate(); + } + } + }; + + + private getPathNamesVersionChangeModal = (pathsToDelete:string[]):string => { + const relatedPaths = _.filter(this.compositionService.forwardingPaths, path => + _.find(pathsToDelete, id => + path.uniqueId === id + ) + ).map(path => path.name); + const pathNames = _.join(relatedPaths, ', ') || 'none'; + return pathNames; + }; + + + private initEditResourceVersion = (component, leftPaletteComponents): void => { + if(this.component instanceof ComponentInstance) { + + this.versions = []; + let sorted:any = _.sortBy(_.toPairs(component.allVersions), (item) => { + return item[0] !== "undefined" && this.versioning(item[0]); + }); + _.forEach(sorted, (item) => { + this.versions.push({label: item[0], value: item[1]}); + }); + + let highestVersion = _.last(sorted)[0]; + + if (parseFloat(highestVersion) % 1) { //if highest is minor, make sure it is the latest checked in - + let latestVersionComponent: LeftPaletteComponent = _.maxBy( + _.filter(leftPaletteComponents, (leftPaletteComponent: LeftPaletteComponent) => { //latest checked in + return (leftPaletteComponent.systemName === component.systemName || leftPaletteComponent.uuid === component.uuid); + }) + , (component) => { + return component.version + }); + + let latestVersion: string = latestVersionComponent ? latestVersionComponent.version : highestVersion; + + if (latestVersion && highestVersion != latestVersion) { //highest is checked out - remove from options + this.versions = this.versions.filter(version => version.label != highestVersion); + } + } + } + } + + private isDisabled() { + return this.isViewOnly || this.component['archived'] || this.component['resourceType'] === 'CVFC' + } + +}; + diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/panel-tab.component.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/panel-tab.component.ts new file mode 100644 index 0000000000..c148a4e579 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/panel-tab.component.ts @@ -0,0 +1,55 @@ +import { NgModule, Component, Compiler, ViewContainerRef, ViewChild, Input, ComponentRef, ComponentFactoryResolver, ChangeDetectorRef } from '@angular/core'; +import {Component as TopologyTemplate} from "app/models"; +import { SdcUiServices } from "onap-ui-angular"; + +// Helper component to add dynamic tabs +@Component({ + selector: 'panel-tab', + template: `<div #content></div>` +}) +export class PanelTabComponent { + @ViewChild('content', { read: ViewContainerRef }) content; + @Input() isActive:boolean; + @Input() panelTabType; + @Input() input; + @Input() isViewOnly:boolean; + @Input() component:TopologyTemplate; + @Input() componentType; + cmpRef: ComponentRef<any>; + private isViewInitialized: boolean = false; + + constructor(private componentFactoryResolver: ComponentFactoryResolver, + private cdRef: ChangeDetectorRef) { } + + updateComponent() { + if (!this.isViewInitialized || !this.isActive) { + return; + } + if (this.cmpRef) { + this.cmpRef.destroy(); + } + + let factory = this.componentFactoryResolver.resolveComponentFactory(this.panelTabType); + this.cmpRef = this.content.createComponent(factory); + this.cmpRef.instance.input = this.input; + this.cmpRef.instance.isViewOnly = this.isViewOnly; + this.cmpRef.instance.component = this.component; + this.cmpRef.instance.componentType = this.componentType; + this.cdRef.detectChanges(); + } + + ngOnChanges() { + this.updateComponent(); + } + + ngAfterViewInit() { + this.isViewInitialized = true; + this.updateComponent(); + } + + ngOnDestroy() { + if (this.cmpRef) { + this.cmpRef.destroy(); + } + } +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/panel-tabs.less b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/panel-tabs.less new file mode 100644 index 0000000000..b3c03f85c5 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/panel-tabs.less @@ -0,0 +1,65 @@ +@import '../../../../../../assets/styles/variables'; +@import '../../../../../../assets/styles/override'; + + +// --------------------------------------------------------------------------------------------------- +///* override sdc-ui library tabs */ +// --------------------------------------------------------------------------------------------------- + + +:host ::ng-deep .sdc-tabs { + + .sdc-tabs-list { + display: flex; + border-bottom: 1px solid @sdcui_color_silver; + min-height: min-content; + } + .sdc-tab { + background-color: @sdcui_color_white; + border: 1px solid @sdcui_color_silver; + border-left: none; + border-bottom: none; + height: 36px; + width: 60px; + display: flex; + align-content: center; + justify-content: center; + cursor: pointer; + padding: 0; + margin: 0; + + + &.sdc-tab-active { + background-color: @sdcui_color_silver; + border-bottom: none; + } + &[disabled] { + opacity: 0.3; + cursor: default; + } + } + &.sdc-tabs-header { + .sdc-tab { + font-size: 24px; + } + } + &.sdc-tabs-menu { + .sdc-tab { + font-size: 14px; + padding: 0px 10px 4px 10px; + } + } + .sdc-tab-content { + margin-top: 0; + flex:1; + overflow-y:auto; + } +} + + +:host ::ng-deep .expand-collapse-title { + margin-top: 1px; + background-color: #eaeaea; + color: #5a5a5a; + font-family: OpenSans-Semibold, sans-serif; +} diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-information-tab.component.html b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-information-tab.component.html deleted file mode 100644 index 2a1c58c4cf..0000000000 --- a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-information-tab.component.html +++ /dev/null @@ -1,50 +0,0 @@ -<!-- - ~ Copyright (C) 2018 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. - --> - -<ng2-expand-collapse state="0"> - <header tooltip="General Information">General Info</header> - <content> - <!-- TYPE --> - <div class="component-details-panel-item"> - <span class="name" [innerHTML]="'GENERAL_LABEL_TYPE' | translate"></span> - <span class="value" data-tests-id="rightTab_componentType" tooltip="{{policy.policyTypeUid}}">{{policy.policyTypeUid}}</span> - </div> - - <!-- CATEGORY --> - <div class="component-details-panel-item"> - <span class="name" [innerHTML]="'GENERAL_LABEL_CATEGORY' | translate"></span> - <span class="value" data-tests-id="rightTab_category" tooltip="Policy">Policy</span> - </div> - - <!-- SUB CATEGORY --> - <div class="component-details-panel-item"> - <span class="name" [innerHTML]="'GENERAL_LABEL_SUB_CATEGORY' | translate"></span> - <span class="value" data-tests-id="rightTab_subCategory" tooltip="Policy">Policy</span> - </div> - - <!-- VERSION --> - <div class="component-details-panel-item"> - <span class="name" [innerHTML]="'GENERAL_LABEL_VERSION' | translate"></span> - <span class="value" data-tests-id="rightTab_version" tooltip="{{policy.version}}">{{policy.version}}</span> - </div> - - <!-- DESCRIPTION --> - <div class="component-details-panel-item description"> - <span class="name" [innerHTML]="'GENERAL_LABEL_DESCRIPTION' | translate"></span> - <span class="value" ellipsis="policy.description" max-chars="55" data-tests-id="rightTab_description">{{policy.description}}</span> - </div> - </content> -</ng2-expand-collapse> diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-properties-tab.component.less b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-properties-tab.component.less deleted file mode 100644 index e69de29bb2..0000000000 --- a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-properties-tab.component.less +++ /dev/null diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-tabs.component.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-tabs.component.ts deleted file mode 100644 index 1e2739901d..0000000000 --- a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-tabs.component.ts +++ /dev/null @@ -1,72 +0,0 @@ -/*- - * ============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========================================================= - */ - -import * as _ from "lodash"; -import { Component, Inject, Input, Output, EventEmitter, AfterViewInit, OnChanges } from "@angular/core"; -import { TranslateService } from './../../../../../shared/translator/translate.service'; -import { PoliciesService } from "../../../../../services/policies.service"; -import { Component as TopologyTemplate, ComponentInstance, IAppMenu } from "app/models"; -import { PolicyInstance } from 'app/models/graph/zones/policy-instance'; -import { GRAPH_EVENTS } from './../../../../../../utils/constants'; -import { EventListenerService } from 'app/services/event-listener-service'; -import { ZoneInstance } from 'app/models/graph/zones/zone-instance'; -import { SimpleChanges } from "@angular/core/src/metadata/lifecycle_hooks"; - -@Component({ - selector: 'policy-tabs', - templateUrl: './policy-tabs.component.html' -}) -export class PolicyTabsComponent implements OnChanges { - - @Input() topologyTemplate:TopologyTemplate; - @Input() selectedZoneInstanceType:string; - @Input() selectedZoneInstanceId:string; - @Input() isViewOnly: boolean; - @Output() isLoading: EventEmitter<boolean> = new EventEmitter<boolean>(); - - private policy:PolicyInstance; - - constructor(private translateService:TranslateService, - private policiesService:PoliciesService - ) { - - } - - ngOnChanges(changes: SimpleChanges): void { - this.initPolicy(); - } - - private initPolicy = ():void => { - this.isLoading.emit(true); - this.policiesService.getSpecificPolicy(this.topologyTemplate.componentType, this.topologyTemplate.uniqueId, this.selectedZoneInstanceId).subscribe( - policy => { - this.policy = policy; - console.log(JSON.stringify(policy)); - }, - error => console.log("Error getting policy!"), - () => this.isLoading.emit(false) - ); - } - - private setIsLoading = (value) :void => { - this.isLoading.emit(value); - } - -} diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-tabs.module.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-tabs.module.ts deleted file mode 100644 index 38dc19e1af..0000000000 --- a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-tabs.module.ts +++ /dev/null @@ -1,68 +0,0 @@ -/*- - * ============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========================================================= - */ -import { NgModule } from "@angular/core"; -import { HttpModule } from "@angular/http"; -import { FormsModule } from "@angular/forms"; -import { BrowserModule } from "@angular/platform-browser"; -import { UiElementsModule } from 'app/ng2/components/ui/ui-elements.module'; -import { ExpandCollapseComponent } from 'app/ng2/components/ui/expand-collapse/expand-collapse.component'; -import { PoliciesService } from "../../../../../services/policies.service"; -import { PolicyInformationTabComponent } from "./policy-information-tab.component"; -import { PolicyTargetsTabComponent } from "./policy-targets-tab.component"; -import { PolicyTabsComponent } from "./policy-tabs.component"; -import { PolicyPropertiesTabComponent } from "./policy-properties-tab.component"; -import { SdcUiComponentsModule } from "sdc-ui/lib/angular"; -import { TranslateModule } from './../../../../../shared/translator/translate.module'; - -@NgModule({ - declarations: [ - PolicyInformationTabComponent, - PolicyTargetsTabComponent, - PolicyPropertiesTabComponent, - PolicyTabsComponent - ], - imports: [ - BrowserModule, - FormsModule, - HttpModule, - SdcUiComponentsModule, - TranslateModule, - UiElementsModule - ], - entryComponents: [ - PolicyInformationTabComponent, - PolicyTargetsTabComponent, - PolicyPropertiesTabComponent, - PolicyTabsComponent, - ExpandCollapseComponent - ], - exports: [ - PolicyInformationTabComponent, - PolicyTargetsTabComponent, - PolicyPropertiesTabComponent, - PolicyTabsComponent - ], - providers: [ - PoliciesService - ] -}) -export class PolicyTabsModule { - -} diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-targets-tab.component.less b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-targets-tab.component.less deleted file mode 100644 index cd7ace2b6f..0000000000 --- a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-targets-tab.component.less +++ /dev/null @@ -1,12 +0,0 @@ -/deep/ -.component-details-panel-tab-policy-targets { - .component-details-panel-large-item { - display: flex; - flex-direction: row; - justify-content: space-between; - } - .w-sdc-designer-sidebar-section-title { - display: flex; - justify-content: space-between; - } -}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-targets-tab.component.html b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policy-targets-tab/policy-targets-tab.component.html index e263836fb1..838fd8bb51 100644 --- a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-targets-tab.component.html +++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policy-targets-tab/policy-targets-tab.component.html @@ -14,7 +14,7 @@ ~ limitations under the License. --> -<div class="w-sdc-designer-sidebar-section-title" titleTooltip="Targets">Targets +<h1 class="w-sdc-designer-sidebar-section-title" titleTooltip="Targets">Targets <svg-icon-label *ngIf="!isViewOnly" class="add-policy-button" name="plus-circle-o" @@ -24,7 +24,7 @@ labelPlacement="right" (click)="openAddTargetModal()"> </svg-icon-label> -</div> +</h1> <div class="expand-collapse-content"> <ul> <li *ngFor="let target of targets; let i = index" class="component-details-panel-large-item" @@ -40,7 +40,7 @@ </li> </ul> - <div *ngIf="targets.length===0" class="component-details-panel-tab-no-data"> + <div *ngIf="!targets || targets.length===0" class="component-details-panel-tab-no-data"> <div class="component-details-panel-tab-no-data-title">No data to display yet</div> <div class="component-details-panel-tab-no-data-content">Add targets to policy to see targets</div> </div> diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/base/base-tab.component.less b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policy-targets-tab/policy-targets-tab.component.less index aa8e75115f..d16a1595df 100644 --- a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/base/base-tab.component.less +++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policy-targets-tab/policy-targets-tab.component.less @@ -1,8 +1,6 @@ @import './../../../../../../../assets/styles/mixins'; -@import "./../../../../../../../assets/styles/variables-old"; -@import './../../../../../../../assets/styles/mixins_old'; -/deep/ + .expand-collapse-content { padding: 20px; } @@ -25,7 +23,9 @@ white-space: nowrap; height: 32px; line-height: 32px; - vertical-align: middle; + display: flex; + flex-direction: row; + justify-content: space-between; &:hover { background-color: #f8f8f8; @@ -37,30 +37,25 @@ } } -.component-details-panel-item { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - height: 22px; - line-height: 22px; - vertical-align: middle; - - &.description { - margin-top: 28px; - white-space: normal; - word-wrap: break-word; - .value { - max-width: none; - font-weight: normal; - font-family: @font-opensans-regular; - } - } - - .name { font-family: OpenSans-Semibold, sans-serif; } - .value { } -} - .component-details-panel-item-delete { cursor: pointer; visibility: hidden; } + +/deep/ .w-sdc-designer-sidebar-section-title { + color: #5a5a5a; + font-family: OpenSans-Semibold, sans-serif; + font-size: 14px; + background-color: #eaeaea; + cursor: pointer; + display: flex; + justify-content: space-between; + align-items: center; + text-transform: uppercase; + line-height: 32px; + padding: 0 10px 0 20px; + margin-top: 1px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policy-targets-tab/policy-targets-tab.component.spec.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policy-targets-tab/policy-targets-tab.component.spec.ts new file mode 100644 index 0000000000..7774138cab --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policy-targets-tab/policy-targets-tab.component.spec.ts @@ -0,0 +1,113 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { SdcUiCommon, SdcUiComponents, SdcUiServices } from 'onap-ui-angular'; +import { Observable } from 'rxjs/Rx'; +import { Mock } from 'ts-mockery'; +import { ConfigureFn, configureTests } from '../../../../../../../jest/test-config.helper'; +import { ComponentMetadata } from '../../../../../../models/component-metadata'; +import { EventListenerService } from '../../../../../../services/event-listener-service'; +import { TranslateService } from '../../../../../shared/translator/translate.service'; +import { WorkspaceService } from '../../../../workspace/workspace.service'; +import { CompositionService } from '../../../composition.service'; +import { PolicyTargetsTabComponent } from "app/ng2/pages/composition/panel/panel-tabs/policy-targets-tab/policy-targets-tab.component"; +import { PoliciesService } from "app/services-ng2"; +import { PolicyInstance, GroupInstance } from "app/models"; +import { NgxsModule } from "@ngxs/store"; +import { GraphState } from "app/ng2/pages/composition/common/store/graph.state"; +import { WorkspaceState } from "app/ng2/store/states/workspace.state"; +import { TargetUiObject } from "app/models/ui-models/ui-target-object"; +import { TargetOrMemberType } from "app/utils"; + + + + +describe('policy targets tab component', () => { + + let fixture: ComponentFixture<PolicyTargetsTabComponent>; + let component: PolicyTargetsTabComponent; + + let policiesServiceMock = Mock.of<PoliciesService>( + { + updateTargets: jest.fn().mockImplementation((compType, uid, policyUniqueId, updatedTargets) => { + if (updatedTargets === undefined) { + return Observable.throwError('error'); + } else { + return Observable.of(updatedTargets); + } + } + )}); + + let compositionServiceMock = { + componentInstances: [{uniqueId: '1', name: 'inst1'}, + {uniqueId: '2', name: 'inst2'}, + {uniqueId: '3', name: 'inst3'}, + {uniqueId: '4', name: 'inst4'}, + {uniqueId: '5', name: 'inst5'} + ], + groupInstances : [ + Mock.of<GroupInstance>({uniqueId: "group1", name: "group1"}), + Mock.of<GroupInstance>({uniqueId: "group2", name: "group2"}), + Mock.of<GroupInstance>({uniqueId: "group3", name: "group3"}) + ] + }; + + let workspaceServiceMock = { + metadata: Mock.of<ComponentMetadata>() + }; + + let modalServiceMock = { + openInfoModal: jest.fn(), + openCustomModal: jest.fn().mockImplementation(() => { return { + innerModalContent: { instance: { existingElements: targetsToAdd }}, + closeModal: jest.fn() + }}) + }; + + let loaderServiceMock = { + activate: jest.fn(), + deactivate: jest.fn() + }; + + const targetsToAdd = [ + <TargetUiObject>{uniqueId: '1', name: 'inst1', type: TargetOrMemberType.COMPONENT_INSTANCES}, + <TargetUiObject>{uniqueId: "group1", name: "group1", type: TargetOrMemberType.GROUPS} + ]; + + const policyInstanceMock = Mock.of<PolicyInstance>( + { getTargetsAsUiObject: jest.fn().mockImplementation( () => targetsToAdd) + }); + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [PolicyTargetsTabComponent], + imports: [NgxsModule.forRoot([WorkspaceState])], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + {provide: TranslateService, useValue: { translate: jest.fn() }}, + {provide: PoliciesService, useValue: policiesServiceMock}, + {provide: SdcUiServices.ModalService, useValue: modalServiceMock }, + {provide: EventListenerService, useValue: {} }, + {provide: CompositionService, useValue: compositionServiceMock }, + {provide: WorkspaceService, useValue: workspaceServiceMock}, + {provide: SdcUiServices.LoaderService, useValue: loaderServiceMock } + ], + }); + + fixture = TestBed.createComponent(PolicyTargetsTabComponent); + component = fixture.componentInstance; + component.policy = policyInstanceMock; + }); + + + it('if there are no existing targets, all component instances AND all groups are available for adding', () => { + component.targets = []; + const optionalTargetsToAdd = component.getOptionalsTargetsToAdd(); + expect(optionalTargetsToAdd).toHaveLength(8); + }); + + it('list of available instances to add does not include existing targets', () => { + component.targets = targetsToAdd; + const optionalMembersToAdd = component.getOptionalsTargetsToAdd(); + expect(optionalMembersToAdd).toHaveLength(6); + }); +}); diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-targets-tab.component.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policy-targets-tab/policy-targets-tab.component.ts index b79f4d9e07..f117290397 100644 --- a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-targets-tab.component.ts +++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policy-targets-tab/policy-targets-tab.component.ts @@ -19,84 +19,106 @@ */ import * as _ from "lodash"; -import { Component, Input, Output, EventEmitter, OnChanges, HostBinding, OnDestroy } from "@angular/core"; +import { Component, Input, Output, EventEmitter, OnChanges, HostBinding, OnDestroy, OnInit } from "@angular/core"; import { TranslateService } from './../../../../../shared/translator/translate.service'; -import { Component as TopologyTemplate } from "app/models"; import { PoliciesService } from "../../../../../services/policies.service"; -import { PolicyInstance, PolicyTargetsMap } from './../../../../../../models/graph/zones/policy-instance'; -import { SimpleChanges } from "@angular/core/src/metadata/lifecycle_hooks"; -import { SdcUiComponents } from "sdc-ui/lib/angular"; -import { IModalConfig } from "sdc-ui/lib/angular/modals/models/modal-config"; +import { PolicyInstance } from './../../../../../../models/graph/zones/policy-instance'; +import { SdcUiComponents, SdcUiCommon, SdcUiServices } from "onap-ui-angular"; import { AddElementsComponent } from "../../../../../components/ui/modal/add-elements/add-elements.component"; import { TargetUiObject } from "../../../../../../models/ui-models/ui-target-object"; import { ComponentInstance } from "../../../../../../models/componentsInstances/componentInstance"; import { TargetOrMemberType } from "../../../../../../utils/constants"; import { GRAPH_EVENTS } from 'app/utils'; import { EventListenerService } from 'app/services/event-listener-service'; +import { CompositionService } from "app/ng2/pages/composition/composition.service"; +import { WorkspaceService } from "app/ng2/pages/workspace/workspace.service"; +import { Store } from "@ngxs/store"; +import { Select } from "@ngxs/store"; +import { Observable } from "rxjs"; +import { tap } from "rxjs/operators"; +import {GraphState} from "../../../common/store/graph.state"; @Component({ selector: 'policy-targets-tab', templateUrl: './policy-targets-tab.component.html', - styleUrls: ['./../base/base-tab.component.less', 'policy-targets-tab.component.less'] + styleUrls: ['policy-targets-tab.component.less'] }) + +export class PolicyTargetsTabComponent implements OnInit { -export class PolicyTargetsTabComponent implements OnChanges, OnDestroy { + @Input() input:any; - private targets: Array<TargetUiObject>; // UI object to hold all targets with names. - @Input() policy: PolicyInstance; - @Input() topologyTemplate: TopologyTemplate; @Input() isViewOnly: boolean; - @Output() isLoading: EventEmitter<boolean> = new EventEmitter<boolean>(); @HostBinding('class') classes = 'component-details-panel-tab-policy-targets'; + @Select(GraphState.getSelectedComponent) policy$: Observable<PolicyInstance>; + public policy: PolicyInstance; + private subscription; + + private addModalInstance: SdcUiComponents.ModalComponent; + public targets: Array<TargetUiObject>; // UI object to hold all targets with names. + constructor(private translateService: TranslateService, private policiesService: PoliciesService, - private modalService: SdcUiComponents.ModalService, - private eventListenerService: EventListenerService - ) { - this.eventListenerService.registerObserverCallback(GRAPH_EVENTS.ON_POLICY_INSTANCE_UPDATE, this.initTargets) - } + private modalService: SdcUiServices.ModalService, + private eventListenerService: EventListenerService, + private compositionService: CompositionService, + private workspaceService: WorkspaceService, + private loaderService: SdcUiServices.LoaderService, + private store: Store + ) { } - ngOnChanges(changes:SimpleChanges):void { - this.initTargets(); + ngOnInit() { + this.subscription = this.policy$.pipe( + tap((policy) => { + if(policy instanceof PolicyInstance){ + this.policy = policy; + this.targets = this.policy.getTargetsAsUiObject(<ComponentInstance[]>this.compositionService.componentInstances, this.compositionService.groupInstances); + } + })).subscribe(); } - ngOnDestroy() { - this.eventListenerService.unRegisterObserver(GRAPH_EVENTS.ON_POLICY_INSTANCE_UPDATE); + ngOnDestroy () { + if(this.subscription) + this.subscription.unsubscribe(); } deleteTarget(target: TargetUiObject): void { - this.isLoading.emit(true); - this.policiesService.deletePolicyTarget(this.topologyTemplate.componentType, this.topologyTemplate.uniqueId, this.policy, target.uniqueId, target.type).subscribe( + this.loaderService.activate(); + this.policiesService.deletePolicyTarget(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId, this.policy, target.uniqueId, target.type).subscribe( (policyInstance:PolicyInstance) => { + this.targets = this.targets.filter(item => item.uniqueId !== target.uniqueId); this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_POLICY_INSTANCE_UPDATE, policyInstance); + // this.store.dispatch(new UpdateSelectedComponentAction({uniqueId: policyInstance.uniqueId, type:ComponentType.})); + }, + error => { + console.log("Error deleting target!"); + this.loaderService.deactivate(); }, - error => console.log("Error deleting target!"), - () => this.isLoading.emit(false) + () => this.loaderService.deactivate() ); } - private initTargets = (policyInstance?: PolicyInstance) => { - this.policy = policyInstance ? policyInstance : this.policy; - this.targets = this.policy.getTargetsAsUiObject(this.topologyTemplate.componentInstances, this.topologyTemplate.groupInstances); - } addTargets = ():void => { - var targetsToAdd:Array<TargetUiObject> = this.modalService.getCurrentInstance().innerModalContent.instance.existingElements; //TODO refactor sdc-ui modal in order to return the data + var targetsToAdd:Array<TargetUiObject> = this.addModalInstance.innerModalContent.instance.existingElements; //TODO refactor sdc-ui modal in order to return the data if(targetsToAdd.length > 0) { - this.modalService.closeModal(); - this.isLoading.emit(true); - var updatedTarget: Array<TargetUiObject> = _.union(this.targets, targetsToAdd); - this.policiesService.updateTargets(this.topologyTemplate.componentType, this.topologyTemplate.uniqueId, this.policy.uniqueId, updatedTarget).subscribe( + this.addModalInstance.closeModal(); + this.loaderService.activate(); + var updatedTargets: Array<TargetUiObject> = _.union(this.targets, targetsToAdd); + this.policiesService.updateTargets(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId, this.policy.uniqueId, updatedTargets).subscribe( (updatedPolicyInstance:PolicyInstance) => { + this.targets = updatedTargets; this.eventListenerService.notifyObservers(GRAPH_EVENTS.ON_POLICY_INSTANCE_UPDATE, updatedPolicyInstance); + // this.store.dispatch(new UpdateSelectedComponentAction({component: updatedPolicyInstance})); }, error => { console.log("Error updating targets!"); + this.loaderService.deactivate(); }, - () => this.isLoading.emit(false) + () => this.loaderService.deactivate() ); } } @@ -104,7 +126,7 @@ export class PolicyTargetsTabComponent implements OnChanges, OnDestroy { getOptionalsTargetsToAdd():Array<TargetUiObject> { let optionalsTargetsToAdd:Array<TargetUiObject> = []; // adding all instances as optional targets to add if not already exist - _.forEach(this.topologyTemplate.componentInstances, (instance:ComponentInstance) => { + _.forEach(this.compositionService.componentInstances, (instance:ComponentInstance) => { if (!_.some(this.targets, (target:TargetUiObject) => { return target.uniqueId === instance.uniqueId })) { @@ -113,7 +135,7 @@ export class PolicyTargetsTabComponent implements OnChanges, OnDestroy { }); // adding all groups as optional targets to add if not already exist - _.forEach(this.topologyTemplate.groupInstances, (groupInstance:ComponentInstance) => { // adding all instances as optional targets to add if not already exist + _.forEach(this.compositionService.groupInstances, (groupInstance:ComponentInstance) => { // adding all instances as optional targets to add if not already exist if (!_.some(this.targets, (target:TargetUiObject) => { return target.uniqueId === groupInstance.uniqueId })) { @@ -125,21 +147,20 @@ export class PolicyTargetsTabComponent implements OnChanges, OnDestroy { } openAddTargetModal(): void { - let addTargetModalConfig: IModalConfig = { + let addTargetModalConfig = { title: this.policy.name + " ADD TARGETS", size: "md", - type: "custom", + type: SdcUiCommon.ModalType.custom, testId: "addTargetsModal", buttons: [ {text: "ADD TARGETS", size: 'xsm', callback: this.addTargets, closeModal: false}, {text: 'CANCEL', size: 'sm', type: "secondary", closeModal: true} ] - }; + } as SdcUiCommon.IModalConfig; var optionalTargetsToAdd = this.getOptionalsTargetsToAdd(); - this.modalService.openCustomModal(addTargetModalConfig, AddElementsComponent, { + this.addModalInstance = this.modalService.openCustomModal(addTargetModalConfig, AddElementsComponent, { elementsToAdd: optionalTargetsToAdd, elementName: "target" }); - } } diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/properties-tab/properties-tab.component.html b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/properties-tab/properties-tab.component.html new file mode 100644 index 0000000000..86c6fea1ef --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/properties-tab/properties-tab.component.html @@ -0,0 +1,97 @@ +<ng2-expand-collapse state="0"> + <header sdc-tooltip tooltip-text="{{input.title}}">{{input.title}}</header> + <content> + <div class="w-sdc-designer-sidebar-section"> + <div *ngIf="properties"> + <ng-container *ngFor="let key of objectKeys(properties); let idx = index"> + <sdc-accordion [title]="groupNameByKey(key) + ' Properties'" [css-class]="'properties-accordion'" [arrow-direction]="'right'" [testId]="groupNameByKey(key) + 'properties'" [open]="true"> + + <!--ng-show="isShowDetailsSection" --> + <div class="i-sdc-designer-sidebar-section-content-item" *ngIf="!groupPropertiesByInstance"> + <div class="i-sdc-designer-sidebar-section-content-item-property-and-attribute" attr.data-tests-id="propertyRow" + *ngFor="let property of properties[key]"> + + <div class="property-details"> + <span class="i-sdc-designer-sidebar-section-content-item-property-and-attribute-label" + [ngClass]="{'hand enabled': !isViewOnly}" + sdc-tooltip tooltip-text="{{property.name}}" + (click)="!isViewOnly && updateProperty(property)" + attr.data-tests-id="{{property.name}}">{{property.name}}</span> + <span class="i-sdc-designer-sidebar-section-content-item-property-value" *ngIf="isPropertyOwner()" + sdc-tooltip tooltip-text="{{property.defaultValue}}">{{property.defaultValue}}</span> + <span class="i-sdc-designer-sidebar-section-content-item-property-value" *ngIf="!isPropertyOwner()" + sdc-tooltip tooltip-text="{{property.value}}" + attr.data-tests-id="value_{{property.name}}">{{property.value}}</span> + </div> + <div class="property-buttons"> + <svg-icon *ngIf="!isViewOnly && (isPropertyOwner() && !property.readonly)" name="trash-o" clickable="true" size="medium" mode="info" testId="delete_{{property.name}}" (click)="deleteProperty(property)"></svg-icon> + </div> + </div> + </div> + <div class="i-sdc-designer-sidebar-section-content-item" *ngIf="groupPropertiesByInstance"> + <ng-container *ngFor="let InstanceProperties of properties[key]; let propIndex = index"> + <div class="vfci-properties-group"> + <div class="second-level"> + <div class="expand-collapse-title-icon"></div> + <span class="w-sdc-designer-sidebar-section-title-text" sdc-tooltip tooltip-text="{{getComponentInstanceNameFromInstanceByKey(InstanceProperties.key)}} Properties" + attr.data-tests-id="vfci-properties">{{getComponentInstanceNameFromInstanceByKey(InstanceProperties.key) + ' Properties'}}</span> + </div> + </div> + <div class="w-sdc-designer-sidebar-section-content instance-properties {{propIndex}}"> + <div class="i-sdc-designer-sidebar-section-content-item"> + <div class="i-sdc-designer-sidebar-section-content-item-property-and-attribute" attr.data-tests-id="propertyRow" + *ngFor="let instanceProperty of InstanceProperties.value"> + <div> + <span class="i-sdc-designer-sidebar-section-content-item-property-and-attribute-label" + [ngClass]="{'hand enabled': !isViewOnly}" + sdc-tooltip tooltip-text="{{instanceProperty.name}}" + attr.data-tests-id="vfci-property">{{instanceProperty.name}}</span> + </div> + <div> + <span class="i-sdc-designer-sidebar-section-content-item-property-value" + sdc-tooltip tooltip-text="{{instanceProperty.value === undefined ? instanceProperty.defaultValue : instanceProperty.value}}"> + {{instanceProperty.value === undefined ? instanceProperty.defaultValue : instanceProperty.value}}</span> + </div> + </div> + </div> + </div> + </ng-container> + </div> + <!--<div class="w-sdc-designer-sidebar-section-footer" *ngIf="(!isViewOnly && isPropertyOwner()) || showAddPropertyButton">--> + <!--<button class="w-sdc-designer-sidebar-section-footer-action tlv-btn blue" attr.data-tests-id="addGrey" (click)="addProperty()" type="button">--> + <!--Add Property--> + <!--</button>--> + <!--</div>--> + </sdc-accordion> + </ng-container> + </div> + + <!--attributes--> + <div *ngIf="attributes"> + <ng-container *ngFor="let key of objectKeys(attributes); let attrIndex = index"> + <sdc-accordion [title]="groupNameByKey(key) + ' Attributes'" [arrow-direction]="'right'" [testId]="groupNameByKey(key) + 'attributes'" [css-class]="'attributes-accordion'"> + <!--ng-show="isShowDetailsSection" --> + <div class="i-sdc-designer-sidebar-section-content-item"> + <div class="i-sdc-designer-sidebar-section-content-item-property-and-attribute" + *ngFor="let attribute of attributes[key]"> + <div> + <span class="i-sdc-designer-sidebar-section-content-item-property-and-attribute-label" + [ngClass]="{'hand enabled': !isViewOnly}" + sdc-tooltip tooltip-text="{{attribute.name}}" + (click)="!isViewOnly && viewAttribute(attribute)" + attr.data-tests-id="{{attribute.name}}-attr">{{attribute.name}}</span> + </div> + <div> + <span class="i-sdc-designer-sidebar-section-content-item-property-value" *ngIf="isPropertyOwner()" + sdc-tooltip tooltip-text="{{attribute.defaultValue}}">{{attribute.defaultValue}}</span> + <span class="i-sdc-designer-sidebar-section-content-item-property-value" *ngIf="!isPropertyOwner()" + sdc-tooltip tooltip-text="{{attribute.value}}" attr.data-tests-id="value-of-{{attribute.name}}">{{attribute.value}}</span> + </div> + </div> + </div> + </sdc-accordion> + </ng-container> + </div> + </div> + </content> +</ng2-expand-collapse> diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/properties-tab/properties-tab.component.less b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/properties-tab/properties-tab.component.less new file mode 100644 index 0000000000..5cb0697da1 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/properties-tab/properties-tab.component.less @@ -0,0 +1,66 @@ +.scroll-container { + display: flex; + overflow-y: auto; +} + +.i-sdc-designer-sidebar-section-content-item-property-and-attribute { + color: #666666; + font-family: OpenSans-Semibold, sans-serif; + font-size: 14px; + border-bottom: 1px solid #cdcdcd; + min-height: 72px; + padding: 15px 10px 10px 18px; + // position: relative; + display:flex; + + .property-details { + flex:1; + } + + .property-buttons { + flex: 0 0 auto; + align-self: center; + } +} + +.i-sdc-designer-sidebar-section-content-item-property-and-attribute-label { + display: block; + font-weight: bold; + &:hover { + color: #3b7b9b; + } +} + +.i-sdc-designer-sidebar-section-content-item-property-and-attribute-label, .i-sdc-designer-sidebar-section-content-item-property-value { + overflow: hidden; + text-overflow: ellipsis; + max-width: 245px; + white-space: nowrap; + display: block; +} + + + +/deep/ .expand-collapse-content { + max-height: max-content; + padding: 10px 0; + + .sdc-accordion .sdc-accordion-header { + + background-color: #e6f6fb; + border-left: solid #009fdb 4px; + box-shadow: 0 0px 3px -1px rgba(0, 0, 0, 0.3); + margin-bottom: 2px; + width: auto; + height: auto; + padding: 10px; + color: #666666; + font-family: OpenSans-Semibold, sans-serif; + font-size: 14px; + + } + + /deep/.sdc-accordion .sdc-accordion-body { + padding-left: 0; + } +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/properties-tab/properties-tab.component.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/properties-tab/properties-tab.component.ts new file mode 100644 index 0000000000..b4b8248ed0 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/properties-tab/properties-tab.component.ts @@ -0,0 +1,212 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { Store } from '@ngxs/store'; +import { + AttributeModel, + AttributesGroup, + Component as TopologyTemplate, + ComponentMetadata, + FullComponentInstance, + PropertiesGroup, + PropertyModel +} from 'app/models'; +import { CompositionService } from 'app/ng2/pages/composition/composition.service'; +import { WorkspaceService } from 'app/ng2/pages/workspace/workspace.service'; +import { GroupByPipe } from 'app/ng2/pipes/groupBy.pipe'; +import { ResourceNamePipe } from 'app/ng2/pipes/resource-name.pipe'; +import { TopologyTemplateService } from 'app/ng2/services/component-services/topology-template.service'; +import { ComponentGenericResponse } from 'app/ng2/services/responses/component-generic-response'; +import { TranslateService } from 'app/ng2/shared/translator/translate.service'; +import { ModalsHandler } from 'app/utils'; +import { SdcUiCommon, SdcUiComponents, SdcUiServices } from 'onap-ui-angular'; +import {SelectedComponentType, TogglePanelLoadingAction} from "../../../common/store/graph.actions"; + +@Component({ + selector: 'properties-tab', + templateUrl: './properties-tab.component.html', + styleUrls: ['./properties-tab.component.less'] +}) +export class PropertiesTabComponent implements OnInit { + attributes: AttributesGroup; + isComponentInstanceSelected: boolean; + properties: PropertiesGroup; + groupPropertiesByInstance: boolean; + propertiesMessage: string; + metadata: ComponentMetadata; + objectKeys = Object.keys; + + @Input() isViewOnly: boolean; + @Input() componentType: SelectedComponentType; + @Input() component: FullComponentInstance | TopologyTemplate; + @Input() input: {title: string}; + + constructor(private store: Store, + private workspaceService: WorkspaceService, + private compositionService: CompositionService, + private modalsHandler: ModalsHandler, + private topologyTemplateService: TopologyTemplateService, + private modalService: SdcUiServices.ModalService, + private translateService: TranslateService, + private groupByPipe: GroupByPipe) { + } + + ngOnInit() { + this.metadata = this.workspaceService.metadata; + this.isComponentInstanceSelected = this.componentType === SelectedComponentType.COMPONENT_INSTANCE; + this.getComponentInstancesPropertiesAndAttributes(); + } + + public isPropertyOwner = (): boolean => { + return this.component instanceof TopologyTemplate && this.component.isResource(); + } + + public updateProperty = (property: PropertyModel): void => { + this.openEditPropertyModal(property); + } + + public deleteProperty = (property: PropertyModel): void => { + + const onOk: Function = (): void => { + this.store.dispatch(new TogglePanelLoadingAction({isLoading: true})); + this.topologyTemplateService.deleteProperty(this.component.componentType, this.component.uniqueId, property.uniqueId) + .subscribe((response) => { + this.store.dispatch(new TogglePanelLoadingAction({isLoading: false})); + this.component.properties = this.component.properties.filter((prop) => prop.uniqueId !== property.uniqueId); + this.initComponentProperties(); + }, () => { + this.store.dispatch(new TogglePanelLoadingAction({isLoading: false})); + }); + }; + + const title: string = this.translateService.translate('PROPERTY_VIEW_DELETE_MODAL_TITLE'); + const message: string = this.translateService.translate('PROPERTY_VIEW_DELETE_MODAL_TEXT', {name: property.name}); + const okButton = { + testId: 'OK', + text: 'OK', + type: SdcUiCommon.ButtonType.info, + callback: onOk, + closeModal: true} as SdcUiComponents.ModalButtonComponent; + this.modalService.openInfoModal(title, message, 'delete-modal', [okButton]); + } + + public groupNameByKey = (key: string): string => { + switch (key) { + case 'derived': + return 'Derived'; + + case this.metadata.uniqueId: + return ResourceNamePipe.getDisplayName(this.metadata.name); + + default: + return this.getComponentInstanceNameFromInstanceByKey(key); + } + } + + public getComponentInstanceNameFromInstanceByKey = (key: string): string => { + let instanceName: string = ''; + const componentInstance = this.compositionService.getComponentInstances().find((item) => item.uniqueId === key); + if (key !== undefined && componentInstance) { + + instanceName = ResourceNamePipe.getDisplayName(componentInstance.name); + } + return instanceName; + } + + private getComponentInstancesPropertiesAndAttributes = () => { + this.topologyTemplateService.getComponentInstanceAttributesAndProperties( + this.workspaceService.metadata.uniqueId, + this.workspaceService.metadata.componentType) + .subscribe((genericResponse: ComponentGenericResponse) => { + this.compositionService.componentInstancesAttributes = genericResponse.componentInstancesAttributes || new AttributesGroup(); + this.compositionService.componentInstancesProperties = genericResponse.componentInstancesProperties; + this.initPropertiesAndAttributes(); + }); + } + + private initComponentProperties = (): void => { + let result: PropertiesGroup = {}; + + this.propertiesMessage = undefined; + this.groupPropertiesByInstance = false; + if (this.component instanceof FullComponentInstance) { + result[this.component.uniqueId] = _.orderBy(this.compositionService.componentInstancesProperties[this.component.uniqueId], ['name']); + if (this.component.originType === 'VF') { + this.groupPropertiesByInstance = true; + result[this.component.uniqueId] = Array.from(this.groupByPipe.transform(result[this.component.uniqueId], 'path')); + } + } else if (this.metadata.isService()) { + // Temporally fix to hide properties for service (UI stack when there are many properties) + result = this.compositionService.componentInstancesProperties; + this.propertiesMessage = 'Note: properties for service are disabled'; + } else { + const componentUid = this.component.uniqueId; + result[componentUid] = Array<PropertyModel>(); + const derived = Array<PropertyModel>(); + _.forEach(this.component.properties, (property: PropertyModel) => { + if (componentUid === property.parentUniqueId) { + result[componentUid].push(property); + } else { + property.readonly = true; + derived.push(property); + } + }); + if (derived.length) { + result['derived'] = derived; + } + this.objectKeys(result).forEach((key) => { result[key] = _.orderBy(result[key], ['name']); }); + } + this.properties = result; + } + + private initComponentAttributes = (): void => { + let result: AttributesGroup = {}; + + if (this.component) { + if (this.component instanceof FullComponentInstance) { + result[this.component.uniqueId] = this.compositionService.componentInstancesAttributes[this.component.uniqueId] || []; + } else if (this.metadata.isService()) { + result = this.compositionService.componentInstancesAttributes; + } else { + result[this.component.uniqueId] = (this.component as TopologyTemplate).attributes; + } + this.attributes = result; + this.objectKeys(this.attributes).forEach((key) => { + this.attributes[key] = _.orderBy(this.attributes[key], ['name']); + }); + + } + } + + /** + * 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.metadata.isService() || !!this.component; + } + + /** + * 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.newOpenEditPropertyModal(property, + (this.isPropertyOwner() ? + this.properties[property.parentUniqueId] : + this.properties[property.resourceInstanceUniqueId]) || [], + this.isPropertyValueOwner(), 'component', property.resourceInstanceUniqueId).then((updatedProperty: PropertyModel) => { + if (updatedProperty) { + const oldProp = _.find(this.properties[updatedProperty.resourceInstanceUniqueId], + (prop: PropertyModel) => prop.uniqueId === updatedProperty.uniqueId); + oldProp.value = updatedProperty.value; + } + }); + } + + private initPropertiesAndAttributes = (): void => { + this.initComponentProperties(); + this.initComponentAttributes(); + } +} diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/req-capabilities-tab/req-capabilities-tab.component.html b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/req-capabilities-tab/req-capabilities-tab.component.html new file mode 100644 index 0000000000..27e05ec1f0 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/req-capabilities-tab/req-capabilities-tab.component.html @@ -0,0 +1,36 @@ +<div class="w-sdc-designer-sidebar-tab-content sdc-general-tab relations"> + <div *ngIf="!isCurrentDisplayComponentIsComplex(); else complexComponentTemplate"> + <div class="w-sdc-designer-sidebar-section w-sdc-designer-sidebar-section-relations"> + <sdc-accordion [title]="'Capabilities'" [arrow-direction]="'right'" [testId]="'Capabilities-accordion'"> + <div *ngFor="let capability of capabilities" class="relations-details-container"> + <div class="relations-name">{{capability.name}} </div> + <div class="relations-desc"> {{capability.type}} </div> + </div> + </sdc-accordion> + </div> + <div class="w-sdc-designer-sidebar-section w-sdc-designer-sidebar-section-relations"> + <sdc-accordion [title]="'Requirements'" [arrow-direction]="'right'" [testId]="'Requirements-accordion'"> + <requirement-list [component]='component' [requirements]="requirements" [isInstanceSelected]="isComponentInstanceSelected"></requirement-list> + </sdc-accordion> + + </div> + </div> + + <ng-template #complexComponentTemplate> + <sdc-accordion *ngIf="capabilitiesInstancesMap" [title]="'Capabilities'" [arrow-direction]="'right'" [testId]="'Capabilities-accordion'"> + <sdc-accordion *ngFor="let key of objectKeys(capabilitiesInstancesMap); let i = index" [title]="key"> + <div *ngFor="let capability of capabilitiesInstancesMap[key]" class="relations-details-container"> + <div class="relations-name">{{capability.name}} </div> + <div class="relations-desc"> {{capability.type}} </div> + </div> + </sdc-accordion> + </sdc-accordion> + + <sdc-accordion *ngIf="requirementsInstancesMap" [title]="'Requirements'" [arrow-direction]="'right'" [testId]="'Requirements-accordion'"> + <sdc-accordion *ngFor="let key of objectKeys(requirementsInstancesMap); let i = index" [title]="key"> + <requirement-list [component]='component' [requirements]="requirementsInstancesMap[key]" [isInstanceSelected]="isComponentInstanceSelected"></requirement-list> + </sdc-accordion> + </sdc-accordion> + + </ng-template> +</div> diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/req-capabilities-tab/req-capabilities-tab.component.less b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/req-capabilities-tab/req-capabilities-tab.component.less new file mode 100644 index 0000000000..fe4573aadc --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/req-capabilities-tab/req-capabilities-tab.component.less @@ -0,0 +1,57 @@ + +/deep/.sdc-accordion { + margin-bottom: 0; + display: grid; + + .sdc-accordion-header { + background-color: #e6f6fb; + border-left: solid #009fdb 4px; + box-shadow: 0 0px 3px -1px rgba(0, 0, 0, 0.3); + margin-bottom: 2px; + width: auto; + height: auto; + padding: 10px; + color: #666666; + font-family: OpenSans-Semibold, sans-serif; + font-size: 14px; + } + + .sdc-accordion-body.open { + padding-left: 0; + padding-top: 0; + .sdc-accordion-header { /*Second level - nested accordion */ + background-color: #f8f8f8; + padding: 4px 20px 4px 37px; + border-bottom: 1px solid #d2d2d2; + border-left:none; + height: 30px; + } + } +} + + +.relations-details-container { + border-bottom: 1px solid #cdcdcd; + padding: 10px 10px 10px 18px; + + font-size: 14px; + font-family: OpenSans-Regular, sans-serif; + + .relations-name { + color: #666666; + font-weight: bold; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + text-transform: capitalize; + max-width: 240px; + display: inline-block; + } + + .relations-desc { + color: #8c8c8c; + word-wrap: break-word; + white-space: normal; + max-width: 265px; + } +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/req-capabilities-tab/req-capabilities-tab.component.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/req-capabilities-tab/req-capabilities-tab.component.ts new file mode 100644 index 0000000000..03697b38f2 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/req-capabilities-tab/req-capabilities-tab.component.ts @@ -0,0 +1,165 @@ +import { Component, OnInit, Input, OnDestroy } from '@angular/core'; +import { Component as TopologyTemplate, Capability, Requirement, CapabilitiesGroup, RequirementsGroup, ComponentInstance, FullComponentInstance } from "app/models"; +import { Store } from "@ngxs/store"; +import { GRAPH_EVENTS } from "app/utils"; +import { ComponentGenericResponse } from "app/ng2/services/responses/component-generic-response"; +import { TopologyTemplateService } from "app/ng2/services/component-services/topology-template.service"; +import { EventListenerService } from "app/services"; +import { WorkspaceService } from "app/ng2/pages/workspace/workspace.service"; +import { CompositionService } from "app/ng2/pages/composition/composition.service"; +import {SelectedComponentType, TogglePanelLoadingAction} from "../../../common/store/graph.actions"; + + +export class InstanceCapabilitiesMap { + [key:string]:Array<Capability>; +} + +export class InstanceRequirementsMap { + [key:string]:Array<Requirement>; +} + +@Component({ + selector: 'req-capabilities-tab', + templateUrl: './req-capabilities-tab.component.html', + styleUrls: ['./req-capabilities-tab.component.less'] +}) +export class ReqAndCapabilitiesTabComponent implements OnInit, OnDestroy { + + isComponentInstanceSelected: boolean; + capabilities:Array<Capability>; + requirements:Array<Requirement>; + capabilitiesInstancesMap:InstanceCapabilitiesMap; + requirementsInstancesMap:InstanceRequirementsMap; + objectKeys = Object.keys; + + @Input() isViewOnly: boolean; + @Input() componentType: SelectedComponentType; + @Input() component: TopologyTemplate | FullComponentInstance; + @Input() input: any; + + + constructor(private store: Store, + private topologyTemplateService:TopologyTemplateService, + private workspaceService: WorkspaceService, + private compositionService: CompositionService, + private eventListenerService:EventListenerService) { } + + ngOnInit(): void { + + this.isComponentInstanceSelected = this.componentType === SelectedComponentType.COMPONENT_INSTANCE; + + this.requirements = []; + this.capabilities = []; + this.initEvents(); + this.initRequirementsAndCapabilities(); + + } + + 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); + } + + ngOnDestroy(): void { + 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); + } + + public isCurrentDisplayComponentIsComplex = ():boolean => { + + if (this.component instanceof FullComponentInstance) { + if (this.component.originType === 'VF') { + return true; + } + return false; + } else { + return this.component.isComplex(); + } + } + + private loadComplexComponentData = () => { + this.store.dispatch(new TogglePanelLoadingAction({isLoading: true})); + + this.topologyTemplateService.getCapabilitiesAndRequirements(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId).subscribe((response:ComponentGenericResponse) => { + this.workspaceService.metadata.capabilities = response.capabilities; + this.workspaceService.metadata.requirements = response.requirements; + this.setScopeCapabilitiesRequirements(response.capabilities, response.requirements); + this.initInstancesMap(); + this.store.dispatch(new TogglePanelLoadingAction({isLoading: false})); + }, (error) => { this.store.dispatch(new TogglePanelLoadingAction({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.capabilities = this.extractValuesFromMap(capabilities); + this.requirements = this.extractValuesFromMap(requirements); + } + + + private initInstancesMap = ():void => { + + this.capabilitiesInstancesMap = new InstanceCapabilitiesMap(); + _.forEach(this.capabilities, (capability:Capability) => { + if (this.capabilitiesInstancesMap[capability.ownerName]) { + this.capabilitiesInstancesMap[capability.ownerName] = this.capabilitiesInstancesMap[capability.ownerName].concat(capability); + } else { + this.capabilitiesInstancesMap[capability.ownerName] = new Array<Capability>(capability); + } + }); + + this.requirementsInstancesMap = new InstanceRequirementsMap(); + _.forEach(this.requirements, (requirement:Requirement) => { + if (this.requirementsInstancesMap[requirement.ownerName]) { + this.requirementsInstancesMap[requirement.ownerName] = this.requirementsInstancesMap[requirement.ownerName].concat(requirement); + } else { + this.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.component instanceof FullComponentInstance) { + this.store.dispatch(new TogglePanelLoadingAction({isLoading: false})); + this.setScopeCapabilitiesRequirements(this.component.capabilities, this.component.requirements); + if (this.component.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.component.capabilities || !this.component.requirements) { + this.loadComplexComponentData(); + + } else { + this.store.dispatch(new TogglePanelLoadingAction({isLoading: false})); + this.setScopeCapabilitiesRequirements(this.component.capabilities, this.component.requirements); + this.initInstancesMap(); + } + } + } + + private updateRequirementCapabilities = () => { + if (!this.isComponentInstanceSelected) { + this.loadComplexComponentData(); + } + } + + + + +} + diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/req-capabilities-tab/requirement-list/requirement-list.component.html b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/req-capabilities-tab/requirement-list/requirement-list.component.html new file mode 100644 index 0000000000..8292729cf8 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/req-capabilities-tab/requirement-list/requirement-list.component.html @@ -0,0 +1,20 @@ +<div class="i-sdc-designer-sidebar-capabilities-requirements"> + <div class="i-sdc-designer-sidebar-section-content-item-relations-group"> + <div class="i-sdc-designer-sidebar-section-content-item-relations" + *ngFor="let requirement of requirements"> + <div class="i-sdc-designer-sidebar-section-content-item-relations-details"> + <div class="i-sdc-designer-sidebar-section-content-item-relations-details-name">{{requirement.name}} </div> + <div class="i-sdc-designer-sidebar-section-content-item-relations-details-desc">{{requirement.node}} + <div *ngIf="getRelation(requirement) != null"> + <div class="i-sdc-designer-sidebar-section-content-item-relations-details-indent-box"></div> + <div class="i-sdc-designer-sidebar-section-content-item-relations-details-child"> + <span class="i-sdc-designer-sidebar-section-content-item-relations-details-desc">{{getRelation(requirement).type}} <br/></span> + <span class="i-sdc-designer-sidebar-section-content-item-relations-details-name">{{getRelation(requirement).requirementName}}</span> + </div> + </div> + </div> + </div> + </div> + </div> + </div> +
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/req-capabilities-tab/requirement-list/requirement-list.component.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/req-capabilities-tab/requirement-list/requirement-list.component.ts new file mode 100644 index 0000000000..e167c47dcc --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/req-capabilities-tab/requirement-list/requirement-list.component.ts @@ -0,0 +1,40 @@ +import { Component, Input } from '@angular/core'; +import { Component as TopologyTemplate, RelationshipModel, Relationship, Requirement } from "app/models"; +import { CompositionService } from "app/ng2/pages/composition/composition.service"; +import { ResourceNamePipe } from "app/ng2/pipes/resource-name.pipe"; + +@Component({ + selector: 'requirement-list', + templateUrl: './requirement-list.component.html' +}) +export class RequirementListComponent { + @Input() component: TopologyTemplate; + @Input() requirements: Array<Requirement>; + @Input() isInstanceSelected:boolean; + + + constructor(private compositionService: CompositionService) { } + + + public getRelation = (requirement:any):any => { + if (this.isInstanceSelected && this.component.componentInstancesRelations) { + let relationItem:Array<RelationshipModel> = _.filter(this.component.componentInstancesRelations, (relation:RelationshipModel) => { + return relation.fromNode === this.component.uniqueId && + _.filter(relation.relationships, (relationship:Relationship) => { + return relationship.relation.requirement == requirement.name && relationship.relation.requirementOwnerId == requirement.ownerId; + }).length; + }); + + if (relationItem && relationItem.length) { + return { + type: requirement.relationship.split('.').pop(), + requirementName: ResourceNamePipe.getDisplayName(this.compositionService.componentInstances[_.map + (this.compositionService.componentInstances, "uniqueId").indexOf(relationItem[0].toNode)].name) + }; + } + } + return null; + }; + +}; + diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/service-consumption-tab/service-consumption-tab.component.html b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/service-consumption-tab/service-consumption-tab.component.html new file mode 100644 index 0000000000..a52c841156 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/service-consumption-tab/service-consumption-tab.component.html @@ -0,0 +1,15 @@ +<ng2-expand-collapse state="0"> + <header sdc-tooltip tooltip-text="{{input.title}}">{{input.title}}</header> + <content> + <service-consumption + [parentService]="metadata" + [selectedService]="component" + [selectedServiceInstanceId]="component.uniqueId" + [instancesMappedList]="instancesMappedList" + [parentServiceInputs]="componentInputs" + [instancesCapabilitiesMap]="instancesCapabilitiesMap" + [readonly]="isViewOnly"> + </service-consumption> + </content> +</ng2-expand-collapse> + diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-properties-tab.component.less b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/service-consumption-tab/service-consumption-tab.component.less index e69de29bb2..e69de29bb2 100644 --- a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/groups/group-properties-tab.component.less +++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/service-consumption-tab/service-consumption-tab.component.less diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/service-consumption-tab/service-consumption-tab.component.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/service-consumption-tab/service-consumption-tab.component.ts new file mode 100644 index 0000000000..8715afd047 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/service-consumption-tab/service-consumption-tab.component.ts @@ -0,0 +1,89 @@ + +import { Component, Input } from '@angular/core'; +import { Store } from '@ngxs/store'; +import { + CapabilitiesGroup, + Capability, + Component as TopologyTemplate, + ComponentInstance, + FullComponentInstance, + InputBEModel, + InputsGroup, + InterfaceModel, + PropertiesGroup +} from 'app/models'; +import { ComponentMetadata } from '../../../../../../models/component-metadata'; +import { ServiceInstanceObject } from '../../../../../../models/service-instance-properties-and-interfaces'; +import { EventListenerService } from '../../../../../../services/event-listener-service'; +import { TopologyTemplateService } from '../../../../../services/component-services/topology-template.service'; +import { ComponentGenericResponse } from '../../../../../services/responses/component-generic-response'; +import { WorkspaceService } from '../../../../workspace/workspace.service'; +import { SelectedComponentType } from '../../../common/store/graph.actions'; +import { CompositionService } from '../../../composition.service'; + +@Component({ + selector: 'service-consumption-tab', + templateUrl: './service-consumption-tab.component.html', + styleUrls: ['./service-consumption-tab.component.less'], +}) +export class ServiceConsumptionTabComponent { + isComponentInstanceSelected: boolean; + + instancesMappedList: ServiceInstanceObject[]; + componentInstancesProperties: PropertiesGroup; + componentInstancesInputs: InputsGroup; + componentInstancesInterfaces: Map<string, InterfaceModel[]>; + componentInputs: InputBEModel[]; + componentCapabilities: Capability[]; + instancesCapabilitiesMap: Map<string, Capability[]>; + metadata: ComponentMetadata; + + @Input() isViewOnly: boolean; + @Input() componentType: SelectedComponentType; + @Input() component: TopologyTemplate | FullComponentInstance; + @Input() input: any; + + constructor(private store: Store, + private topologyTemplateService: TopologyTemplateService, + private workspaceService: WorkspaceService, + private compositionService: CompositionService, + private eventListenerService: EventListenerService ) {} + ngOnInit() { + this.metadata = this.workspaceService.metadata; + this.isComponentInstanceSelected = this.componentType === SelectedComponentType.COMPONENT_INSTANCE; + this.initInstances(); + } + + private initInstances = (): void => { + this.topologyTemplateService.getServiceConsumptionData(this.metadata.componentType, this.metadata.uniqueId).subscribe((genericResponse: ComponentGenericResponse) => { + this.componentInstancesProperties = genericResponse.componentInstancesProperties; + this.componentInstancesInputs = genericResponse.componentInstancesInputs; + this.componentInstancesInterfaces = genericResponse.componentInstancesInterfaces; + this.componentInputs = genericResponse.inputs; + this.buildInstancesCapabilitiesMap(genericResponse.componentInstances); + this.updateInstanceAttributes(); + }); + } + + private buildInstancesCapabilitiesMap = (componentInstances: Array<ComponentInstance>): void => { + this.instancesCapabilitiesMap = new Map(); + let flattenCapabilities = []; + _.forEach(componentInstances, (componentInstance) => { + flattenCapabilities = CapabilitiesGroup.getFlattenedCapabilities(componentInstance.capabilities); + this.instancesCapabilitiesMap[componentInstance.uniqueId] = _.filter(flattenCapabilities, cap => cap.properties && cap.ownerId === componentInstance.uniqueId); + }); + } + + private updateInstanceAttributes = (): void => { + if (this.isComponentInstanceSelected && this.componentInstancesProperties) { + this.instancesMappedList = this.compositionService.componentInstances.map((coInstance) => new ServiceInstanceObject({ + id: coInstance.uniqueId, + name: coInstance.name, + properties: this.componentInstancesProperties[coInstance.uniqueId] || [], + inputs: this.componentInstancesInputs[coInstance.uniqueId] || [], + interfaces: this.componentInstancesInterfaces[coInstance.uniqueId] || [] + })); + } + } + +} diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/service-dependencies-tab/service-dependencies-tab.component.html b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/service-dependencies-tab/service-dependencies-tab.component.html new file mode 100644 index 0000000000..47351a46a1 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/service-dependencies-tab/service-dependencies-tab.component.html @@ -0,0 +1,18 @@ +<ng2-expand-collapse state="0"> + <header sdc-tooltip tooltip-text="{{input.title}}">{{input.title}}</header> + <content> + <div *ngIf="isComponentInstanceSelected"> + <service-dependencies + [compositeService]="metaData" + [currentServiceInstance]="component" + [selectedInstanceProperties]="selectedInstanceProperties" + [selectedInstanceSiblings]="selectedInstanceSiblings" + [selectedInstanceConstraints]="selectedInstanceConstraints" + [readonly]="isViewOnly" + (dependencyStatus)="notifyDependencyEventsObserver($event)" + (updateRulesListEvent)="updateSelectedInstanceConstraints($event)" + (loadRulesListEvent)="loadConstraints()"> + </service-dependencies> + </div> + </content> +</ng2-expand-collapse> diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/service-dependencies-tab/service-dependencies-tab.component.less b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/service-dependencies-tab/service-dependencies-tab.component.less new file mode 100644 index 0000000000..47e26e2d64 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/service-dependencies-tab/service-dependencies-tab.component.less @@ -0,0 +1,3 @@ +:host /deep/ .expand-collapse-content { + padding: 0 0 10px; +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/service-dependencies-tab/service-dependencies-tab.component.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/service-dependencies-tab/service-dependencies-tab.component.ts new file mode 100644 index 0000000000..5171e3b607 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/service-dependencies-tab/service-dependencies-tab.component.ts @@ -0,0 +1,95 @@ + +import { Component, Input } from '@angular/core'; +import { Store } from '@ngxs/store'; +import { + CapabilitiesGroup, + Capability, + Component as TopologyTemplate, + ComponentInstance, + FullComponentInstance, + InputBEModel, + InputsGroup, + InterfaceModel, + PropertiesGroup, + PropertyBEModel, +} from 'app/models'; +import { DEPENDENCY_EVENTS } from 'app/utils/constants'; +import { ComponentMetadata } from '../../../../../../models/component-metadata'; +import { ServiceInstanceObject } from '../../../../../../models/service-instance-properties-and-interfaces'; +import { EventListenerService } from '../../../../../../services/event-listener-service'; +import { ConstraintObject } from '../../../../../components/logic/service-dependencies/service-dependencies.component'; +import { TopologyTemplateService } from '../../../../../services/component-services/topology-template.service'; +import { ComponentGenericResponse } from '../../../../../services/responses/component-generic-response'; +import { WorkspaceService } from '../../../../workspace/workspace.service'; +import { SelectedComponentType } from '../../../common/store/graph.actions'; +import { CompositionService } from '../../../composition.service'; + +@Component({ + selector: 'service-dependencies-tab', + templateUrl: 'service-dependencies-tab.component.html', + styleUrls: ['service-dependencies-tab.component.less'] +}) +export class ServiceDependenciesTabComponent { + isComponentInstanceSelected: boolean; + + selectedInstanceSiblings: ServiceInstanceObject[]; + componentInstancesConstraints: any[]; + selectedInstanceConstraints: ConstraintObject[]; + selectedInstanceProperties: PropertyBEModel[]; + componentInstanceProperties: PropertiesGroup; + metaData: ComponentMetadata; + + @Input() isViewOnly: boolean; + @Input() componentType: SelectedComponentType; + @Input() component: FullComponentInstance | TopologyTemplate; + @Input() input: any; + + constructor(private store: Store, + private topologyTemplateService: TopologyTemplateService, + private workspaceService: WorkspaceService, + private compositionService: CompositionService, + private eventListenerService: EventListenerService) { + } + + ngOnInit() { + this.metaData = this.workspaceService.metadata; + this.isComponentInstanceSelected = this.componentType === SelectedComponentType.COMPONENT_INSTANCE; + this.initInstancesWithProperties(); + this.loadConstraints(); + this.initInstancesWithProperties(); + } + + public loadConstraints = (): void => { + this.topologyTemplateService.getServiceFilterConstraints(this.metaData.componentType, this.metaData.uniqueId).subscribe((response) => { + this.componentInstancesConstraints = response.nodeFilterData; + }); + } + + public notifyDependencyEventsObserver = (isChecked: boolean): void => { + this.eventListenerService.notifyObservers(DEPENDENCY_EVENTS.ON_DEPENDENCY_CHANGE, isChecked); + } + + public updateSelectedInstanceConstraints = (constraintsList:Array<ConstraintObject>):void => { + this.componentInstancesConstraints[this.component.uniqueId].properties = constraintsList; + this.selectedInstanceConstraints = this.componentInstancesConstraints[this.component.uniqueId].properties; + } + + private initInstancesWithProperties = (): void => { + this.topologyTemplateService.getComponentInstanceProperties(this.metaData.componentType, this.metaData.uniqueId).subscribe((genericResponse: ComponentGenericResponse) => { + this.componentInstanceProperties = genericResponse.componentInstancesProperties; + this.updateInstanceAttributes(); + }); + } + + private updateInstanceAttributes = (): void => { + if (this.isComponentInstanceSelected && this.componentInstanceProperties) { + const instancesMappedList = this.compositionService.componentInstances.map((coInstance) => new ServiceInstanceObject({ + id: coInstance.uniqueId, + name: coInstance.name, + properties: this.componentInstanceProperties[coInstance.uniqueId] || [] + })); + this.selectedInstanceProperties = this.componentInstanceProperties[this.component.uniqueId]; + this.selectedInstanceSiblings = instancesMappedList.filter((coInstance) => coInstance.id !== this.component.uniqueId); + } + } +} diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel.component.html b/catalog-ui/src/app/ng2/pages/composition/panel/panel.component.html deleted file mode 100644 index 9bb809249a..0000000000 --- a/catalog-ui/src/app/ng2/pages/composition/panel/panel.component.html +++ /dev/null @@ -1,50 +0,0 @@ -<!-- - ~ Copyright (C) 2018 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. - --> - -<ng2-composition-panel-header - [name]="selectedZoneInstanceName" - [topologyTemplate]="topologyTemplate" - [selectedZoneInstanceType]="selectedZoneInstanceType" - [selectedZoneInstanceId]="selectedZoneInstanceId" - [nonCertified]="nonCertified" - [isViewOnly]="isViewOnly" - [isLoading]="isLoading" -></ng2-composition-panel-header> - -<div class="component-details-panel-tabs"> - <loader [display]="isLoading" [size]="'large'" [relative]="true" [loaderDelay]="500"></loader> - - <div *ngIf="selectedZoneInstanceType === zoneInstanceType.POLICY"> - <policy-tabs - [topologyTemplate]="topologyTemplate" - [selectedZoneInstanceType]="selectedZoneInstanceType" - [selectedZoneInstanceId]="selectedZoneInstanceId" - [isViewOnly]="isViewOnly" - (isLoading)="setIsLoading($event)" - ></policy-tabs> - </div> - - <div *ngIf="selectedZoneInstanceType === zoneInstanceType.GROUP"> - <group-tabs - [topologyTemplate]="topologyTemplate" - [selectedZoneInstanceType]="selectedZoneInstanceType" - [selectedZoneInstanceId]="selectedZoneInstanceId" - [isViewOnly]="isViewOnly" - (isLoading)="setIsLoading($event)" - ></group-tabs> - </div> - -</div> diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel.component.less b/catalog-ui/src/app/ng2/pages/composition/panel/panel.component.less deleted file mode 100644 index 1777d54486..0000000000 --- a/catalog-ui/src/app/ng2/pages/composition/panel/panel.component.less +++ /dev/null @@ -1,11 +0,0 @@ -/deep/ -.component-details-panel { - - color: #666666; - font-family: OpenSans-Regular, sans-serif; - font-size: 14px; - - .component-details-panel-tabs { - - } -} diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel.component.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel.component.ts deleted file mode 100644 index 53599d6366..0000000000 --- a/catalog-ui/src/app/ng2/pages/composition/panel/panel.component.ts +++ /dev/null @@ -1,60 +0,0 @@ -/*- - * ============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========================================================= - */ - -import * as _ from "lodash"; -import { Component, Inject, Input, Output, EventEmitter, AfterViewInit, SimpleChanges, HostBinding } from "@angular/core"; -import { Component as TopologyTemplate, ComponentInstance, IAppMenu } from "app/models"; -import { PolicyInstance } from 'app/models/graph/zones/policy-instance'; -import { TranslateService } from 'app/ng2/shared/translator/translate.service'; -import { ZoneInstanceType } from "app/models/graph/zones/zone-instance"; -import { GroupsService } from "../../../services/groups.service"; -import { PoliciesService } from "../../../services/policies.service"; -import { SdcUiComponents } from "sdc-ui/lib/angular"; -import { IZoneService } from "../../../../models/graph/zones/zone"; - -@Component({ - selector: 'ng2-composition-panel', - templateUrl: './panel.component.html', - styleUrls: ['./panel.component.less'], - providers: [TranslateService] -}) -export class CompositionPanelComponent { - - @Input() topologyTemplate: TopologyTemplate; - @Input() selectedZoneInstanceType: ZoneInstanceType; - @Input() selectedZoneInstanceId: string; - @Input() selectedZoneInstanceName: string; - @Input() nonCertified: boolean; - @Input() isViewOnly: boolean; - @Input() isLoading: boolean; - - - @HostBinding('class') classes = 'component-details-panel'; - - private zoneInstanceType = ZoneInstanceType; // Expose ZoneInstanceType to use in template. - - constructor(){ - } - - private setIsLoading = (value):void => { - this.isLoading = value; - } - -} diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel.module.ts b/catalog-ui/src/app/ng2/pages/composition/panel/panel.module.ts deleted file mode 100644 index 57f6be8b8e..0000000000 --- a/catalog-ui/src/app/ng2/pages/composition/panel/panel.module.ts +++ /dev/null @@ -1,54 +0,0 @@ -/*- - * ============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========================================================= - */ -import {NgModule} from "@angular/core"; -import {HttpModule} from "@angular/http"; -import {FormsModule} from "@angular/forms"; -import {BrowserModule} from "@angular/platform-browser"; -import {CompositionPanelComponent} from "./panel.component"; -import {CompositionPanelHeaderModule} from "app/ng2/pages/composition/panel/panel-header/panel-header.module"; -import {GroupTabsModule} from "./panel-tabs/groups/group-tabs.module"; -import {PolicyTabsModule} from "./panel-tabs/policies/policy-tabs.module"; -import {SdcUiComponents} from "sdc-ui/lib/angular"; -import {UiElementsModule} from 'app/ng2/components/ui/ui-elements.module'; -import {AddElementsModule} from "../../../components/ui/modal/add-elements/add-elements.module"; - -@NgModule({ - declarations: [ - CompositionPanelComponent - ], - imports: [ - BrowserModule, - FormsModule, - HttpModule, - CompositionPanelHeaderModule, - PolicyTabsModule, - GroupTabsModule, - UiElementsModule, - AddElementsModule - ], - entryComponents: [ - CompositionPanelComponent - ], - exports: [], - providers: [SdcUiComponents.ModalService] -}) -export class CompositionPanelModule { - -} diff --git a/catalog-ui/src/app/ng2/pages/home/__snapshots__/home.component.spec.ts.snap b/catalog-ui/src/app/ng2/pages/home/__snapshots__/home.component.spec.ts.snap new file mode 100644 index 0000000000..ae5445e546 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/home/__snapshots__/home.component.spec.ts.snap @@ -0,0 +1,28 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`home component should match current snapshot 1`] = ` +<home-page + $state={[Function Object]} + authService={[Function Object]} + cacheService={[Function Object]} + componentShouldReload={[Function Function]} + homeService={[Function Object]} + importVSPService={[Function Object]} + initFolders={[Function Function]} + isDefaultFilter={[Function Function]} + loaderService={[Function Object]} + modalService={[Function Object]} + modalsHandler={[Function Object]} + sdcConfig={[Function Object]} + sdcMenu={[Function Object]} + translateService={[Function Object]} + updateFilter={[Function Function]} +> + <div + class="sdc-catalog-container" + > + + <top-nav /> + </div> +</home-page> +`; diff --git a/catalog-ui/src/app/ng2/pages/home/folders.ts b/catalog-ui/src/app/ng2/pages/home/folders.ts new file mode 100644 index 0000000000..036ae329b7 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/home/folders.ts @@ -0,0 +1,93 @@ + +export interface IItemMenu { + +} + +export interface IMenuItemProperties { + text:string; + group:string; + state:string; + dist:string; + groupname:string; + states:Array<any>; +} + +export class FoldersMenu { + private _folders:Array<FoldersItemsMenu> = []; + + constructor(folders:Array<IMenuItemProperties>) { + let self = this; + folders.forEach(function (folder:IMenuItemProperties) { + if (folder.groupname) { + self._folders.push(new FoldersItemsMenuGroup(folder)); + } else { + self._folders.push(new FoldersItemsMenu(folder)); + } + }); + self._folders[0].setSelected(true); + } + + public getFolders():Array<FoldersItemsMenu> { + return this._folders; + } + + public getCurrentFolder():FoldersItemsMenu { + let menuItem:FoldersItemsMenu = undefined; + this.getFolders().forEach(function (tmpFolder:FoldersItemsMenu) { + if (tmpFolder.isSelected()) { + menuItem = tmpFolder; + } + }); + return menuItem; + } + + public setSelected(folder:FoldersItemsMenu):void { + this.getFolders().forEach(function (tmpFolder:FoldersItemsMenu) { + tmpFolder.setSelected(false); + }); + folder.setSelected(true); + } +} + +export class FoldersItemsMenu implements IItemMenu { + public text:string; + public group:string; + public state:string; + public dist:string; + public states:Array<any>; + + private selected:boolean = false; + + constructor(menuProperties:IMenuItemProperties) { + this.text = menuProperties.text; + this.group = menuProperties.group; + this.state = menuProperties.state; + this.states = menuProperties.states; + this.dist = menuProperties.dist; + } + + public isSelected():boolean { + return this.selected; + } + + public setSelected(value:boolean):void { + this.selected = value; + } + + public isGroup():boolean { + return false; + } +} + +export class FoldersItemsMenuGroup extends FoldersItemsMenu { + public groupname:string; + + constructor(menuProperties:IMenuItemProperties) { + super(menuProperties); + this.groupname = menuProperties.groupname; + } + + public isGroup():boolean { + return true; + } +} diff --git a/catalog-ui/src/app/ng2/pages/home/home.component.html b/catalog-ui/src/app/ng2/pages/home/home.component.html new file mode 100644 index 0000000000..1c8c2b4373 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/home/home.component.html @@ -0,0 +1,88 @@ +<div class="sdc-catalog-container"> + <div class="w-sdc-main-container" *ngIf="user"> + + <div id="dashboard-main-scroll" infiniteScroll class="w-sdc-main-right-container" (infiniteScroll)="raiseNumberOfElementToDisplay()" [infiniteScrollDistance]="100"> + + <div class='w-sdc-row-flex-items'> + + <!-- ADD Component --> + <div *ngIf="user.role === 'DESIGNER'" class="w-sdc-dashboard-card-new" + (mouseleave)="setDisplayActions(false)" + (mouseover)="setDisplayActions(true)"> + <div class="w-sdc-dashboard-card-new-content" data-tests-id="AddButtonsArea"> + <div class="w-sdc-dashboard-card-new-content-plus" [hidden]="displayActions"></div> + <div class="sdc-dashboard-create-element-container" [hidden]="!displayActions"> + <sdc-button *ngIf="roles[user.role].dashboard.showCreateNew" testId="createResourceButton" size="medium" type="secondary" text="Add VF" (click)="openCreateModal('RESOURCE')"></sdc-button> + <sdc-button *ngIf="roles[user.role].dashboard.showCreateNew" testId="createCRButton" size="medium" type="secondary" text="Add CR" (click)="createCR()"></sdc-button> + <sdc-button *ngIf="roles[user.role].dashboard.showCreateNew" testId="createPNFButton" size="medium" type="secondary" text="Add PNF" (click)="createPNF()"></sdc-button> + <sdc-button *ngIf="roles[user.role].dashboard.showCreateNew" testId="createServiceButton" size="medium" type="secondary" text="Add Service" (click)="openCreateModal('SERVICE')"></sdc-button> + </div> + </div> + </div> + + <!-- Import Component --> + <div *ngIf="user.role === 'DESIGNER'" class="w-sdc-dashboard-card-new" + (mouseleave)="setDisplayActions(false)" + (mouseover)="setDisplayActions(true)"> + <div class="w-sdc-dashboard-card-new-content" data-tests-id="importButtonsArea" > + <div class="w-sdc-dashboard-card-import-content-plus" [hidden]="displayActions"></div> + <div class="sdc-dashboard-import-element-container" [hidden]="!displayActions"> + <sdc-button-file-opener + *ngIf="roles[user.role].dashboard.showCreateNew" + size="medium" + type="secondary" + text="Import VFC" + testId="importVFCbutton" + [extensions]="sdcConfig.toscaFileExtension" + (fileUpload)="onImportVfc($event)" + [convertToBase64]="true" + ></sdc-button-file-opener> + <sdc-button *ngIf="roles[user.role].dashboard.showCreateNew" data-tests-id="importButtonsVSP" size="medium" type="secondary" text="Import VSP" (click)="notificationIconCallback()"></sdc-button> + <sdc-button-file-opener + *ngIf="roles[user.role].dashboard.showCreateNew" + size="medium" + type="secondary" + text="Import DCAE" + testId="importDCAE" + [extensions]="sdcConfig.csarFileExtension" + (fileUpload)="onImportVf($event)" + [convertToBase64]="true" + ></sdc-button-file-opener> + </div> + </div> + </div> + + <!-- Tile new --> + <ui-tile *ngFor="let item of homeFilteredSlicedItems" + [component]="item" (onTileClick)="goToComponent(item)"></ui-tile> + <!-- Tile new --> + + </div> + + </div> + + <div class="w-sdc-left-sidebar"> + <div class="i-sdc-left-sidebar-item " + *ngFor="let folder of folders.getFolders()" + [ngClass]="{'category-title': folder.isGroup(), 'selectedLink': folder.isSelected()}"> + + <span *ngIf="folder.isGroup()" class="title-text">{{folder.text}}</span> + <sdc-checkbox *ngIf="!folder.isGroup() && !folder.dist" + [label]="folder.text" + [attr.data-tests-id]="'filter-' + folder.state" + [checked]="homeFilter.selectedStatuses.indexOf(folder.state) !== -1" + (checkedChange)="changeCheckboxesFilter(homeFilter.selectedStatuses, folder.state, $event)"></sdc-checkbox> + + <sdc-checkbox *ngIf="!folder.isGroup() && folder.dist" + [label]="folder.text" + [checked]="homeFilter.distributed.indexOf(folder.dist) !== -1" + (checkedChange)="changeCheckboxesFilter(homeFilter.distributed, folder.dist, $event)"></sdc-checkbox> + <span class="i-sdc-left-sidebar-item-state-count" [attr.data-tests-id]="'count-' + folder.state">{{entitiesCount(folder)}}</span> + </div> + </div> + + </div> + + <top-nav [topLvlSelectedIndex]="0" [version]="version" [searchTerm]="homeFilter.search.filterTerm" (searchTermChange)="changeFilterTerm($event)" [notificationIconCallback]="notificationIconCallback"></top-nav> + +</div> diff --git a/catalog-ui/src/app/ng2/pages/home/home.component.less b/catalog-ui/src/app/ng2/pages/home/home.component.less new file mode 100644 index 0000000000..c5b73748ba --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/home/home.component.less @@ -0,0 +1,126 @@ +@import '../../../../assets/styles/mixins_old'; +@import '../../../../assets/styles/sprite'; +.w-sdc-left-sidebar-nav { + margin-top: 46px; +} + +.w-sdc-main-right-container { + height: 100%; + overflow-y: scroll; +} + +.w-sdc-main-right-container-element { + float: left; + height: 217px; + width: 217px; + margin: 10px; + position: relative; +} + +.w-sdc-main-right-container-element-details-container { + position: absolute; + top: 165px; + left: 50px; +} + +.w-sdc-main-right-container-element-name { + font-weight: bold; +} + +.i-sdc-left-sidebar-item{ + display: flex; + &.category-title .title-text, sdc-checkbox{ + flex-grow: 1; + } + &:not(.category-title).i-sdc-left-sidebar-item-state-count { + line-height: 14px; + } +} + + +//////////////////////////////Cards//////////////////// +.w-sdc-dashboard-card-new { + border: 2px dashed @color_m; + .border-radius(2px); + cursor: pointer; + display: inline-block; + height: 198px; + margin: 11px; + position: relative; + vertical-align: middle; + width: 202px; +} + +.w-sdc-dashboard-card-new-content { + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + height: 100%; +} + +.w-sdc-dashboard-card-new-content-plus { + .sprite-new; + .add-icon; + position: relative; + margin-bottom: 20px; + + &:after { + .n_14_m; + content: 'ADD'; + position: absolute; + top: 25px; + left: -3px; + vertical-align: -50%; + } +} + +.w-sdc-dashboard-card-import-content-plus { + .sprite-new; + .import-icon; + position: relative; + margin-bottom: 20px; + + &:after { + .n_14_m; + content: 'IMPORT'; + position: absolute; + top: 25px; + left: -16px; + vertical-align: -50%; + } +} + +.sdc-dashboard-create-element-container, +.sdc-dashboard-import-element-container { + + width: 140px; + + sdc-button, + sdc-button-file-opener { + padding-bottom: 5px; + &:last-child{ + padding-bottom: 0; + } + } + + .import-file{ + position: relative; + file-opener{ + position: absolute; + top: 0; + /deep/ input[type="file"] { + .hand; + filter: alpha(opacity=0); + opacity: 0; + position: absolute; + top: 0; + left: 0; + width: 140px; + height: 36px; + } + } + } +} + + diff --git a/catalog-ui/src/app/ng2/pages/home/home.component.spec.ts b/catalog-ui/src/app/ng2/pages/home/home.component.spec.ts new file mode 100644 index 0000000000..df854024fa --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/home/home.component.spec.ts @@ -0,0 +1,270 @@ + +import { SdcConfigToken, ISdcConfig } from "../../config/sdc-config.config"; +import { SdcMenuToken, IAppMenu } from "../../config/sdc-menu.config"; + + +import { async, ComponentFixture, TestBed } from "@angular/core/testing"; +import { HomeComponent } from "./home.component"; +import {ConfigureFn, configureTests} from "../../../../jest/test-config.helper"; +import {NO_ERRORS_SCHEMA} from "@angular/core"; +import { TranslateService } from "../../shared/translator/translate.service"; +import { HomeService, CacheService, AuthenticationService, ImportVSPService } from '../../../../app/services-ng2'; +import { ModalsHandler } from "../../../../app/utils"; +import { SdcUiServices } from "onap-ui-angular"; +import {ComponentType, ResourceType} from "../../../utils/constants"; +import { FoldersMenu, FoldersItemsMenu, FoldersItemsMenuGroup } from './folders'; +import { HomeFilter } from "../../../../app/models/home-filter"; +import {Component} from "../../../models/components/component"; + + + + +describe('home component', () => { + + // const mockedEvent = <MouseEvent>{ target: {} } + let fixture: ComponentFixture<HomeComponent>; + // let eventServiceMock: Partial<EventListenerService>; + + let importVspService: Partial<ImportVSPService>; + let mockStateService; + let modalServiceMock :Partial<SdcUiServices.ModalService>; + let translateServiceMock : Partial<TranslateService>; + let foldersItemsMenuMock; + let homeFilterMock :Partial<HomeFilter>; + let foldersMock; + let loaderServiceMock; + + + beforeEach( + async(() => { + modalServiceMock = { + openWarningModal: jest.fn() + } + + mockStateService = { + // go: jest.fn().mockReturnValue( new Promise.resolve((resolve, reject )=> resolve())) + go: jest.fn() + } + + translateServiceMock = { + translate: jest.fn() + } + + homeFilterMock = { + search: jest.fn, + toUrlParam: jest.fn() + } + + foldersMock = { + setSelected: jest.fn() + } + + loaderServiceMock = { + activate: jest.fn(), + deactivate: jest.fn() + } + + const configure: ConfigureFn = testBed => { + testBed.configureTestingModule({ + declarations: [HomeComponent], + imports: [], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + {provide: SdcConfigToken, useValue: {"csarFileExtension":"csar", "toscaFileExtension":"yaml,yml"}}, + {provide: SdcMenuToken, useValue: {}}, + {provide: "$state", useValue: mockStateService}, + {provide: HomeService, useValue: {}}, + {provide: AuthenticationService, useValue: {}}, + {provide: CacheService, useValue: {}}, + {provide: TranslateService, useValue: translateServiceMock}, + {provide: ModalsHandler, useValue: {}}, + {provide: SdcUiServices.ModalService, useValue: modalServiceMock}, + {provide: SdcUiServices.LoaderService, useValue: loaderServiceMock}, + {provide: ImportVSPService, useValue: {}} + ], + }); + }; + + configureTests(configure).then(testBed => { + fixture = testBed.createComponent(HomeComponent); + }); + }) + ); + + + it('should match current snapshot', () => { + expect(fixture).toMatchSnapshot(); + }); + + it('should call on home component openCreateModal with null imported file', () => { + const component = TestBed.createComponent(HomeComponent); + let componentType:string = 'test'; + let importedFile:any = null; + component.componentInstance.openCreateModal(componentType, importedFile); + expect(mockStateService.go).toBeCalledWith('workspace.general', {type: componentType.toLowerCase()}); + }); + + + it('should call on home component openCreateModal with imported file', () => { + const component = TestBed.createComponent(HomeComponent); + component.componentInstance.initEntities = jest.fn(); + let componentType:string = 'test'; + let importedFile:any = 'importedFile'; + component.componentInstance.openCreateModal(componentType, importedFile); + expect(component.componentInstance.initEntities).toBeCalledWith(true); + }); + + + it ('should call on home component onImportVf without file without extension', () => { + const component = TestBed.createComponent(HomeComponent); + let file:any = {filename : 'test'}; + let expectedTitle:string = translateServiceMock.translate("NEW_SERVICE_RESOURCE_ERROR_VALID_CSAR_EXTENSIONS_TITLE"); + let expectedMessage:string = translateServiceMock.translate("NEW_SERVICE_RESOURCE_ERROR_VALID_CSAR_EXTENSIONS", {"csarFileExtension":"csar"}); + component.componentInstance.onImportVf(file); + expect(modalServiceMock.openWarningModal).toBeCalledWith(expectedTitle, expectedMessage , 'error-invalid-csar-ext'); + }); + + + it ('should call on home component onImportVf with file without extension' , () => { + const component = TestBed.createComponent(HomeComponent); + let file:any = {filename : 'test.csar'}; + component.componentInstance.onImportVf(file); + expect(mockStateService.go).toBeCalledWith('workspace.general', { + type: ComponentType.RESOURCE.toLowerCase(), + importedFile: file, + resourceType: ResourceType.VF + }); + }); + + + it ('should call on home component onImportVfc without file without extension', () => { + const component = TestBed.createComponent(HomeComponent); + let file:any = {filename : 'test'}; + let expectedTitle:string = translateServiceMock.translate("NEW_SERVICE_RESOURCE_ERROR_VALID_TOSCA_EXTENSIONS_TITLE"); + let expectedMessage:string = translateServiceMock.translate("NEW_SERVICE_RESOURCE_ERROR_VALID_TOSCA_EXTENSIONS", {"toscaFileExtension":"yaml,yml"}); + component.componentInstance.onImportVfc(file); + expect(modalServiceMock.openWarningModal).toBeCalledWith(expectedTitle, expectedMessage , 'error-invalid-tosca-ext'); + }); + + it ('should call on home component onImportVfc with file without extension' , () => { + const component = TestBed.createComponent(HomeComponent); + let file:any = {filename : 'test.yml'}; + component.componentInstance.onImportVfc(file); + expect(mockStateService.go).toBeCalledWith('workspace.general', { + type: ComponentType.RESOURCE.toLowerCase(), + importedFile: file, + resourceType: ResourceType.VFC + }); + }); + + it ('should call on home component createPNF' , () => { + const component = TestBed.createComponent(HomeComponent); + component.componentInstance.createPNF(); + expect(mockStateService.go).toBeCalledWith('workspace.general', { + type: ComponentType.RESOURCE.toLowerCase(), + resourceType: ResourceType.PNF + }); + }); + + it ('should call on home component createCR' , () => { + const component = TestBed.createComponent(HomeComponent); + component.componentInstance.createCR(); + expect(mockStateService.go).toBeCalledWith('workspace.general', { + type: ComponentType.RESOURCE.toLowerCase(), + resourceType: ResourceType.CR + }); + }); + + + it ('should call on home component updateFilter' , () => { + const component = TestBed.createComponent(HomeComponent); + component.componentInstance.homeFilter = homeFilterMock; + component.componentInstance.filterHomeItems = jest.fn(); + component.componentInstance.updateFilter(); + + expect(mockStateService.go).toBeCalledWith('.', homeFilterMock.toUrlParam(), {location: 'replace', notify: false}); + // expect(spy).toHaveBeenCalledTimes(1); + + // let spy = spyOn(homeFilterMock, 'toUrlParam').and.returnValue({ + // 'filter.term': '', + // 'filter.distributed': '', + // 'filter.status':'' + // }); + }); + + // it ('should call on home component setSelectedFolder' , () => { + // const component = TestBed.createComponent(HomeComponent); + // let folderItem:Partial<FoldersItemsMenu> = { text:'someThing'}; + // let folderItem1:number; + // component.componentInstance.folders = foldersMock; + // expect(foldersMock.setSelected).toBeCalledWith(folderItem); + // }); + + // it ('should call on home component goToComponent' , () => { + // const component = TestBed.createComponent(HomeComponent); + // let componentParam:Partial<Component> = { uuid:'someThing', uniqueId:'uniqueID', componentType:'componentType'}; + // component.componentInstance.goToComponent(componentParam); + // expect(loaderServiceMock.activate).toHaveBeenCalled(); + // // expect(mockStateService.go).toBeCalledWith('workspace.general', {id: componentParam.uniqueId, type: componentParam.componentType.toLowerCase()}).then(function(){ + // // loaderServiceMock.deactivate(); + // // }); + // expect(mockStateService.go).toBeCalled(); + // }); + + // it ('should call on home component raiseNumberOfElementToDisplay so numberOfItemToDisplay will be 0' , () => { + // const component = TestBed.createComponent(HomeComponent); + // component.componentInstance.raiseNumberOfElementToDisplay(); + // expect(component.componentInstance.numberOfItemToDisplay).toEqual(0); + // }); + // + // it ('should call on home component raiseNumberOfElementToDisplay with min(2,70) so numberOfItemToDisplay will be 2' , () => { + // const component = TestBed.createComponent(HomeComponent); + // component.componentInstance.homeItems = ['item1', 'item2']; + // component.componentInstance.numberOfItemToDisplay = 70; + // component.componentInstance.raiseNumberOfElementToDisplay(true); + // expect(component.componentInstance.numberOfItemToDisplay).toEqual(2); + // }); + // + // it ('should call on home component raiseNumberOfElementToDisplay with min(3,35) so numberOfItemToDisplay will be 2 after fullPagesAmount is calculated' , () => { + // const component = TestBed.createComponent(HomeComponent); + // component.componentInstance.homeItems = ['item1', 'item2', 'item3']; + // component.componentInstance.numberOfItemToDisplay = 70; + // component.componentInstance.numberOfItemToDisplay = 0; + // component.componentInstance.raiseNumberOfElementToDisplay(false); + // expect(component.componentInstance.numberOfItemToDisplay).toEqual(3); + // }); + // + // + // it ('should call on home component changeFilterTerm' , () => { + // const component = TestBed.createComponent(HomeComponent); + // component.componentInstance.changeFilterTerm("testStr"); + // // expect ( "testStr" ).toEqual(homeFilterMock.search.) + // }); + + + + + + + // it ('should call on home component entitiesCount' , () => { + // const component = TestBed.createComponent(HomeComponent); + // component.componentInstance.entitiesCount("aaa"); + // expect(mockStateService.go).toBeCalledWith('workspace.general', { + // type: ComponentType.RESOURCE.toLowerCase(), + // resourceType: ResourceType.CR + // }); + // }); + + + // it('should call on home component notificationIconCallback', () => { + // const component = TestBed.createComponent(HomeComponent); + // component.componentInstance.initEntities = jest.fn(); + // component.componentInstance.notificationIconCallback(); + // expect(mockStateService.go).toBeCalledWith('workspace.general', {}); + // }); + + + + + +});
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/home/home.component.ts b/catalog-ui/src/app/ng2/pages/home/home.component.ts new file mode 100644 index 0000000000..1b69eba929 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/home/home.component.ts @@ -0,0 +1,358 @@ +/*- + * ============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 as NgComponent, Inject, OnInit } from '@angular/core'; +import { Component, IConfigRoles, IUserProperties, Resource } from 'app/models'; +import { HomeFilter } from 'app/models/home-filter'; +import { AuthenticationService, CacheService, HomeService } from 'app/services-ng2'; +import { ModalsHandler } from 'app/utils'; +import { SdcUiServices } from 'onap-ui-angular'; +import { CHANGE_COMPONENT_CSAR_VERSION_FLAG, ComponentType, ResourceType } from '../../../utils/constants'; +import { ImportVSPService } from '../../components/modals/onboarding-modal/import-vsp.service'; +import { ISdcConfig, SdcConfigToken } from '../../config/sdc-config.config'; +import { IAppMenu, SdcMenuToken } from '../../config/sdc-menu.config'; +import { EntityFilterPipe } from '../../pipes/entity-filter.pipe'; +import { TranslateService } from '../../shared/translator/translate.service'; +import { FoldersItemsMenu, FoldersItemsMenuGroup, FoldersMenu } from './folders'; + +@NgComponent({ + selector: 'home-page', + templateUrl: './home.component.html', + styleUrls: ['./home.component.less'] +}) +export class HomeComponent implements OnInit { + public numberOfItemToDisplay: number; + public homeItems: Component[]; + public homeFilteredItems: Component[]; + public homeFilteredSlicedItems: Component[]; + public folders: FoldersMenu; + public roles: IConfigRoles; + public user: IUserProperties; + public showTutorial: boolean; + public isFirstTime: boolean; + public version: string; + public homeFilter: HomeFilter; + public vfcmtType: string; + public displayActions: boolean; + + constructor( + @Inject(SdcConfigToken) private sdcConfig: ISdcConfig, + @Inject(SdcMenuToken) public sdcMenu: IAppMenu, + @Inject('$state') private $state: ng.ui.IStateService, + private homeService: HomeService, + private authService: AuthenticationService, + private cacheService: CacheService, + private translateService: TranslateService, + private modalsHandler: ModalsHandler, + private modalService: SdcUiServices.ModalService, + private loaderService: SdcUiServices.LoaderService, + private importVSPService: ImportVSPService + ) {} + + ngOnInit(): void { + this.initHomeComponentVars(); + this.initFolders(); + this.initEntities(); + + if (this.$state.params) { + if (this.$state.params.folder) { + const folderName = this.$state.params.folder.replaceAll('_', ' '); + + const selectedFolder = this.folders.getFolders().find((tmpFolder: FoldersItemsMenu) => tmpFolder.text === folderName); + if (selectedFolder) { + this.setSelectedFolder(selectedFolder); + } + // Show the tutorial if needed when the dashboard page is opened.<script src="bower_components/angular-filter/dist/angular-filter.min.js"></script> + // This is called from the welcome page. + } else if (this.$state.params.show === 'tutorial') { + this.showTutorial = true; + this.isFirstTime = true; + } + } + } + + // Open onboarding modal + public notificationIconCallback(): void { + this.importVSPService.openOnboardingModal().subscribe((result) => { + if (!result.previousComponent || result.previousComponent.csarVersion !== result.componentCsar.csarVersion) { + this.cacheService.set(CHANGE_COMPONENT_CSAR_VERSION_FLAG, result.componentCsar.csarVersion); + } + this.$state.go('workspace.general', { + id: result.previousComponent && result.previousComponent.uniqueId, + componentCsar: result.componentCsar, + type: result.type + }); + }); + } + + public onImportVf(file: any): void { + if (file && file.filename) { + // Check that the file has valid extension. + const fileExtension: string = file.filename.split('.').pop(); + if (this.sdcConfig.csarFileExtension.indexOf(fileExtension.toLowerCase()) !== -1) { + this.$state.go('workspace.general', { + type: ComponentType.RESOURCE.toLowerCase(), + importedFile: file, + resourceType: ResourceType.VF + }); + } else { + const title: string = this.translateService.translate('NEW_SERVICE_RESOURCE_ERROR_VALID_CSAR_EXTENSIONS_TITLE'); + const message: string = this.translateService.translate('NEW_SERVICE_RESOURCE_ERROR_VALID_CSAR_EXTENSIONS', {extensions: this.sdcConfig.csarFileExtension}); + this.modalService.openWarningModal(title, message, 'error-invalid-csar-ext'); + } + } + } + + public onImportVfc(file: any): void { + if (file && file.filename) { + // Check that the file has valid extension. + const fileExtension: string = file.filename.split('.').pop(); + if (this.sdcConfig.toscaFileExtension.indexOf(fileExtension.toLowerCase()) !== -1) { + this.$state.go('workspace.general', { + type: ComponentType.RESOURCE.toLowerCase(), + importedFile: file, + resourceType: ResourceType.VFC + }); + } else { + const title: string = this.translateService.translate('NEW_SERVICE_RESOURCE_ERROR_VALID_TOSCA_EXTENSIONS_TITLE'); + const message: string = this.translateService.translate('NEW_SERVICE_RESOURCE_ERROR_VALID_TOSCA_EXTENSIONS', {extensions: this.sdcConfig.toscaFileExtension}); + this.modalService.openWarningModal(title, message, 'error-invalid-tosca-ext'); + } + } + } + + public openCreateModal(componentType: string, importedFile: any): void { + if (importedFile) { + this.initEntities(true); // Return from import + } else { + this.$state.go('workspace.general', {type: componentType.toLowerCase()}); + } + } + + public createPNF(): void { + this.$state.go('workspace.general', { + type: ComponentType.RESOURCE.toLowerCase(), + resourceType: ResourceType.PNF + }); + } + + public createCR(): void { + this.$state.go('workspace.general', { + type: ComponentType.RESOURCE.toLowerCase(), + resourceType: ResourceType.CR + }); + } + + public entitiesCount(folderItem: FoldersItemsMenu): any { + let total: number = 0; + if (folderItem.isGroup()) { + this.folders.getFolders().forEach((tmpFolder: FoldersItemsMenu) => { + if (tmpFolder.group && tmpFolder.group === (folderItem as FoldersItemsMenuGroup).groupname) { + total = total + this._getTotalCounts(tmpFolder); + } + }); + } else { + total = total + this._getTotalCounts(folderItem); + } + return total; + } + + public updateFilter = () => { + this.$state.go('.', this.homeFilter.toUrlParam(), {location: 'replace', notify: false}); + this.filterHomeItems(); + } + + public getCurrentFolderDistributed(): any[] { + const states = []; + if (this.folders) { + const folderItem: FoldersItemsMenu = this.folders.getCurrentFolder(); + if (folderItem.isGroup()) { + this.folders.getFolders().forEach((tmpFolder: FoldersItemsMenu) => { + if (tmpFolder.group && tmpFolder.group === (folderItem as FoldersItemsMenuGroup).groupname) { + this._setStates(tmpFolder, states); + } + }); + } else { + this._setStates(folderItem, states); + } + } + return states; + } + + public setSelectedFolder(folderItem: FoldersItemsMenu): void { + this.folders.setSelected(folderItem); + } + + public goToComponent(component: Component): void { + const loaderService = this.loaderService; + loaderService.activate(); + this.$state.go('workspace.general', {id: component.uniqueId, type: component.componentType.toLowerCase()}).then(() => { + loaderService.deactivate(); + }); + } + + public raiseNumberOfElementToDisplay(recalculate: boolean = false) { + const scrollPageAmount = 35; + if (!this.homeItems) { + this.numberOfItemToDisplay = 0; + } else if (this.homeItems.length > this.numberOfItemToDisplay || recalculate) { + let fullPagesAmount = Math.ceil(this.numberOfItemToDisplay / scrollPageAmount) * scrollPageAmount; + if (!recalculate || fullPagesAmount === 0) { // TODO trigger infiniteScroll to check bottom and fire onBottomHit by itself (sdc-ui) + fullPagesAmount += scrollPageAmount; + } + this.numberOfItemToDisplay = Math.min(this.homeItems.length, fullPagesAmount); + this.homeFilteredSlicedItems = this.homeFilteredItems.slice(0, this.numberOfItemToDisplay); + } + } + + public changeCheckboxesFilter(checkboxesFilterArray: string[], checkboxValue: string, checked?: boolean) { + const checkboxIdx = checkboxesFilterArray.indexOf(checkboxValue); + + checked = (checked !== undefined) ? checked : checkboxIdx === -1; + if (checked && checkboxIdx === -1) { + checkboxesFilterArray.push(checkboxValue); + } else if (!checked && checkboxIdx !== -1) { + checkboxesFilterArray.splice(checkboxIdx, 1); + } + this.updateFilter(); + } + + public changeFilterTerm(filterTerm: string): void { + this.homeFilter.search = { filterTerm }; + this.updateFilter(); + } + + public setDisplayActions(display?: boolean) { + this.displayActions = display !== undefined ? display : !this.displayActions; + } + + private _getTotalCounts(tmpFolder): number { + let total: number = 0; + if (tmpFolder.dist !== undefined) { + const distributions = tmpFolder.dist.split(','); + distributions.forEach((item: any) => { + total = total + this.getEntitiesByStateDist(tmpFolder.state, item).length; + }); + } else { + total = total + this.getEntitiesByStateDist(tmpFolder.state, tmpFolder.dist).length; + } + return total; + } + + private _setStates(tmpFolder, states) { + if (tmpFolder.states !== undefined) { + tmpFolder.states.forEach((item: any) => { + states.push({state: item.state, dist: item.dist}); + }); + } else { + states.push({state: tmpFolder.state, dist: tmpFolder.dist}); + } + } + + private initEntities(reload?: boolean) { + if (reload || this.componentShouldReload()) { + this.loaderService.activate(); + this.homeService.getAllComponents(true).subscribe( + (components: Component[]) => { + this.cacheService.set('breadcrumbsComponentsState', this.$state.current.name); // dashboard + this.cacheService.set('breadcrumbsComponents', components); + this.homeItems = components; + this.loaderService.deactivate(); + this.filterHomeItems(); + }, (error) => { this.loaderService.deactivate(); }); + } else { + this.homeItems = this.cacheService.get('breadcrumbsComponents'); + this.filterHomeItems(); + } + } + + private isDefaultFilter = (): boolean => { + const defaultFilter = new HomeFilter(); + return angular.equals(defaultFilter, this.homeFilter); + } + + private componentShouldReload = (): boolean => { + const breadcrumbsValid: boolean = (this.$state.current.name === this.cacheService.get('breadcrumbsComponentsState') && this.cacheService.contains('breadcrumbsComponents')); + return !breadcrumbsValid || this.isDefaultFilter(); + } + + private getEntitiesByStateDist(state: string, dist: string): Component[] { + let gObj: Component[]; + if (this.homeItems && (state || dist)) { + gObj = this.homeItems.filter((obj: Component) => { + if (dist !== undefined && obj.distributionStatus === dist && obj.lifecycleState === state) { + return true; + } else if (dist === undefined && (obj.lifecycleState === state || obj.distributionStatus === state)) { + return true; + } + return false; + }); + } else { + gObj = []; + } + return gObj; + } + + private filterHomeItems() { + this.homeFilteredItems = this.makeFilteredItems(this.homeItems, this.homeFilter); + this.raiseNumberOfElementToDisplay(true); + this.homeFilteredSlicedItems = this.homeFilteredItems.slice(0, this.numberOfItemToDisplay); + } + + private makeFilteredItems(homeItems: Component[], filter: HomeFilter) { + let filteredComponents: Component[] = homeItems; + + // filter: exclude all resources of type 'vfcmtType': + filteredComponents = filteredComponents.filter((c) => + !c.isResource() || (c as Resource).resourceType.indexOf(this.vfcmtType) === -1); + + // common entity filter + // -------------------------------------------------------------------------- + filteredComponents = EntityFilterPipe.transform(filteredComponents, filter); + + return filteredComponents; + } + + private initFolders = (): void => { + // Note: Do not use SdcUi.ChecklistComponent for folders checkboxes, since from the data structure + // it is not determined that all checkboxes under the same group are managed by the same selectedValues array. + if (this.user) { + this.folders = new FoldersMenu(this.roles[this.user.role].folder); + } + } + + private initHomeComponentVars(): void { + this.version = this.cacheService.get('version'); + this.numberOfItemToDisplay = 0; + this.displayActions = false; + this.user = this.authService.getLoggedinUser(); + this.roles = this.sdcMenu.roles; + this.showTutorial = false; + this.isFirstTime = false; + this.vfcmtType = ResourceType.VFCMT; + + // Checkboxes filter init + this.homeFilter = new HomeFilter(this.$state.params); + + // bind callbacks that are transferred as inputs + this.notificationIconCallback = this.notificationIconCallback.bind(this); + } + +} diff --git a/catalog-ui/src/app/ng2/pages/home/home.module.ts b/catalog-ui/src/app/ng2/pages/home/home.module.ts new file mode 100644 index 0000000000..3e7c0cd312 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/home/home.module.ts @@ -0,0 +1,31 @@ +import { NgModule } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { HomeComponent } from "./home.component"; +import { LayoutModule } from "../../components/layout/layout.module"; +import { UiElementsModule } from "../../components/ui/ui-elements.module"; +import { GlobalPipesModule } from "../../pipes/global-pipes.module"; +import { TranslateModule } from "../../shared/translator/translate.module"; +import { SdcUiComponentsModule } from "onap-ui-angular"; + +@NgModule({ + declarations: [ + HomeComponent + ], + imports: [ + CommonModule, + SdcUiComponentsModule, + LayoutModule, + UiElementsModule, + GlobalPipesModule, + TranslateModule + ], + exports: [ + HomeComponent + ], + entryComponents: [ + HomeComponent + ], + providers: [] +}) +export class HomeModule { +} diff --git a/catalog-ui/src/app/ng2/pages/interface-operation/interface-operation.module.ts b/catalog-ui/src/app/ng2/pages/interface-operation/interface-operation.module.ts index 6292d85422..941b10f943 100644 --- a/catalog-ui/src/app/ng2/pages/interface-operation/interface-operation.module.ts +++ b/catalog-ui/src/app/ng2/pages/interface-operation/interface-operation.module.ts @@ -1,9 +1,9 @@ import {NgModule} from "@angular/core"; import {CommonModule} from "@angular/common"; import {InterfaceOperationComponent} from "./interface-operation.page.component"; -import {SdcUiComponentsModule} from "sdc-ui/lib/angular/index"; import {UiElementsModule} from "app/ng2/components/ui/ui-elements.module"; import {TranslateModule} from "app/ng2/shared/translator/translate.module"; +import { SdcUiComponentsModule } from 'onap-ui-angular'; @NgModule({ declarations: [ diff --git a/catalog-ui/src/app/ng2/pages/interface-operation/interface-operation.page.component.html b/catalog-ui/src/app/ng2/pages/interface-operation/interface-operation.page.component.html index e32a0b60f5..cd06e18267 100644 --- a/catalog-ui/src/app/ng2/pages/interface-operation/interface-operation.page.component.html +++ b/catalog-ui/src/app/ng2/pages/interface-operation/interface-operation.page.component.html @@ -1,5 +1,5 @@ <!-- - ~ Copyright © 2016-2018 European Support Limited + ~ Copyright � 2016-2018 European Support Limited ~ ~ Licensed under the Apache License, Version 2.0 (the "License"); ~ you may not use this file except in compliance with the License. diff --git a/catalog-ui/src/app/ng2/pages/interface-operation/interface-operation.page.component.ts b/catalog-ui/src/app/ng2/pages/interface-operation/interface-operation.page.component.ts index c2a9582ed4..9d41c375f5 100644 --- a/catalog-ui/src/app/ng2/pages/interface-operation/interface-operation.page.component.ts +++ b/catalog-ui/src/app/ng2/pages/interface-operation/interface-operation.page.component.ts @@ -1,14 +1,14 @@ import * as _ from "lodash"; -import {Component, Input, Output, ComponentRef, Inject} from '@angular/core'; -import {Component as IComponent} from 'app/models/components/component'; +import { Component, Input, Output, ComponentRef, Inject } from '@angular/core'; +import {Component as IComponent } from 'app/models/components/component'; -import {SdcConfigToken, ISdcConfig} from "app/ng2/config/sdc-config.config"; -import {TranslateService} from "app/ng2/shared/translator/translate.service"; +import { SdcConfigToken, ISdcConfig } from "app/ng2/config/sdc-config.config"; +import {TranslateService } from "app/ng2/shared/translator/translate.service"; -import {Observable} from "rxjs/Observable"; +import {Observable } from "rxjs/Observable"; -import {ModalComponent} from 'app/ng2/components/ui/modal/modal.component'; -import {ModalService} from 'app/ng2/services/modal.service'; +import {ModalComponent } from 'app/ng2/components/ui/modal/modal.component'; +import {ModalService } from 'app/ng2/services/modal.service'; import { InputBEModel, OperationModel, @@ -18,15 +18,19 @@ import { Capability } from 'app/models'; -import {IModalConfig, IModalButtonComponent} from "sdc-ui/lib/angular/modals/models/modal-config"; -import {SdcUiComponents} from "sdc-ui/lib/angular"; -import {ModalButtonComponent} from "sdc-ui/lib/angular/components"; +// import {SdcUiComponents } from 'sdc-ui/lib/angular'; +// import {ModalButtonComponent } from 'sdc-ui/lib/angular/components'; +// import { IModalButtonComponent, IModalConfig } from 'sdc-ui/lib/angular/modals/models/modal-config'; -import {ComponentServiceNg2} from 'app/ng2/services/component-services/component.service'; -import {WorkflowServiceNg2} from 'app/ng2/services/workflow.service'; -import {PluginsService} from "app/ng2/services/plugins.service"; +import {ComponentServiceNg2 } from 'app/ng2/services/component-services/component.service'; +import {PluginsService } from 'app/ng2/services/plugins.service'; +import {WorkflowServiceNg2 } from 'app/ng2/services/workflow.service'; -import {OperationCreatorComponent, OperationCreatorInput} from 'app/ng2/pages/interface-operation/operation-creator/operation-creator.component'; +import { OperationCreatorComponent, OperationCreatorInput } from 'app/ng2/pages/interface-operation/operation-creator/operation-creator.component'; +import { IModalButtonComponent } from 'onap-ui-angular'; +import { ModalButtonComponent } from 'onap-ui-angular'; +import { IModalConfig } from 'onap-ui-angular'; +import { SdcUiServices } from 'onap-ui-angular'; export class UIOperationModel extends OperationModel { isCollapsed: boolean = true; @@ -61,6 +65,7 @@ export class UIOperationModel extends OperationModel { } } +// tslint:disable-next-line:max-classes-per-file class ModalTranslation { CREATE_TITLE: string; EDIT_TITLE: string; @@ -74,7 +79,7 @@ class ModalTranslation { constructor(private TranslateService: TranslateService) { this.TranslateService.languageChangedObservable.subscribe(lang => { this.CREATE_TITLE = this.TranslateService.translate("INTERFACE_CREATE_TITLE"); - this.EDIT_TITLE = this.TranslateService.translate("INTERFACE_EDIT_TITLE"); + this.EDIT_TITLE = this.TranslateService.translate('INTERFACE_EDIT_TITLE'); this.DELETE_TITLE = this.TranslateService.translate("INTERFACE_DELETE_TITLE"); this.CANCEL_BUTTON = this.TranslateService.translate("INTERFACE_CANCEL_BUTTON"); this.SAVE_BUTTON = this.TranslateService.translate("INTERFACE_SAVE_BUTTON"); @@ -85,6 +90,7 @@ class ModalTranslation { } } +// tslint:disable-next-line:max-classes-per-file export class UIInterfaceModel extends InterfaceModel { isCollapsed: boolean = false; @@ -92,7 +98,7 @@ export class UIInterfaceModel extends InterfaceModel { super(interf); this.operations = _.map( this.operations, - operation => new UIOperationModel(operation) + (operation) => new UIOperationModel(operation) ); } @@ -101,6 +107,7 @@ export class UIInterfaceModel extends InterfaceModel { } } +// tslint:disable-next-line:max-classes-per-file @Component({ selector: 'interface-operation', templateUrl: './interface-operation.page.component.html', @@ -110,16 +117,16 @@ export class UIInterfaceModel extends InterfaceModel { export class InterfaceOperationComponent { - interfaces: Array<UIInterfaceModel>; + interfaces: UIInterfaceModel[]; modalInstance: ComponentRef<ModalComponent>; openOperation: OperationModel; enableWorkflowAssociation: boolean; - inputs: Array<InputBEModel>; + inputs: InputBEModel[]; isLoading: boolean; - interfaceTypes:{ [interfaceType: string]: Array<string> }; + interfaceTypes: { [interfaceType: string]: string[] }; modalTranslation: ModalTranslation; workflowIsOnline: boolean; - workflows: Array<any>; + workflows: any[]; capabilities: CapabilitiesGroup; @Input() component: IComponent; @@ -135,7 +142,8 @@ export class InterfaceOperationComponent { private ComponentServiceNg2: ComponentServiceNg2, private WorkflowServiceNg2: WorkflowServiceNg2, private ModalServiceNg2: ModalService, - private ModalServiceSdcUI: SdcUiComponents.ModalService + private ModalServiceSdcUI: SdcUiServices.ModalService + ) { this.enableWorkflowAssociation = sdcConfig.enableWorkflowAssociation; this.modalTranslation = new ModalTranslation(TranslateService); @@ -146,11 +154,11 @@ export class InterfaceOperationComponent { this.workflowIsOnline = !_.isUndefined(this.PluginsService.getPluginByStateUrl('workflowDesigner')); Observable.forkJoin( - this.ComponentServiceNg2.getInterfaces(this.component), + this.ComponentServiceNg2.getInterfaceOperations(this.component), this.ComponentServiceNg2.getComponentInputs(this.component), this.ComponentServiceNg2.getInterfaceTypes(this.component), this.ComponentServiceNg2.getCapabilitiesAndRequirements(this.component.componentType, this.component.uniqueId) - ).subscribe((response: Array<any>) => { + ).subscribe((response: any[]) => { const callback = (workflows) => { this.isLoading = false; this.initInterfaces(response[0].interfaces); @@ -174,36 +182,36 @@ export class InterfaceOperationComponent { }); } - initInterfaces(interfaces: Array<InterfaceModel>): void { - this.interfaces = _.map(interfaces, interf => new UIInterfaceModel(interf)); + initInterfaces(interfaces: InterfaceModel[]): void { + this.interfaces = _.map(interfaces, (interf) => new UIInterfaceModel(interf)); } sortInterfaces(): void { - this.interfaces = _.filter(this.interfaces, interf => interf.operations && interf.operations.length > 0); // remove empty interfaces + this.interfaces = _.filter(this.interfaces, (interf) => interf.operations && interf.operations.length > 0); // remove empty interfaces this.interfaces.sort((a, b) => a.type.localeCompare(b.type)); // sort interfaces alphabetically - _.forEach(this.interfaces, interf => { + _.forEach(this.interfaces, (interf) => { interf.operations.sort((a, b) => a.name.localeCompare(b.name)); // sort operations alphabetically }); } collapseAll(value: boolean = true): void { - _.forEach(this.interfaces, interf => { + _.forEach(this.interfaces, (interf) => { interf.isCollapsed = value; }); } isAllCollapsed(): boolean { - return _.every(this.interfaces, interf => interf.isCollapsed); + return _.every(this.interfaces, (interf) => interf.isCollapsed); } isAllExpanded(): boolean { - return _.every(this.interfaces, interf => !interf.isCollapsed); + return _.every(this.interfaces, (interf) => !interf.isCollapsed); } isListEmpty(): boolean { return _.filter( this.interfaces, - interf => interf.operations && interf.operations.length > 0 + (interf) => interf.operations && interf.operations.length > 0 ).length === 0; } @@ -291,11 +299,6 @@ export class InterfaceOperationComponent { } - private enableOrDisableSaveButton = (shouldEnable: boolean): void => { - let saveButton: ModalButtonComponent = this.ModalServiceSdcUI.getCurrentInstance().getButtonById('saveButton'); - saveButton.disabled = !shouldEnable; - } - onRemoveOperation = (event: Event, operation: OperationModel): void => { event.stopPropagation(); @@ -303,11 +306,11 @@ export class InterfaceOperationComponent { this.ComponentServiceNg2 .deleteInterfaceOperation(this.component, operation) .subscribe(() => { - const curInterf = _.find(this.interfaces, interf => interf.type === operation.interfaceType); - const index = _.findIndex(curInterf.operations, el => el.uniqueId === operation.uniqueId); + const curInterf = _.find(this.interfaces, (interf) => interf.type === operation.interfaceType); + const index = _.findIndex(curInterf.operations, (el) => el.uniqueId === operation.uniqueId); curInterf.operations.splice(index, 1); if (!curInterf.operations.length) { - const interfIndex = _.findIndex(this.interfaces, interf => interf.type === operation.interfaceType); + const interfIndex = _.findIndex(this.interfaces, (interf) => interf.type === operation.interfaceType); this.interfaces.splice(interfIndex, 1); } }); @@ -322,13 +325,18 @@ export class InterfaceOperationComponent { ); } + private enableOrDisableSaveButton = (shouldEnable: boolean): void => { + const saveButton: ModalButtonComponent = this.ModalServiceSdcUI.getCurrentInstance().getButtonById('saveButton'); + saveButton.disabled = !shouldEnable; + } + private createOperation = (operation: OperationModel): void => { this.ComponentServiceNg2.createInterfaceOperation(this.component, operation).subscribe((response: OperationModel) => { this.openOperation = null; let curInterf = _.find( this.interfaces, - interf => interf.type === operation.interfaceType + (interf) => interf.type === operation.interfaceType ); if (!curInterf) { @@ -358,18 +366,19 @@ export class InterfaceOperationComponent { this.ComponentServiceNg2.updateInterfaceOperation(this.component, operation).subscribe((newOperation: OperationModel) => { this.openOperation = null; - let oldOpIndex, oldInterf; - _.forEach(this.interfaces, interf => { - _.forEach(interf.operations, op => { + let oldOpIndex; + let oldInterf; + _.forEach(this.interfaces, (interf) => { + _.forEach(interf.operations, (op) => { if (op.uniqueId === newOperation.uniqueId) { oldInterf = interf; - oldOpIndex = _.findIndex(interf.operations, el => el.uniqueId === op.uniqueId); + oldOpIndex = _.findIndex(interf.operations, (el) => el.uniqueId === op.uniqueId); } }) }); oldInterf.operations.splice(oldOpIndex, 1); - const newInterf = _.find(this.interfaces, interf => interf.type === operation.interfaceType); + const newInterf = _.find(this.interfaces, (interf) => interf.type === operation.interfaceType); const newOpModel = new UIOperationModel(newOperation); newInterf.operations.push(newOpModel); this.sortInterfaces(); diff --git a/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/operation-creator.component.html b/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/operation-creator.component.html index ec056ad6f2..df2a505fe8 100644 --- a/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/operation-creator.component.html +++ b/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/operation-creator.component.html @@ -180,6 +180,9 @@ <span class="bold-message">{{ 'EMPTY_PARAM_TABLE_NO_SELECTED_WORKFLOW_1' | translate }}</span> <span>{{ 'EMPTY_PARAM_TABLE_NO_SELECTED_WORKFLOW_2' | translate }}</span> </div> + <div *ngIf="!workflows.length"> + Only <span class="bold-message">certified</span> workflow versions can be assigned to an operation + </div> </div> </div> diff --git a/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/operation-creator.component.less b/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/operation-creator.component.less index f2bd0f82af..2721d300c4 100644 --- a/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/operation-creator.component.less +++ b/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/operation-creator.component.less @@ -11,7 +11,7 @@ font-size: 12px; } - .w-sdc-form .form-item { + .w-sdc-form .i-sdc-form-item { margin-bottom: 15px; } diff --git a/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/operation-creator.component.ts b/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/operation-creator.component.ts index e12905654b..12fba24e86 100644 --- a/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/operation-creator.component.ts +++ b/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/operation-creator.component.ts @@ -15,9 +15,9 @@ import { Capability } from 'app/models'; -import {IDropDownOption} from "sdc-ui/lib/angular/form-elements/dropdown/dropdown-models"; import {Tabs, Tab} from "app/ng2/components/ui/tabs/tabs.component"; import {DropdownValue} from "app/ng2/components/ui/form-components/dropdown/ui-element-dropdown.component"; +import { IDropDownOption } from 'onap-ui-angular'; export class DropDownOption implements IDropDownOption { value: string; diff --git a/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/operation-creator.module.ts b/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/operation-creator.module.ts index 0b6f8336c3..b91f3aa4e3 100644 --- a/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/operation-creator.module.ts +++ b/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/operation-creator.module.ts @@ -3,10 +3,10 @@ import {CommonModule} from "@angular/common"; import {FormsModule} from "@angular/forms"; import {FormElementsModule} from "app/ng2/components/ui/form-components/form-elements.module"; -import {SdcUiComponentsModule} from "sdc-ui/lib/angular/index"; -import {UiElementsModule} from "app/ng2/components/ui/ui-elements.module"; import {TranslateModule} from "app/ng2/shared/translator/translate.module"; +import { SdcUiComponentsModule } from 'onap-ui-angular'; +import { UiElementsModule } from '../../../components/ui/ui-elements.module'; import {OperationCreatorComponent} from "./operation-creator.component"; import {ParamRowComponent} from './param-row/param-row.component'; diff --git a/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/param-row/param-row.component.html b/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/param-row/param-row.component.html index 4a4782eaee..b8173eaf15 100644 --- a/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/param-row/param-row.component.html +++ b/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/param-row/param-row.component.html @@ -1,5 +1,5 @@ <!-- - ~ Copyright © 2016-2018 European Support Limited + ~ Copyright © 2016-2018 European Support Limited ~ ~ Licensed under the Apache License, Version 2.0 (the "License"); ~ you may not use this file except in compliance with the License. @@ -13,7 +13,7 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> - + <div class="cell field-name"> <ui-element-input *ngIf="!isAssociateWorkflow" diff --git a/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/param-row/param-row.component.less b/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/param-row/param-row.component.less index f6cda17777..5447fe532b 100644 --- a/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/param-row/param-row.component.less +++ b/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/param-row/param-row.component.less @@ -32,6 +32,7 @@ input { height: 30px; + border: none; padding-left: 10px; } diff --git a/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/param-row/param-row.component.ts b/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/param-row/param-row.component.ts index d32edc78af..de6e703404 100644 --- a/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/param-row/param-row.component.ts +++ b/catalog-ui/src/app/ng2/pages/interface-operation/operation-creator/param-row/param-row.component.ts @@ -1,4 +1,5 @@ import {Component, Input} from '@angular/core'; +import {PROPERTY_DATA} from "app/utils"; import {DataTypeService} from "app/ng2/services/data-type.service"; import {OperationModel, OperationParameter, InputBEModel, DataTypeModel, Capability} from 'app/models'; import {DropdownValue} from "app/ng2/components/ui/form-components/dropdown/ui-element-dropdown.component"; @@ -36,7 +37,7 @@ export class ParamRowComponent { filteredInputProps: Array<DropdownValue> = []; filteredCapabilitiesProps: Array<{capabilityName: string, properties: Array<DropdownValueType>}> = []; - constructor(private dataTypeService: DataTypeService) {} + constructor(private dataTypeService:DataTypeService) {} ngOnInit() { if (this.isInputParam) { diff --git a/catalog-ui/src/app/ng2/pages/page404/page404.component.html b/catalog-ui/src/app/ng2/pages/page404/page404.component.html index 278ab4d551..a9335a59c4 100644 --- a/catalog-ui/src/app/ng2/pages/page404/page404.component.html +++ b/catalog-ui/src/app/ng2/pages/page404/page404.component.html @@ -13,7 +13,6 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> - <div class="page404"> Page404 </div> diff --git a/catalog-ui/src/app/ng2/pages/plugin-not-connected/plugin-not-connected.component.html b/catalog-ui/src/app/ng2/pages/plugin-not-connected/plugin-not-connected.component.html index 98e896f4dc..0f8aeb3a13 100644 --- a/catalog-ui/src/app/ng2/pages/plugin-not-connected/plugin-not-connected.component.html +++ b/catalog-ui/src/app/ng2/pages/plugin-not-connected/plugin-not-connected.component.html @@ -13,8 +13,6 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> - - <div class="plugin-not-connected"> <div class="plugin-error-message"> <div class="icon-wrapper"> diff --git a/catalog-ui/src/app/ng2/pages/plugins/plugin-context-view/plugin-context-view.page.component.html b/catalog-ui/src/app/ng2/pages/plugins/plugin-context-view/plugin-context-view.page.component.html new file mode 100644 index 0000000000..85e83c4310 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/plugins/plugin-context-view/plugin-context-view.page.component.html @@ -0,0 +1,4 @@ +<div class="workspace-plugins"> + <plugin-frame (onLoadingDone)="onLoadingDone(plugin)" [plugin]="plugin" [queryParams]="queryParams"></plugin-frame> + <loader [display]="isLoading && plugin.isOnline" ></loader> +</div>
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/plugins/plugin-context-view/plugin-context-view.page.component.less b/catalog-ui/src/app/ng2/pages/plugins/plugin-context-view/plugin-context-view.page.component.less new file mode 100644 index 0000000000..c913af1931 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/plugins/plugin-context-view/plugin-context-view.page.component.less @@ -0,0 +1,2 @@ +.workspace-plugins { +} diff --git a/catalog-ui/src/app/ng2/pages/plugins/plugin-context-view/plugin-context-view.page.component.ts b/catalog-ui/src/app/ng2/pages/plugins/plugin-context-view/plugin-context-view.page.component.ts new file mode 100644 index 0000000000..21aa8584d5 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/plugins/plugin-context-view/plugin-context-view.page.component.ts @@ -0,0 +1,58 @@ +import {Component, Inject} from "@angular/core"; +import {Component as ComponentData, IUserProperties, Plugin} from "app/models"; +import {CacheService, PluginsService} from "app/services-ng2"; + + +@Component({ + selector: 'plugin-context-view', + templateUrl: './plugin-context-view.page.component.html', + styleUrls: ['./plugin-context-view.page.component.less'] +}) + +export class PluginContextViewPageComponent { + plugin: Plugin; + user: IUserProperties; + queryParams: Object; + isLoading: boolean; + show: boolean; + component: ComponentData; + + constructor(@Inject("$stateParams") private _stateParams, + private cacheService: CacheService, + private pluginsService: PluginsService) { + + this.show = false; + this.component = this._stateParams.component; + this.plugin = this.pluginsService.getPluginByStateUrl(_stateParams.path); + this.user = this.cacheService.get('user'); + } + + ngOnInit() { + this.isLoading = true; + + this.queryParams = { + userId: this.user.userId, + userRole: this.user.role, + displayType: "context", + contextType: this.component.getComponentSubType(), + uuid: this.component.uuid, + lifecycleState: this.component.lifecycleState, + isOwner: this.component.lastUpdaterUserId === this.user.userId, + version: this.component.version, + parentUrl: window.location.origin, + eventsClientId: this.plugin.pluginId + }; + + if (this._stateParams.queryParams) { + _.assign(this.queryParams, this._stateParams.queryParams); + } + } + + onLoadingDone(plugin: Plugin) { + if (plugin.pluginId == this.plugin.pluginId) { + this.isLoading = false; + } + } + + +} diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-tabs.component.html b/catalog-ui/src/app/ng2/pages/plugins/plugin-tab-view/plugin-tab-view.page.component.html index 8d1730f68c..5ce95d11f8 100644 --- a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-tabs.component.html +++ b/catalog-ui/src/app/ng2/pages/plugins/plugin-tab-view/plugin-tab-view.page.component.html @@ -12,17 +12,9 @@ ~ 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. - --> - -<sdc-tabs> - <sdc-tab titleIcon="info-circle"> - <policy-information-tab [policy]="policy" [isViewOnly]="isViewOnly" *ngIf="policy"></policy-information-tab> - </sdc-tab> - <sdc-tab titleIcon="inputs-o"> - <policy-targets-tab [policy]="policy" [topologyTemplate]="topologyTemplate" [isViewOnly]="isViewOnly" (isLoading)="setIsLoading($event)" *ngIf="policy"></policy-targets-tab> - </sdc-tab> - <sdc-tab titleIcon="settings-o"> - <policy-properties-tab [policy]="policy" [topologyTemplate]="topologyTemplate" [isViewOnly]="isViewOnly" *ngIf="policy"></policy-properties-tab> - </sdc-tab> -</sdc-tabs> - +--> +<div class="sdc-catalog-container plugins-tab-container"> + <top-nav [version]="version" [hideSearch]="true"></top-nav> + <plugin-frame (onLoadingDone)="onLoadingDone(plugin)" [plugin]="plugin" [queryParams]="queryParams"></plugin-frame> + <loader [display]="isLoading"></loader> +</div> diff --git a/catalog-ui/src/app/ng2/pages/plugins/plugin-tab-view/plugin-tab-view.page.component.less b/catalog-ui/src/app/ng2/pages/plugins/plugin-tab-view/plugin-tab-view.page.component.less new file mode 100644 index 0000000000..3cb5d1b421 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/plugins/plugin-tab-view/plugin-tab-view.page.component.less @@ -0,0 +1,2 @@ +.plugins-tab-container { +} diff --git a/catalog-ui/src/app/ng2/pages/plugins/plugin-tab-view/plugin-tab-view.page.component.ts b/catalog-ui/src/app/ng2/pages/plugins/plugin-tab-view/plugin-tab-view.page.component.ts new file mode 100644 index 0000000000..7ba8474569 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/plugins/plugin-tab-view/plugin-tab-view.page.component.ts @@ -0,0 +1,45 @@ +import {Component, Inject} from "@angular/core"; +import {IUserProperties, Plugin} from "app/models"; +import {CacheService, PluginsService} from "app/services-ng2"; + +@Component({ + selector: 'plugin-tab-view', + templateUrl: './plugin-tab-view.page.component.html', + styleUrls: ['./plugin-tab-view.page.component.less'] +}) + +export class PluginTabViewPageComponent { + plugin: Plugin; + user: IUserProperties; + version: string; + queryParams: Object; + isLoading: boolean; + + constructor(@Inject("$stateParams") private _stateParams, + private cacheService: CacheService, + private pluginsService: PluginsService) { + + this.plugin = this.pluginsService.getPluginByStateUrl(_stateParams.path); + this.version = this.cacheService.get('version'); + this.user = this.cacheService.get('user'); + } + + ngOnInit() { + this.isLoading = true; + + this.queryParams = { + userId: this.user.userId, + userRole: this.user.role, + displayType: "tab", + parentUrl: window.location.origin, + eventsClientId: this.plugin.pluginId + }; + + } + + onLoadingDone(plugin: Plugin) { + if (plugin.pluginId == this.plugin.pluginId) { + this.isLoading = false; + } + } +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/plugins/plugins-module.ts b/catalog-ui/src/app/ng2/pages/plugins/plugins-module.ts new file mode 100644 index 0000000000..763e329789 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/plugins/plugins-module.ts @@ -0,0 +1,34 @@ +import {NgModule} from "@angular/core"; +import {PluginContextViewPageComponent} from "./plugin-context-view/plugin-context-view.page.component"; +import {PluginFrameModule} from "../../components/ui/plugin/plugin-frame.module"; +import {CommonModule} from "@angular/common"; +import {UiElementsModule} from "../../components/ui/ui-elements.module"; +import {PluginTabViewPageComponent} from "./plugin-tab-view/plugin-tab-view.page.component"; +import {LayoutModule} from "../../components/layout/layout.module"; +import {HttpModule} from "@angular/http"; + +@NgModule({ + declarations: [ + PluginContextViewPageComponent, + PluginTabViewPageComponent + ], + imports: [ + CommonModule, + PluginFrameModule, + UiElementsModule, + LayoutModule, + HttpModule + ], + exports: [ + PluginContextViewPageComponent, + PluginTabViewPageComponent + ], + entryComponents: [ + PluginContextViewPageComponent, + PluginTabViewPageComponent + ] +}) +export class PluginsModule { + +} + diff --git a/catalog-ui/src/app/ng2/pages/properties-assignment/declare-list/declare-list.component.ts b/catalog-ui/src/app/ng2/pages/properties-assignment/declare-list/declare-list.component.ts index 20e04f84b6..fe3106649b 100644 --- a/catalog-ui/src/app/ng2/pages/properties-assignment/declare-list/declare-list.component.ts +++ b/catalog-ui/src/app/ng2/pages/properties-assignment/declare-list/declare-list.component.ts @@ -18,97 +18,95 @@ * ============LICENSE_END========================================================= */ -import * as _ from "lodash"; -import {Component} from '@angular/core'; -import {DropdownValue} from "app/ng2/components/ui/form-components/dropdown/ui-element-dropdown.component"; -import { DataTypeService } from "app/ng2/services/data-type.service"; -import {PropertyBEModel, DataTypesMap} from "app/models"; -import {PROPERTY_DATA} from "app/utils"; -import {PROPERTY_TYPES} from "../../../../utils"; -import { ModalService } from "app/ng2/services/modal.service"; -import { InstancePropertiesAPIMap } from "app/models/properties-inputs/property-fe-map"; -import { ModalModel } from "app/models/modal"; -import { DataTypeModel } from "app/models/data-types"; - - +import { Component } from '@angular/core'; +import { DataTypesMap, PropertyBEModel } from 'app/models'; +import { DataTypeModel } from 'app/models/data-types'; +import { ModalModel } from 'app/models/modal'; +import { InstancePropertiesAPIMap } from 'app/models/properties-inputs/property-fe-map'; +import { DropdownValue } from 'app/ng2/components/ui/form-components/dropdown/ui-element-dropdown.component'; +import { DataTypeService } from 'app/ng2/services/data-type.service'; +import { ModalService } from 'app/ng2/services/modal.service'; +import { PROPERTY_DATA } from 'app/utils'; +import * as _ from 'lodash'; +import { PROPERTY_TYPES } from '../../../../utils'; @Component({ selector: 'declare-list', templateUrl: './declare-list.component.html', - styleUrls:['./declare-list.component.less'], + styleUrls: ['./declare-list.component.less'], }) export class DeclareListComponent { - typesProperties: Array<DropdownValue>; - typesSchemaProperties: Array<DropdownValue>; + typesProperties: DropdownValue[]; + typesSchemaProperties: DropdownValue[]; propertyModel: PropertyBEModel; - //propertyNameValidationPattern:RegExp = /^[a-zA-Z0-9_:-]{1,50}$/; - //commentValidationPattern:RegExp = /^[\u0000-\u00BF]*$/; - //types:Array<string>; - dataTypes:DataTypesMap; - isLoading:boolean; - inputsToCreate:InstancePropertiesAPIMap; - propertiesListString:string; + // propertyNameValidationPattern:RegExp = /^[a-zA-Z0-9_:-]{1,50}$/; + // commentValidationPattern:RegExp = /^[\u0000-\u00BF]*$/; + // types:Array<string>; + dataTypes: DataTypesMap; + isLoading: boolean; + inputsToCreate: InstancePropertiesAPIMap; + propertiesListString: string; privateDataType: DataTypeModel; - constructor(protected dataTypeService:DataTypeService, private modalService:ModalService) {} + constructor(protected dataTypeService: DataTypeService, private modalService: ModalService) {} ngOnInit() { console.log('DeclareListComponent.ngOnInit() - enter'); this.propertyModel = new PropertyBEModel(); this.propertyModel.type = ''; this.propertyModel.schema.property.type = ''; - const types: Array<string> = PROPERTY_DATA.TYPES; //All types - simple type + map + list - this.dataTypes = this.dataTypeService.getAllDataTypes(); //Get all data types in service - const nonPrimitiveTypes :Array<string> = _.filter(Object.keys(this.dataTypes), (type:string)=> { - return types.indexOf(type) == -1; + const types: string[] = PROPERTY_DATA.TYPES; // All types - simple type + map + list + this.dataTypes = this.dataTypeService.getAllDataTypes(); // Get all data types in service + const nonPrimitiveTypes: string[] = _.filter(Object.keys(this.dataTypes), (type: string) => { + return types.indexOf(type) === -1; }); this.typesProperties = _.map(PROPERTY_DATA.TYPES, (type: string) => new DropdownValue(type, type) ); - let typesSimpleProperties = _.map(PROPERTY_DATA.SIMPLE_TYPES, + const typesSimpleProperties = _.map(PROPERTY_DATA.SIMPLE_TYPES, (type: string) => new DropdownValue(type, type) ); - let nonPrimitiveTypesValues = _.map(nonPrimitiveTypes, + const nonPrimitiveTypesValues = _.map(nonPrimitiveTypes, (type: string) => new DropdownValue(type, - type.replace("org.openecomp.datatypes.heat.","")) + type.replace('org.openecomp.datatypes.heat.',"")) ); - this.typesProperties = _.concat(this.typesProperties,nonPrimitiveTypesValues); - this.typesSchemaProperties = _.concat(typesSimpleProperties,nonPrimitiveTypesValues); - this.typesProperties.unshift(new DropdownValue('','Select Type...')); - this.typesSchemaProperties.unshift(new DropdownValue('','Select Schema Type...')); + this.typesProperties = _.concat(this.typesProperties, nonPrimitiveTypesValues); + this.typesSchemaProperties = _.concat(typesSimpleProperties, nonPrimitiveTypesValues); + this.typesProperties.unshift(new DropdownValue('', 'Select Type...')); + this.typesSchemaProperties.unshift(new DropdownValue('', 'Select Schema Type...')); this.inputsToCreate = this.modalService.currentModal.instance.dynamicContent.instance.input.properties; this.propertiesListString = this.modalService.currentModal.instance.dynamicContent.instance.input.propertyNameList.join(", "); this.privateDataType = new DataTypeModel(null); - this.privateDataType.name = "datatype"; + this.privateDataType.name = 'datatype'; console.log('DeclareListComponent.ngOnInit() - leave'); } - checkFormValidForSubmit(){ - const showSchema:boolean = this.showSchema(); - let isSchemaValid: boolean = (showSchema && !this.propertyModel.schema.property.type)? false : true; - if (!showSchema){ + checkFormValidForSubmit() { + const showSchema: boolean = this.showSchema(); + const isSchemaValid: boolean = (showSchema && !this.propertyModel.schema.property.type) ? false : true; + if (!showSchema) { this.propertyModel.schema.property.type = ''; } return this.propertyModel.name && this.propertyModel.type && isSchemaValid; } - showSchema():boolean { + showSchema(): boolean { return [PROPERTY_TYPES.LIST, PROPERTY_TYPES.MAP].indexOf(this.propertyModel.type) > -1; - }; + } - onSchemaTypeChange():void { - if (this.propertyModel.type == PROPERTY_TYPES.MAP) { + onSchemaTypeChange(): void { + if (this.propertyModel.type === PROPERTY_TYPES.MAP) { this.propertyModel.value = JSON.stringify({'': null}); - } else if (this.propertyModel.type == PROPERTY_TYPES.LIST) { + } else if (this.propertyModel.type === PROPERTY_TYPES.LIST) { this.propertyModel.value = JSON.stringify([]); } - }; + } } diff --git a/catalog-ui/src/app/ng2/pages/properties-assignment/declare-list/declare-list.module.ts b/catalog-ui/src/app/ng2/pages/properties-assignment/declare-list/declare-list.module.ts index 54af76a9f5..97667f9261 100644 --- a/catalog-ui/src/app/ng2/pages/properties-assignment/declare-list/declare-list.module.ts +++ b/catalog-ui/src/app/ng2/pages/properties-assignment/declare-list/declare-list.module.ts @@ -18,13 +18,13 @@ * ============LICENSE_END========================================================= */ -import {NgModule} from "@angular/core"; -import {CommonModule} from "@angular/common"; -import {DeclareListComponent} from "./declare-list.component"; -import {FormsModule} from "@angular/forms"; -import {FormElementsModule} from "app/ng2/components/ui/form-components/form-elements.module"; -import {UiElementsModule} from "app/ng2/components/ui/ui-elements.module"; -import {TranslateModule} from "../../../shared/translator/translate.module"; +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { FormElementsModule } from 'app/ng2/components/ui/form-components/form-elements.module'; +import { UiElementsModule } from 'app/ng2/components/ui/ui-elements.module'; +import { TranslateModule } from '../../../shared/translator/translate.module'; +import { DeclareListComponent } from './declare-list.component'; @NgModule({ declarations: [ diff --git a/catalog-ui/src/app/ng2/pages/properties-assignment/properties-assignment.module.ts b/catalog-ui/src/app/ng2/pages/properties-assignment/properties-assignment.module.ts index c46d617b86..f5500d42ae 100644 --- a/catalog-ui/src/app/ng2/pages/properties-assignment/properties-assignment.module.ts +++ b/catalog-ui/src/app/ng2/pages/properties-assignment/properties-assignment.module.ts @@ -19,7 +19,6 @@ */ import { NgModule } from "@angular/core"; import {HierarchyNavigationComponent} from "../../components/logic/hierarchy-navigtion/hierarchy-navigation.component"; -import {HttpModule} from "@angular/http"; import {FormsModule} from "@angular/forms"; import {PropertyTableModule} from "../../components/logic/properties-table/property-table.module"; import {UiElementsModule} from "../../components/ui/ui-elements.module"; @@ -46,12 +45,11 @@ import {ComponentModeService} from "../../services/component-services/component- imports: [ BrowserModule, FormsModule, - HttpModule, GlobalPipesModule, PropertyTableModule, PoliciesTableModule, UiElementsModule], - + entryComponents: [PropertiesAssignmentComponent], exports: [ PropertiesAssignmentComponent diff --git a/catalog-ui/src/app/ng2/pages/properties-assignment/properties-assignment.page.component.html b/catalog-ui/src/app/ng2/pages/properties-assignment/properties-assignment.page.component.html index 580c36284b..8d4215aaec 100644 --- a/catalog-ui/src/app/ng2/pages/properties-assignment/properties-assignment.page.component.html +++ b/catalog-ui/src/app/ng2/pages/properties-assignment/properties-assignment.page.component.html @@ -18,7 +18,7 @@ <div class="main-content"> <div class="left-column"> <div class="main-tabs-section"> - <tabs #propertyInputTabs tabStyle="round-tabs" (tabChanged)="tabChanged($event)" [hideIndicationOnTabChange]="true"> + <tabs #propertyInputTabs tabStyle="round-tabs" (tabChanged)="tabChanged($event)" [hideIndicationOnTabChange]="true" > <tab tabTitle="Properties"> <properties-table class="properties-table" [fePropertiesMap]="instanceFePropertiesMap" @@ -42,12 +42,13 @@ </tab> <tab tabTitle="Inputs"> <inputs-table class="properties-table" - [readonly]="isReadonly" - [inputs]="inputs | searchFilter:'name':searchQuery" - [instanceNamesMap]="componentInstanceNamesMap" - [isLoading]="loadingInputs" - (deleteInput)="deleteInput($event)" - (inputChanged)="dataChanged($event)"> + [fePropertiesMap]="instanceFePropertiesMap" + [readonly]="isReadonly" + [inputs]="inputs | searchFilter:'name':searchQuery" + [instanceNamesMap]="componentInstanceNamesMap" + [isLoading]="loadingInputs" + (deleteInput)="deleteInput($event)" + (inputChanged)="dataChanged($event)"> </inputs-table> </tab> <tab tabTitle="Policies"> diff --git a/catalog-ui/src/app/ng2/pages/properties-assignment/properties-assignment.page.component.less b/catalog-ui/src/app/ng2/pages/properties-assignment/properties-assignment.page.component.less index 855bdc5bcb..a1309aba61 100644 --- a/catalog-ui/src/app/ng2/pages/properties-assignment/properties-assignment.page.component.less +++ b/catalog-ui/src/app/ng2/pages/properties-assignment/properties-assignment.page.component.less @@ -133,13 +133,12 @@ flex-direction:column; margin: 0px 0 0 1em; overflow-x:auto; - .add-btn{ + .add-btn{ align-self: flex-end; margin-top: 10px; margin-bottom: 19px; } - /deep/ .tabs { border-bottom: solid 1px #d0d0d0; } diff --git a/catalog-ui/src/app/ng2/pages/properties-assignment/properties-assignment.page.component.ts b/catalog-ui/src/app/ng2/pages/properties-assignment/properties-assignment.page.component.ts index 061439800f..4b84f0e66f 100644 --- a/catalog-ui/src/app/ng2/pages/properties-assignment/properties-assignment.page.component.ts +++ b/catalog-ui/src/app/ng2/pages/properties-assignment/properties-assignment.page.component.ts @@ -19,58 +19,34 @@ */ import * as _ from "lodash"; -import {Component, ViewChild, Inject, TemplateRef} from "@angular/core"; +import { Component, ViewChild, Inject, TemplateRef } from "@angular/core"; import { PropertiesService } from "../../services/properties.service"; -import { - PropertyFEModel, - InstanceFePropertiesMap, - InstanceBePropertiesMap, - InstancePropertiesAPIMap, - Component as ComponentData, - FilterPropertiesAssignmentData, - ModalModel, - ButtonModel, - Capability, - ToscaPresentationData -} from "app/models"; +import { PropertyFEModel, InstanceFePropertiesMap, InstanceBePropertiesMap, InstancePropertiesAPIMap, Component as ComponentData, FilterPropertiesAssignmentData, ModalModel, ButtonModel } from "app/models"; import { ResourceType } from "app/utils"; -import {ComponentServiceNg2} from "../../services/component-services/component.service"; -import {ComponentInstanceServiceNg2} from "../../services/component-instance-services/component-instance.service" -import { - InputBEModel, - InputFEModel, - ComponentInstance, - GroupInstance, - PolicyInstance, - PropertyBEModel, - DerivedFEProperty, - SimpleFlatProperty, - CapabilitiesGroup -} from "app/models"; +import { ComponentServiceNg2 } from "../../services/component-services/component.service"; +import { TopologyTemplateService } from "../../services/component-services/topology-template.service"; +import { ComponentInstanceServiceNg2 } from "../../services/component-instance-services/component-instance.service" +import { InputBEModel, InputFEModel, ComponentInstance, GroupInstance, PolicyInstance, PropertyBEModel, DerivedFEProperty, SimpleFlatProperty } from "app/models"; import { KeysPipe } from 'app/ng2/pipes/keys.pipe'; -import {WorkspaceMode, EVENTS} from "../../../utils/constants"; -import {EventListenerService} from "app/services/event-listener-service" -import {HierarchyDisplayOptions} from "../../components/logic/hierarchy-navigtion/hierarchy-display-options"; -import {FilterPropertiesAssignmentComponent} from "../../components/logic/filter-properties-assignment/filter-properties-assignment.component"; -import {PropertyRowSelectedEvent} from "../../components/logic/properties-table/properties-table.component"; -import {HierarchyNavService} from "./services/hierarchy-nav.service"; -import {PropertiesUtils} from "./services/properties.utils"; -import {ComponentModeService} from "../../services/component-services/component-mode.service"; -import {ModalService} from "../../services/modal.service"; -import {Tabs, Tab} from "../../components/ui/tabs/tabs.component"; -import {InputsUtils} from "./services/inputs.utils"; -import {PropertyCreatorComponent} from "./property-creator/property-creator.component"; -import {DeclareListComponent} from "./declare-list/declare-list.component"; +import { WorkspaceMode, EVENTS, PROPERTY_TYPES } from "../../../utils/constants"; +import { EventListenerService } from "app/services/event-listener-service" +import { HierarchyDisplayOptions } from "../../components/logic/hierarchy-navigtion/hierarchy-display-options"; +import { FilterPropertiesAssignmentComponent } from "../../components/logic/filter-properties-assignment/filter-properties-assignment.component"; +import { PropertyRowSelectedEvent } from "../../components/logic/properties-table/properties-table.component"; +import { HierarchyNavService } from "./services/hierarchy-nav.service"; +import { PropertiesUtils } from "./services/properties.utils"; +import { ComponentModeService } from "../../services/component-services/component-mode.service"; +import { Tabs, Tab } from "../../components/ui/tabs/tabs.component"; +import { InputsUtils } from "./services/inputs.utils"; import { InstanceFeDetails } from "../../../models/instance-fe-details"; -import { SdcUiComponents } from "sdc-ui/lib/angular"; -//import { ModalService as ModalServiceSdcUI} from "sdc-ui/lib/angular/modals/modal.service"; -import { IModalButtonComponent } from "sdc-ui/lib/angular/modals/models/modal-config"; +import { SdcUiServices, SdcUiCommon } from "onap-ui-angular"; import { UnsavedChangesComponent } from "app/ng2/components/ui/forms/unsaved-changes/unsaved-changes.component"; -import {Observable} from "rxjs"; -import { DataTypeService } from "app/ng2/services/data-type.service"; -import { DataTypeModel } from "app/models"; -import { PROPERTY_DATA, PROPERTY_TYPES } from "app/utils"; -import { PropertyDeclareAPIModel} from "app/models"; +import {PropertyCreatorComponent} from "./property-creator/property-creator.component"; +import {ModalService} from "../../services/modal.service"; +import { DeclareListComponent } from "./declare-list/declare-list.component"; +import { CapabilitiesGroup, Capability } from "../../../models/capability"; +import { ToscaPresentationData } from "../../../models/tosca-presentation"; +import { Observable } from "rxjs"; const SERVICE_SELF_TITLE = "SELF"; @Component({ @@ -119,7 +95,7 @@ export class PropertiesAssignmentComponent { stateChangeStartUnregister:Function; serviceBePropertiesMap: InstanceBePropertiesMap; serviceBeCapabilitiesPropertiesMap: InstanceBePropertiesMap; - selectedInstance_FlattenCapabilitiesList: Array<Capability>; + selectedInstance_FlattenCapabilitiesList: Capability[]; @ViewChild('hierarchyNavTabs') hierarchyNavTabs: Tabs; @ViewChild('propertyInputTabs') propertyInputTabs: Tabs; @@ -136,12 +112,13 @@ export class PropertiesAssignmentComponent { @Inject("$state") private $state:ng.ui.IStateService, @Inject("Notification") private Notification:any, private componentModeService:ComponentModeService, - private ModalService:ModalService, private EventListenerService:EventListenerService, - private ModalServiceSdcUI: SdcUiComponents.ModalService) { + private ModalServiceSdcUI: SdcUiServices.ModalService, + private ModalService: ModalService, + private keysPipe:KeysPipe, + private topologyTemplateService: TopologyTemplateService) { this.instanceFePropertiesMap = new InstanceFePropertiesMap(); - /* This is the way you can access the component data, please do not use any data except metadata, all other data should be received from the new api calls on the first time than if the data is already exist, no need to call the api again - Ask orit if you have any questions*/ this.component = _stateParams.component; @@ -159,8 +136,8 @@ export class PropertiesAssignmentComponent { this.loadingPolicies = true; this.loadingInstances = true; this.loadingProperties = true; - this.componentServiceNg2 - .getComponentInputsWithProperties(this.component) + this.topologyTemplateService + .getComponentInputsWithProperties(this.component.componentType, this.component.uniqueId) .subscribe(response => { _.forEach(response.inputs, (input: InputBEModel) => { const newInput: InputFEModel = new InputFEModel(input); @@ -169,7 +146,7 @@ export class PropertiesAssignmentComponent { }); this.loadingInputs = false; - }); + }, error => {}); //ignore error this.componentServiceNg2 .getComponentResourcePropertiesData(this.component) .subscribe(response => { @@ -177,6 +154,7 @@ export class PropertiesAssignmentComponent { this.instances = []; this.instances.push(...response.componentInstances); this.instances.push(...response.groupInstances); + this.instances.push(...response.policies); _.forEach(response.policies, (policy: any) => { const newPolicy: InputFEModel = new InputFEModel(policy); @@ -199,7 +177,7 @@ export class PropertiesAssignmentComponent { this.loadingProperties = false; } this.selectFirstInstanceByDefault(); - }); + }, error => { this.loadingInstances = false; }); //ignore error this.stateChangeStartUnregister = this.$scope.$on('$stateChangeStart', (event, toState, toParams) => { // stop if has changed properties @@ -238,14 +216,14 @@ export class PropertiesAssignmentComponent { getServiceProperties(){ this.loadingProperties = false; - this.componentServiceNg2 - .getServiceProperties(this.component) - .subscribe(response => { + this.topologyTemplateService + .getServiceProperties(this.component.uniqueId) + .subscribe((response) => { this.serviceBePropertiesMap = new InstanceBePropertiesMap(); this.serviceBePropertiesMap[this.component.uniqueId] = response; this.processInstancePropertiesResponse(this.serviceBePropertiesMap, false); this.loadingProperties = false; - }, error => { + }, (error) => { this.loadingProperties = false; }); } @@ -267,14 +245,12 @@ export class PropertiesAssignmentComponent { this.loadingProperties = true; if (instance instanceof ComponentInstance) { let instanceBePropertiesMap: InstanceBePropertiesMap = new InstanceBePropertiesMap(); - this.selectedInstance_FlattenCapabilitiesList = instance.capabilities ? CapabilitiesGroup.getFlattenedCapabilities(instance.capabilities) : []; if (this.isInput(instance.originType)) { this.componentInstanceServiceNg2 .getComponentInstanceInputs(this.component, instance) .subscribe(response => { instanceBePropertiesMap[instance.uniqueId] = response; this.processInstancePropertiesResponse(instanceBePropertiesMap, true); - this.processInstanceCapabilitiesPropertiesResponse(false); this.loadingProperties = false; }, error => { }); //ignore error @@ -286,7 +262,6 @@ export class PropertiesAssignmentComponent { .subscribe(response => { instanceBePropertiesMap[instance.uniqueId] = response; this.processInstancePropertiesResponse(instanceBePropertiesMap, false); - this.processInstanceCapabilitiesPropertiesResponse(false); this.loadingProperties = false; }, error => { }); //ignore error @@ -305,7 +280,7 @@ export class PropertiesAssignmentComponent { } else if (instance instanceof PolicyInstance) { let instanceBePropertiesMap: InstanceBePropertiesMap = new InstanceBePropertiesMap(); this.componentInstanceServiceNg2 - .getComponentPolicyInstanceProperties(this.component, this.selectedInstanceData.uniqueId) + .getComponentPolicyInstanceProperties(this.component.componentType, this.component.uniqueId, this.selectedInstanceData.uniqueId) .subscribe((response) => { instanceBePropertiesMap[instance.uniqueId] = response; this.processInstancePropertiesResponse(instanceBePropertiesMap, false); @@ -480,7 +455,7 @@ export class PropertiesAssignmentComponent { let selectedGroupInstancesProperties: InstanceBePropertiesMap = new InstanceBePropertiesMap(); let selectedPolicyInstancesProperties: InstanceBePropertiesMap = new InstanceBePropertiesMap(); let selectedComponentInstancesInputs: InstanceBePropertiesMap = new InstanceBePropertiesMap(); - let instancesIds = new KeysPipe().transform(this.instanceFePropertiesMap, []); + let instancesIds = this.keysPipe.transform(this.instanceFePropertiesMap, []); angular.forEach(instancesIds, (instanceId: string): void => { let selectedInstanceData: any = this.instances.find(instance => instance.uniqueId == instanceId); @@ -500,7 +475,7 @@ export class PropertiesAssignmentComponent { let inputsToCreate: InstancePropertiesAPIMap = new InstancePropertiesAPIMap(selectedComponentInstancesInputs, selectedComponentInstancesProperties, selectedGroupInstancesProperties, selectedPolicyInstancesProperties); - //move changed capabilities properties from componentInstanceInputsMap obj to componentInstanceProperties + //move changed capabilities properties from componentInstanceInputsMap obj to componentInstanceProperties inputsToCreate.componentInstanceProperties[this.selectedInstanceData.uniqueId] = (inputsToCreate.componentInstanceProperties[this.selectedInstanceData.uniqueId] || []).concat( _.filter( @@ -526,15 +501,14 @@ export class PropertiesAssignmentComponent { } } ); - - this.componentServiceNg2 + this.topologyTemplateService .createInput(this.component, inputsToCreate, this.isSelf()) - .subscribe(response => { + .subscribe((response) => { this.setInputTabIndication(response.length); this.checkedPropertiesCount = 0; this.checkedChildPropertiesCount = 0; _.forEach(response, (input: InputBEModel) => { - let newInput: InputFEModel = new InputFEModel(input); + const newInput: InputFEModel = new InputFEModel(input); this.inputsUtils.resetInputDefaultValue(newInput, input.defaultValue); this.inputs.push(newInput); this.updatePropertyValueAfterDeclare(newInput); @@ -628,8 +602,8 @@ export class PropertiesAssignmentComponent { }; console.log("save button clicked. input=", input); - this.componentServiceNg2 - .createListInput(this.component, input, this.isSelf()) + this.topologyTemplateService + .createListInput(this.component.uniqueId, input, this.isSelf()) .subscribe(response => { this.setInputTabIndication(response.length); this.checkedPropertiesCount = 0; @@ -662,8 +636,8 @@ export class PropertiesAssignmentComponent { console.log('declareListProperties() - leave'); }; - /*** DECLARE PROPERTIES/POLICIES ***/ - declarePropertiesToPolicies = (): void => { + /*** DECLARE PROPERTIES/POLICIES ***/ + declarePropertiesToPolicies = (): void => { let selectedComponentInstancesProperties: InstanceBePropertiesMap = new InstanceBePropertiesMap(); let instancesIds = new KeysPipe().transform(this.instanceFePropertiesMap, []); @@ -679,7 +653,7 @@ export class PropertiesAssignmentComponent { let policiesToCreate: InstancePropertiesAPIMap = new InstancePropertiesAPIMap(null, selectedComponentInstancesProperties, null, null); this.loadingPolicies = true; - this.componentServiceNg2 + this.topologyTemplateService .createPolicy(this.component, policiesToCreate, this.isSelf()) .subscribe(response => { this.setPolicyTabIndication(response.length); @@ -688,7 +662,7 @@ export class PropertiesAssignmentComponent { this.loadingPolicies = false; }); //ignore error - }; + } displayPoliciesAsDeclared = (policies) => { _.forEach(policies, (policy: any) => { @@ -699,8 +673,7 @@ export class PropertiesAssignmentComponent { this.updatePropertyValueAfterDeclare(newPolicy); this.policies.push(policy); }); - }; - + } saveChangedData = ():Promise<(PropertyBEModel|InputBEModel)[]> => { return new Promise((resolve, reject) => { @@ -736,7 +709,8 @@ export class PropertiesAssignmentComponent { if (changedInputsProperties.length && changedCapabilitiesProperties.length) { request = Observable.forkJoin( this.componentInstanceServiceNg2.updateInstanceInputs(this.component, this.selectedInstanceData.uniqueId, changedInputsProperties), - this.componentInstanceServiceNg2.updateInstanceProperties(this.component, this.selectedInstanceData.uniqueId, changedCapabilitiesProperties) + this.componentInstanceServiceNg2.updateInstanceProperties(this.component.componentType, this.component.uniqueId, + this.selectedInstanceData.uniqueId, changedCapabilitiesProperties) ); } else if (changedInputsProperties.length) { @@ -745,7 +719,7 @@ export class PropertiesAssignmentComponent { } else if (changedCapabilitiesProperties.length) { request = this.componentInstanceServiceNg2 - .updateInstanceProperties(this.component, this.selectedInstanceData.uniqueId, changedCapabilitiesProperties); + .updateInstanceProperties(this.component.componentType, this.component.uniqueId, this.selectedInstanceData.uniqueId, changedCapabilitiesProperties); } handleSuccess = (response) => { // reset each changed property with new value and remove it from changed properties list @@ -757,19 +731,18 @@ export class PropertiesAssignmentComponent { }; } else { if (this.isSelf()) { - request = this.componentServiceNg2.updateServiceProperties(this.component, _.map(changedProperties, cp => { + request = this.topologyTemplateService.updateServiceProperties(this.component.uniqueId, _.map(changedProperties, cp => { delete cp.constraints; return cp; })); } else { request = this.componentInstanceServiceNg2 - .updateInstanceProperties(this.component, this.selectedInstanceData.uniqueId, changedProperties); + .updateInstanceProperties(this.component.componentType, this.component.uniqueId, this.selectedInstanceData.uniqueId, changedProperties); } handleSuccess = (response) => { // reset each changed property with new value and remove it from changed properties list response.forEach((resProp) => { - const changedProp = <PropertyFEModel>_.find(this.changedData, changedDataObject => changedDataObject.uniqueId === resProp.uniqueId); - this.changedData = _.filter(this.changedData, changedDataObject => changedDataObject.uniqueId !== resProp.uniqueId); + const changedProp = <PropertyFEModel>this.changedData.shift(); this.propertiesUtils.resetPropertyValue(changedProp, resProp.value); }); resolve(response); @@ -778,7 +751,7 @@ export class PropertiesAssignmentComponent { } } else if (this.selectedInstanceData instanceof GroupInstance) { request = this.componentInstanceServiceNg2 - .updateComponentGroupInstanceProperties(this.component, this.selectedInstanceData.uniqueId, changedProperties); + .updateComponentGroupInstanceProperties(this.component.componentType, this.component.uniqueId, this.selectedInstanceData.uniqueId, changedProperties); handleSuccess = (response) => { // reset each changed property with new value and remove it from changed properties list response.forEach((resProp) => { @@ -790,7 +763,7 @@ export class PropertiesAssignmentComponent { }; } else if (this.selectedInstanceData instanceof PolicyInstance) { request = this.componentInstanceServiceNg2 - .updateComponentPolicyInstanceProperties(this.component, this.selectedInstanceData.uniqueId, changedProperties); + .updateComponentPolicyInstanceProperties(this.component.componentType, this.component.uniqueId, this.selectedInstanceData.uniqueId, changedProperties); handleSuccess = (response) => { // reset each changed property with new value and remove it from changed properties list response.forEach((resProp) => { @@ -802,6 +775,7 @@ export class PropertiesAssignmentComponent { }; } } else if (this.isInputsTabSelected) { + const changedInputs: InputBEModel[] = this.changedData.map((changedInput) => { changedInput = <InputFEModel>changedInput; const inputBE = new InputBEModel(changedInput); @@ -925,27 +899,27 @@ export class PropertiesAssignmentComponent { { title: modalTitle, size: 'sm', - type: 'custom', - testId: "id", - + type: SdcUiCommon.ModalType.custom, + testId: "navigate-modal", + buttons: [ - {id: 'cancelButton', text: 'Cancel', type: 'secondary', size: 'xsm', closeModal: true, callback: () => reject()}, - {id: 'discardButton', text: 'Discard', type: 'secondary', size: 'xsm', closeModal: true, callback: () => { this.reverseChangedData(); resolve()}}, - {id: 'saveButton', text: 'Save', type: 'primary', size: 'xsm', closeModal: true, disabled: !this.isValidChangedData, callback: () => this.doSaveChangedData(resolve, reject)} - ] as IModalButtonComponent[] - }, UnsavedChangesComponent, {isValidChangedData: this.isValidChangedData}); + {id: 'cancelButton', text: 'Cancel', type: SdcUiCommon.ButtonType.secondary, size: 'xsm', closeModal: true, callback: () => reject()}, + {id: 'discardButton', text: 'Discard', type: SdcUiCommon.ButtonType.secondary, size: 'xsm', closeModal: true, callback: () => { this.reverseChangedData(); resolve()}}, + {id: 'saveButton', text: 'Save', type: SdcUiCommon.ButtonType.primary, size: 'xsm', closeModal: true, disabled: !this.isValidChangedData, callback: () => this.doSaveChangedData(resolve, reject)} + ] as SdcUiCommon.IModalButtonComponent[] + } as SdcUiCommon.IModalConfig, UnsavedChangesComponent, {isValidChangedData: this.isValidChangedData}); }); } updatePropertyValueAfterDeclare = (input: InputFEModel) => { if (this.instanceFePropertiesMap[input.instanceUniqueId]) { - let instanceName = input.instanceUniqueId.slice(input.instanceUniqueId.lastIndexOf('.') + 1); - let propertyForUpdatindVal = _.find(this.instanceFePropertiesMap[input.instanceUniqueId], (feProperty: PropertyFEModel) => { - return feProperty.uniqueId === input.propertyId && + const instanceName = input.instanceUniqueId.slice(input.instanceUniqueId.lastIndexOf('.') + 1); + const propertyForUpdatindVal = _.find(this.instanceFePropertiesMap[input.instanceUniqueId], (feProperty: PropertyFEModel) => { + return feProperty.name == input.relatedPropertyName && (feProperty.name == input.relatedPropertyName || input.name === instanceName.concat('_').concat(feProperty.name.replace(/[.]/g, '_'))); }); - let inputPath = (input.inputPath && input.inputPath != propertyForUpdatindVal.name) ? input.inputPath : undefined; + const inputPath = (input.inputPath && input.inputPath != propertyForUpdatindVal.name) ? input.inputPath : undefined; propertyForUpdatindVal.setAsDeclared(inputPath); //set prop as declared before assigning value this.propertiesService.disableRelatedProperties(propertyForUpdatindVal, inputPath); this.propertiesUtils.resetPropertyValue(propertyForUpdatindVal, input.relatedPropertyValue, inputPath); @@ -968,7 +942,7 @@ export class PropertiesAssignmentComponent { setPolicyTabIndication = (numPolicies: number): void => { this.propertyInputTabs.setTabIndication('Policies', numPolicies); - }; + } resetUnsavedChangesForInput = (input:InputFEModel) => { this.inputsUtils.resetInputDefaultValue(input, input.defaultValue); @@ -1009,9 +983,9 @@ export class PropertiesAssignmentComponent { deletePolicy = (policy: PolicyInstance) => { this.loadingPolicies = true; - this.componentServiceNg2 + this.topologyTemplateService .deletePolicy(this.component, policy) - .subscribe(response => { + .subscribe((response) => { this.policies = this.policies.filter(policy => policy.uniqueId !== response.uniqueId); //Reload the whole instance for now - TODO: CHANGE THIS after the BE starts returning properties within the response, use commented code below instead! this.changeSelectedInstance(this.selectedInstanceData); @@ -1020,25 +994,25 @@ export class PropertiesAssignmentComponent { }; deleteProperty = (property: PropertyFEModel) => { - let propertyToDelete = new PropertyFEModel(property); + const propertyToDelete = new PropertyFEModel(property); this.loadingProperties = true; - let feMap = this.instanceFePropertiesMap; - this.componentServiceNg2 - .deleteServiceProperty(this.component, propertyToDelete) - .subscribe(response => { + const feMap = this.instanceFePropertiesMap; + this.topologyTemplateService + .deleteServiceProperty(this.component.uniqueId, propertyToDelete) + .subscribe((response) => { const props = feMap[this.component.uniqueId]; props.splice(props.findIndex(p => p.uniqueId === response),1); this.loadingProperties = false; - }, error => { + }, (error) => { this.loadingProperties = false; console.error(error); }); - }; + } /*** addProperty ***/ addProperty = () => { let modalTitle = 'Add Property'; - const modal = this.ModalService.createCustomModal(new ModalModel( + let modal = this.ModalService.createCustomModal(new ModalModel( 'sm', modalTitle, null, @@ -1046,10 +1020,10 @@ export class PropertiesAssignmentComponent { new ButtonModel('Save', 'blue', () => { modal.instance.dynamicContent.instance.isLoading = true; const newProperty: PropertyBEModel = modal.instance.dynamicContent.instance.propertyModel; - this.componentServiceNg2.createServiceProperty(this.component, newProperty) - .subscribe(response => { + this.topologyTemplateService.createServiceProperty(this.component.uniqueId, newProperty) + .subscribe((response) => { modal.instance.dynamicContent.instance.isLoading = false; - let newProp: PropertyFEModel = this.propertiesUtils.convertAddPropertyBAToPropertyFE(response); + const newProp: PropertyFEModel = this.propertiesUtils.convertAddPropertyBAToPropertyFE(response); this.instanceFePropertiesMap[this.component.uniqueId].push(newProp); modal.instance.close(); }, (error) => { @@ -1059,7 +1033,6 @@ export class PropertiesAssignmentComponent { title: 'Failure' }); }); - }, () => !modal.instance.dynamicContent.instance.checkFormValidForSubmit()), new ButtonModel('Cancel', 'outline grey', () => { modal.instance.close(); @@ -1069,24 +1042,23 @@ export class PropertiesAssignmentComponent { )); this.ModalService.addDynamicContentToModal(modal, PropertyCreatorComponent, {}); modal.instance.open(); - }; + } /*** SEARCH RELATED FUNCTIONS ***/ searchPropertiesInstances = (filterData:FilterPropertiesAssignmentData) => { let instanceBePropertiesMap:InstanceBePropertiesMap; this.componentServiceNg2 .filterComponentInstanceProperties(this.component, filterData) - .subscribe(response => { - + .subscribe((response) => { this.processInstancePropertiesResponse(response, false); this.hierarchyPropertiesDisplayOptions.searchText = filterData.propertyName;//mark results in tree this.searchPropertyName = filterData.propertyName;//mark in table this.hierarchyNavTabs.triggerTabChange('Composition'); this.propertiesNavigationData = []; this.displayClearSearch = true; - }, error => {}); //ignore error + }, (error) => {}); //ignore error - }; + } clearSearch = () => { this.instancesNavigationData = this.instances; @@ -1106,5 +1078,6 @@ export class PropertiesAssignmentComponent { private isInput = (instanceType:string):boolean =>{ return instanceType === ResourceType.VF || instanceType === ResourceType.PNF || instanceType === ResourceType.CVFC || instanceType === ResourceType.CR; } + } diff --git a/catalog-ui/src/app/ng2/pages/properties-assignment/property-creator/property-creator.component.ts b/catalog-ui/src/app/ng2/pages/properties-assignment/property-creator/property-creator.component.ts index 7d76904539..5053d52cc8 100644 --- a/catalog-ui/src/app/ng2/pages/properties-assignment/property-creator/property-creator.component.ts +++ b/catalog-ui/src/app/ng2/pages/properties-assignment/property-creator/property-creator.component.ts @@ -1,79 +1,78 @@ -import * as _ from "lodash"; -import {Component} from '@angular/core'; -import {DropdownValue} from "app/ng2/components/ui/form-components/dropdown/ui-element-dropdown.component"; -import { DataTypeService } from "app/ng2/services/data-type.service"; -import {PropertyBEModel, DataTypesMap} from "app/models"; -import {PROPERTY_DATA} from "app/utils"; -import {PROPERTY_TYPES} from "../../../../utils"; - +import { Component } from '@angular/core'; +import { DataTypesMap, PropertyBEModel } from 'app/models'; +import { DropdownValue } from 'app/ng2/components/ui/form-components/dropdown/ui-element-dropdown.component'; +import { DataTypeService } from 'app/ng2/services/data-type.service'; +import { PROPERTY_DATA } from 'app/utils'; +import * as _ from 'lodash'; +import { PROPERTY_TYPES } from '../../../../utils'; @Component({ selector: 'property-creator', templateUrl: './property-creator.component.html', - styleUrls:['./property-creator.component.less'], + styleUrls: ['./property-creator.component.less'], }) export class PropertyCreatorComponent { - typesProperties: Array<DropdownValue>; - typesSchemaProperties: Array<DropdownValue>; + typesProperties: DropdownValue[]; + typesSchemaProperties: DropdownValue[]; propertyModel: PropertyBEModel; - //propertyNameValidationPattern:RegExp = /^[a-zA-Z0-9_:-]{1,50}$/; - //commentValidationPattern:RegExp = /^[\u0000-\u00BF]*$/; - //types:Array<string>; - dataTypes:DataTypesMap; - isLoading:boolean; + // propertyNameValidationPattern:RegExp = /^[a-zA-Z0-9_:-]{1,50}$/; + // commentValidationPattern:RegExp = /^[\u0000-\u00BF]*$/; + // types:Array<string>; + dataTypes: DataTypesMap; + isLoading: boolean; - constructor(protected dataTypeService:DataTypeService) {} + constructor(protected dataTypeService: DataTypeService) {} ngOnInit() { this.propertyModel = new PropertyBEModel(); this.propertyModel.type = ''; this.propertyModel.schema.property.type = ''; - const types: Array<string> = PROPERTY_DATA.TYPES; //All types - simple type + map + list - this.dataTypes = this.dataTypeService.getAllDataTypes(); //Get all data types in service - const nonPrimitiveTypes :Array<string> = _.filter(Object.keys(this.dataTypes), (type:string)=> { - return types.indexOf(type) == -1; + const types: string[] = PROPERTY_DATA.TYPES; // All types - simple type + map + list + this.dataTypes = this.dataTypeService.getAllDataTypes(); // Get all data types in service + const nonPrimitiveTypes: string[] = _.filter(Object.keys(this.dataTypes), (type: string) => { + return types.indexOf(type) === -1; }); this.typesProperties = _.map(PROPERTY_DATA.TYPES, (type: string) => new DropdownValue(type, type) ); - let typesSimpleProperties = _.map(PROPERTY_DATA.SIMPLE_TYPES, + const typesSimpleProperties = _.map(PROPERTY_DATA.SIMPLE_TYPES, (type: string) => new DropdownValue(type, type) ); - let nonPrimitiveTypesValues = _.map(nonPrimitiveTypes, + const nonPrimitiveTypesValues = _.map(nonPrimitiveTypes, (type: string) => new DropdownValue(type, - type.replace("org.openecomp.datatypes.heat.","")) + type.replace('org.openecomp.datatypes.heat.', '')) ) .sort((a, b) => a.label.localeCompare(b.label)); - this.typesProperties = _.concat(this.typesProperties,nonPrimitiveTypesValues); - this.typesSchemaProperties = _.concat(typesSimpleProperties,nonPrimitiveTypesValues); - this.typesProperties.unshift(new DropdownValue('','Select Type...')); - this.typesSchemaProperties.unshift(new DropdownValue('','Select Schema Type...')); + this.typesProperties = _.concat(this.typesProperties, nonPrimitiveTypesValues); + this.typesSchemaProperties = _.concat(typesSimpleProperties, nonPrimitiveTypesValues); + this.typesProperties.unshift(new DropdownValue('', 'Select Type...')); + this.typesSchemaProperties.unshift(new DropdownValue('', 'Select Schema Type...')); } - checkFormValidForSubmit(){ - const showSchema:boolean = this.showSchema(); - let isSchemaValid: boolean = (showSchema && !this.propertyModel.schema.property.type)? false : true; - if (!showSchema){ + checkFormValidForSubmit() { + const showSchema: boolean = this.showSchema(); + const isSchemaValid: boolean = (showSchema && !this.propertyModel.schema.property.type) ? false : true; + if (!showSchema) { this.propertyModel.schema.property.type = ''; } return this.propertyModel.name && this.propertyModel.type && isSchemaValid; } - showSchema():boolean { + showSchema(): boolean { return [PROPERTY_TYPES.LIST, PROPERTY_TYPES.MAP].indexOf(this.propertyModel.type) > -1; - }; + } - onSchemaTypeChange():void { - if (this.propertyModel.type == PROPERTY_TYPES.MAP) { + onSchemaTypeChange(): void { + if (this.propertyModel.type === PROPERTY_TYPES.MAP) { this.propertyModel.value = JSON.stringify({'': null}); - } else if (this.propertyModel.type == PROPERTY_TYPES.LIST) { + } else if (this.propertyModel.type === PROPERTY_TYPES.LIST) { this.propertyModel.value = JSON.stringify([]); } - }; + } } diff --git a/catalog-ui/src/app/ng2/pages/properties-assignment/property-creator/property-creator.module.ts b/catalog-ui/src/app/ng2/pages/properties-assignment/property-creator/property-creator.module.ts index 92accb26b5..1cbb4e17ec 100644 --- a/catalog-ui/src/app/ng2/pages/properties-assignment/property-creator/property-creator.module.ts +++ b/catalog-ui/src/app/ng2/pages/properties-assignment/property-creator/property-creator.module.ts @@ -1,10 +1,10 @@ -import {NgModule} from "@angular/core"; -import {CommonModule} from "@angular/common"; -import {PropertyCreatorComponent} from "./property-creator.component"; -import {FormsModule} from "@angular/forms"; -import {FormElementsModule} from "app/ng2/components/ui/form-components/form-elements.module"; -import {UiElementsModule} from "app/ng2/components/ui/ui-elements.module"; -import {TranslateModule} from "../../../shared/translator/translate.module"; +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { FormElementsModule } from 'app/ng2/components/ui/form-components/form-elements.module'; +import { UiElementsModule } from 'app/ng2/components/ui/ui-elements.module'; +import { TranslateModule } from '../../../shared/translator/translate.module'; +import { PropertyCreatorComponent } from './property-creator.component'; @NgModule({ declarations: [ diff --git a/catalog-ui/src/app/ng2/pages/properties-assignment/services/properties.utils.ts b/catalog-ui/src/app/ng2/pages/properties-assignment/services/properties.utils.ts index 011be41611..bd7ccd1bfd 100644 --- a/catalog-ui/src/app/ng2/pages/properties-assignment/services/properties.utils.ts +++ b/catalog-ui/src/app/ng2/pages/properties-assignment/services/properties.utils.ts @@ -48,7 +48,7 @@ export class PropertiesUtils { let newFEProp: PropertyFEModel = new PropertyFEModel(property); //Convert property to FE - this.initValueObjectRef(newFEProp); //initialize valueObj. + this.initValueObjectRef(newFEProp); //initialize valueObj AND creates flattened children propertyFeArray.push(newFEProp); newFEProp.updateExpandedChildPropertyId(newFEProp.name); //display only the first level of children this.dataTypeService.checkForCustomBehavior(newFEProp); @@ -79,8 +79,8 @@ export class PropertiesUtils { return instanceFePropertiesMap; } - public convertAddPropertyBAToPropertyFE = (property: PropertyBEModel):PropertyFEModel => { - let newFEProp: PropertyFEModel = new PropertyFEModel(property); //Convert property to FE + public convertAddPropertyBAToPropertyFE = (property: PropertyBEModel): PropertyFEModel => { + const newFEProp: PropertyFEModel = new PropertyFEModel(property); //Convert property to FE this.initValueObjectRef(newFEProp); newFEProp.updateExpandedChildPropertyId(newFEProp.name); //display only the first level of children this.dataTypeService.checkForCustomBehavior(newFEProp); @@ -108,7 +108,7 @@ export class PropertiesUtils { let tempProps: Array<DerivedFEProperty> = []; let dataTypeObj: DataTypeModel = this.dataTypeService.getDataTypeByTypeName(type); this.dataTypeService.getDerivedDataTypeProperties(dataTypeObj, tempProps, parentName); - return tempProps; + return _.sortBy(tempProps, ['propertiesName']); } /* Sets the valueObj of parent property and its children. diff --git a/catalog-ui/src/app/ng2/pages/req-and-capabilities-editor/capabilities-editor/capabilities-editor.module.ts b/catalog-ui/src/app/ng2/pages/req-and-capabilities-editor/capabilities-editor/capabilities-editor.module.ts index 1e767a5690..104a6d0579 100644 --- a/catalog-ui/src/app/ng2/pages/req-and-capabilities-editor/capabilities-editor/capabilities-editor.module.ts +++ b/catalog-ui/src/app/ng2/pages/req-and-capabilities-editor/capabilities-editor/capabilities-editor.module.ts @@ -5,7 +5,8 @@ import {FormsModule} from "@angular/forms"; import {FormElementsModule} from "app/ng2/components/ui/form-components/form-elements.module"; import {UiElementsModule} from "app/ng2/components/ui/ui-elements.module"; import {TranslateModule} from 'app/ng2/shared/translator/translate.module'; -import {SdcUiComponentsModule} from "sdc-ui/lib/angular/index"; +import { SdcUiComponentsModule } from 'onap-ui-angular'; + @NgModule({ declarations: [ diff --git a/catalog-ui/src/app/ng2/pages/req-and-capabilities-editor/requirements-editor/requirements-editor.module.ts b/catalog-ui/src/app/ng2/pages/req-and-capabilities-editor/requirements-editor/requirements-editor.module.ts index 1be8be51af..d38790a8db 100644 --- a/catalog-ui/src/app/ng2/pages/req-and-capabilities-editor/requirements-editor/requirements-editor.module.ts +++ b/catalog-ui/src/app/ng2/pages/req-and-capabilities-editor/requirements-editor/requirements-editor.module.ts @@ -4,7 +4,7 @@ import {RequirementsEditorComponent} from "./requirements-editor.component"; import {FormsModule} from "@angular/forms"; import {FormElementsModule} from "../../../components/ui/form-components/form-elements.module"; import {TranslateModule} from 'app/ng2/shared/translator/translate.module'; -import {SdcUiComponentsModule} from "sdc-ui/lib/angular/index"; +import { SdcUiComponentsModule } from "onap-ui-angular"; @NgModule({ declarations: [ diff --git a/catalog-ui/src/app/ng2/pages/service-consumption-editor/service-consumption-editor.component.ts b/catalog-ui/src/app/ng2/pages/service-consumption-editor/service-consumption-editor.component.ts index 2c86cc5c5c..8444c6261a 100644 --- a/catalog-ui/src/app/ng2/pages/service-consumption-editor/service-consumption-editor.component.ts +++ b/catalog-ui/src/app/ng2/pages/service-consumption-editor/service-consumption-editor.component.ts @@ -14,29 +14,29 @@ * permissions and limitations under the License. */ -import * as _ from "lodash"; import { Component } from '@angular/core'; -import {ServiceServiceNg2} from "app/ng2/services/component-services/service.service"; import { - Service, - ServiceInstanceObject, - InstanceFePropertiesMap, - InstanceBePropertiesMap, - PropertyBEModel, + Capability, InputBEModel, - OperationModel, + InstanceBePropertiesMap, + InstanceFePropertiesMap, InterfaceModel, - Capability + OperationModel, + PropertyBEModel, + Service } from 'app/models'; -import {ConsumptionInput, ConsumptionInputDetails, ServiceOperation} from 'app/ng2/components/logic/service-consumption/service-consumption.component'; -import {PropertiesUtils} from "app/ng2/pages/properties-assignment/services/properties.utils"; +import { ConsumptionInput, ConsumptionInputDetails, ServiceOperation } from 'app/ng2/components/logic/service-consumption/service-consumption.component'; +import { PropertiesUtils } from 'app/ng2/pages/properties-assignment/services/properties.utils'; +import { ServiceServiceNg2 } from 'app/ng2/services/component-services/service.service'; import { PROPERTY_DATA } from 'app/utils'; - +import * as _ from 'lodash'; +import { ServiceInstanceObject } from '../../../models/service-instance-properties-and-interfaces'; +import { TopologyTemplateService } from '../../services/component-services/topology-template.service'; @Component({ selector: 'service-consumption-editor', templateUrl: './service-consumption-editor.component.html', - styleUrls:['./service-consumption-editor.component.less'], + styleUrls: ['./service-consumption-editor.component.less'], providers: [] }) @@ -45,27 +45,27 @@ export class ServiceConsumptionCreatorComponent { input: { interfaceId: string, serviceOperationIndex: number, - serviceOperations: Array<ServiceOperation>, + serviceOperations: ServiceOperation[], parentService: Service, selectedService: Service, - parentServiceInputs: Array<InputBEModel>, - selectedServiceProperties: Array<PropertyBEModel>, - selectedServiceInstanceId: String, - selectedInstanceSiblings: Array<ServiceInstanceObject>, - selectedInstanceCapabilitisList: Array<Capability>, - siblingsCapabilitiesList: Map<string, Array<Capability>> + parentServiceInputs: InputBEModel[], + selectedServiceProperties: PropertyBEModel[], + selectedServiceInstanceId: string, + selectedInstanceSiblings: ServiceInstanceObject[], + selectedInstanceCapabilitisList: Capability[], + siblingsCapabilitiesList: Map<string, Capability[]> }; - sourceTypes: Array<any> = []; - serviceOperationsList: Array<ServiceOperation>; + sourceTypes: any[] = []; + serviceOperationsList: ServiceOperation[]; serviceOperation: ServiceOperation; currentIndex: number; isLoading: boolean = false; parentService: Service; selectedService: Service; - selectedServiceInstanceId: String; - parentServiceInputs: Array<InputBEModel>; - selectedServiceProperties: Array<PropertyBEModel>; - changedData: Array<ConsumptionInputDetails> = []; + selectedServiceInstanceId: string; + parentServiceInputs: InputBEModel[]; + selectedServiceProperties: PropertyBEModel[]; + changedData: ConsumptionInputDetails[] = []; inputFePropertiesMap: any = []; SOURCE_TYPES = { @@ -75,7 +75,7 @@ export class ServiceConsumptionCreatorComponent { SERVICE_INPUT_LABEL: 'Service Input' }; - constructor(private serviceServiceNg2: ServiceServiceNg2, private propertiesUtils:PropertiesUtils) {} + constructor(private topologyTemplateService: TopologyTemplateService, private propertiesUtils: PropertiesUtils) {} ngOnInit() { this.serviceOperationsList = this.input.serviceOperations; @@ -112,7 +112,7 @@ export class ServiceConsumptionCreatorComponent { capabilities: [] } ]; - _.forEach(this.input.selectedInstanceSiblings, sib => + _.forEach(this.input.selectedInstanceSiblings, (sib) => this.sourceTypes.push({ label: sib.name, value: sib.id, @@ -128,56 +128,84 @@ export class ServiceConsumptionCreatorComponent { } onExpandAll() { - _.forEach(this.serviceOperation.consumptionInputs, coInput => { + _.forEach(this.serviceOperation.consumptionInputs, (coInput) => { coInput.expanded = true; - }) + }); } onCollapseAll() { - _.forEach(this.serviceOperation.consumptionInputs, coInput => { + _.forEach(this.serviceOperation.consumptionInputs, (coInput) => { coInput.expanded = false; - }) + }); } isAllInputExpanded() { - return _.every(this.serviceOperation.consumptionInputs, coInput => coInput.expanded === true); + return _.every(this.serviceOperation.consumptionInputs, (coInput) => coInput.expanded === true); } isAllInputCollapsed() { - return _.every(this.serviceOperation.consumptionInputs, coInput => coInput.expanded === false); + return _.every(this.serviceOperation.consumptionInputs, (coInput) => coInput.expanded === false); } onChangePage(newIndex) { if (newIndex >= 0 && newIndex < this.serviceOperationsList.length) { this.currentIndex = newIndex; this.serviceOperation = this.serviceOperationsList[newIndex]; - if(!this.serviceOperation.consumptionInputs || this.serviceOperation.consumptionInputs.length === 0) { + if (!this.serviceOperation.consumptionInputs || this.serviceOperation.consumptionInputs.length === 0) { this.initConsumptionInputs(); } this.getComplexPropertiesForCurrentInputsOfOperation(this.serviceOperation.consumptionInputs); } } + checkFormValidForSubmit(): boolean { + return this.isValidInputsValues() && this.isMandatoryFieldsValid(); + } + + checkFormValidForNavigation(): boolean { + return this.isMandatoryFieldsValid() && (this.changedData.length === 0 || this.isValidInputsValues()); + } + + onChange(value: any, isValid: boolean, consumptionInput: ConsumptionInputDetails) { + consumptionInput.updateValidity(isValid); + const dataChangedIndex = this.changedData.findIndex((changedItem) => changedItem.inputId === consumptionInput.inputId); + if (value !== consumptionInput.origVal) { + if (dataChangedIndex === -1) { + this.changedData.push(consumptionInput); + } + } else { + if (dataChangedIndex !== -1) { + this.changedData.splice(dataChangedIndex, 1); + } + } + } + + onComplexPropertyChanged(property, consumptionInput) { + consumptionInput.value = JSON.stringify(property.valueObj); + this.onChange(property.valueObj, property.valueObjIsValid , consumptionInput); + } + private initConsumptionInputs() { this.isLoading = true; - this.serviceServiceNg2.getServiceConsumptionInputs(this.parentService, this.selectedServiceInstanceId, this.input.interfaceId, this.serviceOperation.operation).subscribe((result: Array<ConsumptionInput>) => { + this.topologyTemplateService.getServiceConsumptionInputs(this.parentService.uniqueId, this.selectedServiceInstanceId, + this.input.interfaceId, this.serviceOperation.operation).subscribe((result: ConsumptionInput[]) => { this.isLoading = false; this.serviceOperation.consumptionInputs = this.analyzeCurrentConsumptionInputs(result); this.getComplexPropertiesForCurrentInputsOfOperation(this.serviceOperation.consumptionInputs); - }, err=> { + }, (err) => { this.isLoading = false; }); } - private analyzeCurrentConsumptionInputs(result: Array<any>): Array<ConsumptionInputDetails> { - let inputsResult: Array<ConsumptionInputDetails> = []; - let currentOp = this.serviceOperation.operation; - if(currentOp) { - inputsResult = _.map(result, input => { - let sourceVal = input.source || this.SOURCE_TYPES.STATIC; - let consumptionInputDetails: ConsumptionInputDetails = _.cloneDeep(input); + private analyzeCurrentConsumptionInputs(result: any[]): ConsumptionInputDetails[] { + let inputsResult: ConsumptionInputDetails[] = []; + const currentOp = this.serviceOperation.operation; + if (currentOp) { + inputsResult = _.map(result, (input) => { + const sourceVal = input.source || this.SOURCE_TYPES.STATIC; + const consumptionInputDetails: ConsumptionInputDetails = _.cloneDeep(input); consumptionInputDetails.source = sourceVal; consumptionInputDetails.isValid = true; consumptionInputDetails.expanded = false; - let filteredListsObj = this.getFilteredProps(sourceVal, input.type); + const filteredListsObj = this.getFilteredProps(sourceVal, input.type); consumptionInputDetails.assignValueLabel = this.getAssignValueLabel(sourceVal); consumptionInputDetails.associatedProps = filteredListsObj.associatedPropsList; consumptionInputDetails.associatedInterfaces = filteredListsObj.associatedInterfacesList; @@ -190,15 +218,14 @@ export class ServiceConsumptionCreatorComponent { private onSourceChanged(consumptionInput: ConsumptionInputDetails): void { consumptionInput.assignValueLabel = this.getAssignValueLabel(consumptionInput.source); - let filteredListsObj = this.getFilteredProps(consumptionInput.source, consumptionInput.type); + const filteredListsObj = this.getFilteredProps(consumptionInput.source, consumptionInput.type); consumptionInput.associatedProps = filteredListsObj.associatedPropsList; consumptionInput.associatedInterfaces = filteredListsObj.associatedInterfacesList; consumptionInput.associatedCapabilities = filteredListsObj.associatedCapabilitiesList; - if(consumptionInput.source === this.SOURCE_TYPES.STATIC) { - if(PROPERTY_DATA.SIMPLE_TYPES.indexOf(consumptionInput.type) !== -1) { - consumptionInput.value = consumptionInput.defaultValue || ""; - } - else { + if (consumptionInput.source === this.SOURCE_TYPES.STATIC) { + if (PROPERTY_DATA.SIMPLE_TYPES.indexOf(consumptionInput.type) !== -1) { + consumptionInput.value = consumptionInput.defaultValue || ''; + } else { consumptionInput.value = null; Object.assign(this.inputFePropertiesMap, this.processPropertiesOfComplexTypeInput(consumptionInput)); } @@ -206,9 +233,11 @@ export class ServiceConsumptionCreatorComponent { } private getFilteredProps(sourceVal, inputType) { - let currentSourceObj = this.sourceTypes.find(s => s.value === sourceVal); - let associatedInterfacesList = [], associatedPropsList = [], associatedCapabilitiesPropsList: Array<Capability> = []; - if(currentSourceObj) { + const currentSourceObj = this.sourceTypes.find((s) => s.value === sourceVal); + let associatedInterfacesList = []; + let associatedPropsList = []; + let associatedCapabilitiesPropsList: Capability[] = []; + if (currentSourceObj) { if (currentSourceObj.interfaces) { associatedInterfacesList = this.getFilteredInterfaceOutputs(currentSourceObj, inputType); } @@ -221,31 +250,31 @@ export class ServiceConsumptionCreatorComponent { associatedCapabilitiesPropsList = _.reduce(currentSourceObj.capabilities, (filteredCapsList, capability: Capability) => { - let filteredProps = _.filter(capability.properties, prop => prop.type === inputType); + const filteredProps = _.filter(capability.properties, (prop) => prop.type === inputType); if (filteredProps.length) { - let cap = new Capability(capability); + const cap = new Capability(capability); cap.properties = filteredProps; filteredCapsList.push(cap); } - return filteredCapsList + return filteredCapsList; }, []); } return { - associatedPropsList: associatedPropsList, - associatedInterfacesList: associatedInterfacesList, + associatedPropsList, + associatedInterfacesList, associatedCapabilitiesList: associatedCapabilitiesPropsList - } + }; } private getFilteredInterfaceOutputs(currentSourceObj, inputType) { - let currentServiceOperationId = this.serviceOperation.operation.uniqueId; - let filteredInterfacesList = []; - Object.keys(currentSourceObj.interfaces).map(interfId => { - let interfaceObj: InterfaceModel = new InterfaceModel(currentSourceObj.interfaces[interfId]); - Object.keys(interfaceObj.operations).map(opId => { - if(currentServiceOperationId !== opId) { - let operationObj: OperationModel = interfaceObj.operations[opId]; - let filteredOutputsList = _.filter(operationObj.outputs.listToscaDataDefinition, output => output.type === inputType); + const currentServiceOperationId = this.serviceOperation.operation.uniqueId; + const filteredInterfacesList = []; + Object.keys(currentSourceObj.interfaces).map((interfId) => { + const interfaceObj: InterfaceModel = new InterfaceModel(currentSourceObj.interfaces[interfId]); + Object.keys(interfaceObj.operations).map((opId) => { + if (currentServiceOperationId !== opId) { + const operationObj: OperationModel = interfaceObj.operations[opId]; + const filteredOutputsList = _.filter(operationObj.outputs.listToscaDataDefinition, (output) => output.type === inputType); if (filteredOutputsList.length) { filteredInterfacesList.push({ name: `${interfaceObj.type}.${operationObj.name}`, @@ -259,25 +288,23 @@ export class ServiceConsumptionCreatorComponent { return filteredInterfacesList; } - getAssignValueLabel(selectedSource: string): string { - if(selectedSource === this.SOURCE_TYPES.STATIC || selectedSource === "") { + private getAssignValueLabel(selectedSource: string): string { + if (selectedSource === this.SOURCE_TYPES.STATIC || selectedSource === '') { return this.SOURCE_TYPES.STATIC; - } - else { - if(selectedSource === this.parentService.uniqueId) { //parent is the source + } else { + if (selectedSource === this.parentService.uniqueId) { // parent is the source return this.SOURCE_TYPES.SERVICE_INPUT_LABEL; } return this.SOURCE_TYPES.SERVICE_PROPERTY_LABEL; } } - private isValidInputsValues(): boolean { return this.changedData.length > 0 && this.changedData.every((changedItem) => changedItem.isValid); } private isMandatoryFieldsValid(): boolean { - const invalid: Array<ConsumptionInputDetails> = this.serviceOperation.consumptionInputs.filter(item => + const invalid: ConsumptionInputDetails[] = this.serviceOperation.consumptionInputs.filter((item) => item.required && (item.value === null || typeof item.value === 'undefined' || item.value === '')); if (invalid.length > 0) { return false; @@ -285,45 +312,19 @@ export class ServiceConsumptionCreatorComponent { return true; } - checkFormValidForSubmit(): boolean { - return this.isValidInputsValues() && this.isMandatoryFieldsValid(); - } - - checkFormValidForNavigation(): boolean { - return this.isMandatoryFieldsValid() && (this.changedData.length === 0 || this.isValidInputsValues()); - } - - onChange(value: any, isValid: boolean, consumptionInput: ConsumptionInputDetails) { - consumptionInput.updateValidity(isValid); - const dataChangedIndex = this.changedData.findIndex((changedItem) => changedItem.inputId === consumptionInput.inputId); - if (value !== consumptionInput.origVal) { - if (dataChangedIndex === -1) { - this.changedData.push(consumptionInput); - } - } else { - if (dataChangedIndex !== -1) { - this.changedData.splice(dataChangedIndex, 1); - } - } - } - - private getComplexPropertiesForCurrentInputsOfOperation(opInputs: Array<ConsumptionInput>) { - _.forEach(opInputs, input => { - if(PROPERTY_DATA.SIMPLE_TYPES.indexOf(input.type) === -1 && input.source === this.SOURCE_TYPES.STATIC) { + private getComplexPropertiesForCurrentInputsOfOperation(opInputs: ConsumptionInput[]) { + _.forEach(opInputs, (input) => { + if (PROPERTY_DATA.SIMPLE_TYPES.indexOf(input.type) === -1 && input.source === this.SOURCE_TYPES.STATIC) { Object.assign(this.inputFePropertiesMap, this.processPropertiesOfComplexTypeInput(input)); } }); } private processPropertiesOfComplexTypeInput(input: ConsumptionInput): InstanceFePropertiesMap { - let inputBePropertiesMap: InstanceBePropertiesMap = new InstanceBePropertiesMap(); + const inputBePropertiesMap: InstanceBePropertiesMap = new InstanceBePropertiesMap(); inputBePropertiesMap[input.name] = [input]; - let originTypeIsVF = false; - return this.propertiesUtils.convertPropertiesMapToFEAndCreateChildren(inputBePropertiesMap, originTypeIsVF); //create flattened children and init values + const originTypeIsVF = false; + return this.propertiesUtils.convertPropertiesMapToFEAndCreateChildren(inputBePropertiesMap, originTypeIsVF); // create flattened children and init values } - onComplexPropertyChanged(property, consumptionInput) { - consumptionInput.value = JSON.stringify(property.valueObj); - this.onChange(property.valueObj, property.valueObjIsValid , consumptionInput); - } -}
\ No newline at end of file +} diff --git a/catalog-ui/src/app/ng2/pages/service-consumption-editor/service-consumption-editor.module.ts b/catalog-ui/src/app/ng2/pages/service-consumption-editor/service-consumption-editor.module.ts index e37cd76716..43e88eb0dc 100644 --- a/catalog-ui/src/app/ng2/pages/service-consumption-editor/service-consumption-editor.module.ts +++ b/catalog-ui/src/app/ng2/pages/service-consumption-editor/service-consumption-editor.module.ts @@ -1,11 +1,11 @@ -import { NgModule } from "@angular/core"; -import {CommonModule} from "@angular/common"; -import {ServiceConsumptionCreatorComponent} from "./service-consumption-editor.component"; -import {FormsModule} from "@angular/forms"; -import {FormElementsModule} from "app/ng2/components/ui/form-components/form-elements.module"; -import {UiElementsModule} from "app/ng2/components/ui/ui-elements.module"; -import {PropertyTableModule} from 'app/ng2/components/logic/properties-table/property-table.module'; -import {TranslateModule} from 'app/ng2/shared/translator/translate.module'; +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { PropertyTableModule } from 'app/ng2/components/logic/properties-table/property-table.module'; +import { FormElementsModule } from 'app/ng2/components/ui/form-components/form-elements.module'; +import { UiElementsModule } from 'app/ng2/components/ui/ui-elements.module'; +import { TranslateModule } from 'app/ng2/shared/translator/translate.module'; +import { ServiceConsumptionCreatorComponent } from './service-consumption-editor.component'; @NgModule({ declarations: [ @@ -25,4 +25,4 @@ import {TranslateModule} from 'app/ng2/shared/translator/translate.module'; providers: [] }) export class ServiceConsumptionCreatorModule { -}
\ No newline at end of file +} diff --git a/catalog-ui/src/app/ng2/pages/service-dependencies-editor/service-dependencies-editor.component.ts b/catalog-ui/src/app/ng2/pages/service-dependencies-editor/service-dependencies-editor.component.ts index 271dd4ada0..708742ae0c 100644 --- a/catalog-ui/src/app/ng2/pages/service-dependencies-editor/service-dependencies-editor.component.ts +++ b/catalog-ui/src/app/ng2/pages/service-dependencies-editor/service-dependencies-editor.component.ts @@ -14,20 +14,23 @@ * permissions and limitations under the License. */ import { Component } from '@angular/core'; -import {ServiceServiceNg2} from "app/ng2/services/component-services/service.service"; -import {ConstraintObjectUI, OPERATOR_TYPES} from 'app/ng2/components/logic/service-dependencies/service-dependencies.component'; -import {ServiceInstanceObject, PropertyBEModel, InputBEModel} from 'app/models'; +import { InputBEModel, PropertyBEModel } from 'app/models'; +import { ConstraintObjectUI, OPERATOR_TYPES } from 'app/ng2/components/logic/service-dependencies/service-dependencies.component'; +import { DropdownValue } from 'app/ng2/components/ui/form-components/dropdown/ui-element-dropdown.component'; +import { ServiceServiceNg2 } from 'app/ng2/services/component-services/service.service'; import { PROPERTY_DATA } from 'app/utils'; -import {DropdownValue} from 'app/ng2/components/ui/form-components/dropdown/ui-element-dropdown.component'; +import { ServiceInstanceObject } from '../../../models/service-instance-properties-and-interfaces'; -export class UIDropDownSourceTypesElement extends DropdownValue{ - options: Array<any>; +export class UIDropDownSourceTypesElement extends DropdownValue { + options: any[]; assignedLabel: string; type: string; - constructor(input?: any){ - if(input) { - let value = input.value || ''; - let label = input.label || ''; + constructor(input?: any) { + if (input) { + const value = input.value || ''; + const label = input.label || ''; + // const hidden = input.hidden || ''; + // const selected = input.selected || ''; super(value, label); this.options = input.options; this.assignedLabel = input.assignedLabel; @@ -36,10 +39,11 @@ export class UIDropDownSourceTypesElement extends DropdownValue{ } } +// tslint:disable-next-line:max-classes-per-file @Component({ selector: 'service-dependencies-editor', templateUrl: './service-dependencies-editor.component.html', - styleUrls:['./service-dependencies-editor.component.less'], + styleUrls: ['./service-dependencies-editor.component.less'], providers: [ServiceServiceNg2] }) @@ -47,44 +51,42 @@ export class ServiceDependenciesEditorComponent { input: { serviceRuleIndex: number, - serviceRules: Array<ConstraintObjectUI>, + serviceRules: ConstraintObjectUI[], compositeServiceName: string, currentServiceName: string, - parentServiceInputs: Array<InputBEModel>, - selectedInstanceProperties: Array<PropertyBEModel>, - operatorTypes: Array<DropdownValue>, - selectedInstanceSiblings: Array<ServiceInstanceObject> + parentServiceInputs: InputBEModel[], + selectedInstanceProperties: PropertyBEModel[], + operatorTypes: DropdownValue[], + selectedInstanceSiblings: ServiceInstanceObject[] }; currentServiceName: string; - selectedServiceProperties: Array<PropertyBEModel>; + selectedServiceProperties: PropertyBEModel[]; selectedPropertyObj: PropertyBEModel; - ddValueSelectedServicePropertiesNames: Array<DropdownValue>; - operatorTypes: Array<DropdownValue>; - sourceTypes: Array<UIDropDownSourceTypesElement> = []; + ddValueSelectedServicePropertiesNames: DropdownValue[]; + operatorTypes: DropdownValue[]; + sourceTypes: UIDropDownSourceTypesElement[] = []; currentRule: ConstraintObjectUI; currentIndex: number; - listOfValuesToAssign: Array<DropdownValue>; - listOfSourceOptions: Array<PropertyBEModel>; + listOfValuesToAssign: DropdownValue[]; + listOfSourceOptions: PropertyBEModel[]; assignedValueLabel: string; - serviceRulesList: Array<ConstraintObjectUI>; - + serviceRulesList: ConstraintObjectUI[]; SOURCE_TYPES = { STATIC: {label: 'Static', value: 'static'}, SERVICE_PROPERTY: {label: 'Service Property', value: 'property'} }; - ngOnInit() { this.currentIndex = this.input.serviceRuleIndex; this.serviceRulesList = this.input.serviceRules; this.currentRule = this.serviceRulesList && this.input.serviceRuleIndex >= 0 ? - this.serviceRulesList[this.input.serviceRuleIndex]: - new ConstraintObjectUI({sourceName: this.SOURCE_TYPES.STATIC.value, sourceType: this.SOURCE_TYPES.STATIC.value, value: "", constraintOperator: OPERATOR_TYPES.EQUAL}); + this.serviceRulesList[this.input.serviceRuleIndex] : + new ConstraintObjectUI({sourceName: this.SOURCE_TYPES.STATIC.value, sourceType: this.SOURCE_TYPES.STATIC.value, value: '', constraintOperator: OPERATOR_TYPES.EQUAL}); this.currentServiceName = this.input.currentServiceName; this.operatorTypes = this.input.operatorTypes; this.selectedServiceProperties = this.input.selectedInstanceProperties; - this.ddValueSelectedServicePropertiesNames = _.map(this.input.selectedInstanceProperties, prop => new DropdownValue(prop.name, prop.name)); + this.ddValueSelectedServicePropertiesNames = _.map(this.input.selectedInstanceProperties, (prop) => new DropdownValue(prop.name, prop.name)); this.initSourceTypes(); this.syncRuleData(); this.updateSourceTypesRelatedValues(); @@ -100,7 +102,7 @@ export class ServiceDependenciesEditorComponent { type: this.SOURCE_TYPES.SERVICE_PROPERTY.value, options: this.input.parentServiceInputs }); - _.forEach(this.input.selectedInstanceSiblings, sib => + _.forEach(this.input.selectedInstanceSiblings, (sib) => this.sourceTypes.push({ label: sib.name, value: sib.name, @@ -112,28 +114,27 @@ export class ServiceDependenciesEditorComponent { } syncRuleData() { - if(!this.currentRule.sourceName && this.currentRule.sourceType === this.SOURCE_TYPES.STATIC.value) { + if (!this.currentRule.sourceName && this.currentRule.sourceType === this.SOURCE_TYPES.STATIC.value) { this.currentRule.sourceName = this.SOURCE_TYPES.STATIC.value; } - this.selectedPropertyObj = _.find(this.selectedServiceProperties, prop => prop.name === this.currentRule.servicePropertyName); + this.selectedPropertyObj = _.find(this.selectedServiceProperties, (prop) => prop.name === this.currentRule.servicePropertyName); this.updateOperatorTypesList(); this.updateSourceTypesRelatedValues(); } updateOperatorTypesList() { if (this.selectedPropertyObj && PROPERTY_DATA.SIMPLE_TYPES_COMPARABLE.indexOf(this.selectedPropertyObj.type) === -1) { - this.operatorTypes = [{label: "=", value: OPERATOR_TYPES.EQUAL}]; + this.operatorTypes = [{label: '=', value: OPERATOR_TYPES.EQUAL}]; this.currentRule.constraintOperator = OPERATOR_TYPES.EQUAL; - } - else { + } else { this.operatorTypes = this.input.operatorTypes; } } updateSourceTypesRelatedValues() { - if(this.currentRule.sourceName) { - let selectedSourceType: UIDropDownSourceTypesElement = this.sourceTypes.find( - t => t.value === this.currentRule.sourceName && t.type === this.currentRule.sourceType + if (this.currentRule.sourceName) { + const selectedSourceType: UIDropDownSourceTypesElement = this.sourceTypes.find( + (t) => t.value === this.currentRule.sourceName && t.type === this.currentRule.sourceType ); this.listOfSourceOptions = selectedSourceType.options || []; this.assignedValueLabel = selectedSourceType.assignedLabel || this.SOURCE_TYPES.STATIC.label; @@ -150,7 +151,7 @@ export class ServiceDependenciesEditorComponent { } onServicePropertyChanged() { - this.selectedPropertyObj = _.find(this.selectedServiceProperties, prop => prop.name === this.currentRule.servicePropertyName); + this.selectedPropertyObj = _.find(this.selectedServiceProperties, (prop) => prop.name === this.currentRule.servicePropertyName); this.updateOperatorTypesList(); this.filterOptionsByType(); this.currentRule.value = ''; @@ -165,11 +166,11 @@ export class ServiceDependenciesEditorComponent { } filterOptionsByType() { - if(!this.selectedPropertyObj) { + if (!this.selectedPropertyObj) { this.listOfValuesToAssign = []; return; } - this.listOfValuesToAssign = this.listOfSourceOptions.reduce((result, op:PropertyBEModel) => { + this.listOfValuesToAssign = this.listOfSourceOptions.reduce((result, op: PropertyBEModel) => { if (op.type === this.selectedPropertyObj.type && (!op.schemaType || op.schemaType === this.selectedPropertyObj.schemaType)) { result.push(new DropdownValue(op.name, op.name)); } @@ -182,11 +183,11 @@ export class ServiceDependenciesEditorComponent { } checkFormValidForSubmit() { - if(!this.serviceRulesList) { //for create modal - let isStatic = this.currentRule.sourceName === this.SOURCE_TYPES.STATIC.value; + if (!this.serviceRulesList) { // for create modal + const isStatic = this.currentRule.sourceName === this.SOURCE_TYPES.STATIC.value; return this.currentRule.isValidRule(isStatic); } - //for update all rules - return this.serviceRulesList.every(rule => rule.isValidRule(rule.sourceName === this.SOURCE_TYPES.STATIC.value)); + // for update all rules + return this.serviceRulesList.every((rule) => rule.isValidRule(rule.sourceName === this.SOURCE_TYPES.STATIC.value)); } -}
\ No newline at end of file +} diff --git a/catalog-ui/src/app/ng2/pages/service-dependencies-editor/service-dependencies-editor.module.ts b/catalog-ui/src/app/ng2/pages/service-dependencies-editor/service-dependencies-editor.module.ts index 98ac997bf7..7b128f4468 100644 --- a/catalog-ui/src/app/ng2/pages/service-dependencies-editor/service-dependencies-editor.module.ts +++ b/catalog-ui/src/app/ng2/pages/service-dependencies-editor/service-dependencies-editor.module.ts @@ -1,9 +1,9 @@ -import { NgModule } from "@angular/core"; -import {CommonModule} from "@angular/common"; -import {ServiceDependenciesEditorComponent} from "./service-dependencies-editor.component"; -import {FormsModule} from "@angular/forms"; -import {FormElementsModule} from "app/ng2/components/ui/form-components/form-elements.module"; -import {UiElementsModule} from "app/ng2/components/ui/ui-elements.module"; +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { FormElementsModule } from 'app/ng2/components/ui/form-components/form-elements.module'; +import { UiElementsModule } from 'app/ng2/components/ui/ui-elements.module'; +import { ServiceDependenciesEditorComponent } from './service-dependencies-editor.component'; @NgModule({ declarations: [ @@ -22,4 +22,4 @@ import {UiElementsModule} from "app/ng2/components/ui/ui-elements.module"; providers: [] }) export class ServiceDependenciesEditorModule { -}
\ No newline at end of file +} diff --git a/catalog-ui/src/app/ng2/pages/workspace/activity-log/activity-log.component.html b/catalog-ui/src/app/ng2/pages/workspace/activity-log/activity-log.component.html new file mode 100644 index 0000000000..d7cf2f930a --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/activity-log/activity-log.component.html @@ -0,0 +1,68 @@ +<!-- + ~ Copyright (C) 2018 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. +--> +<div class="activity-log"> + <div class="sdc-filter-bar-wrapper"> + <sdc-filter-bar + [placeHolder]="'Search...'" + [testId]="activityLogSearchBar" + (keyup)="updateFilter($event)"> + </sdc-filter-bar> + </div> + <ngx-datatable + columnMode="flex" + [footerHeight]="0" + [limit]="50" + [headerHeight]="40" + [rowHeight]="35" + #activityLogTable + [rows]="activities"> + + <ngx-datatable-column name="Time" [flexGrow]="2" [prop]="'TIMESTAMP'"> + <ng-template ngx-datatable-cell-template let-row="row"> + {{row.TIMESTAMP | date }} | {{row.TIMESTAMP | date:"HH:mm O"}} + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column name="Action" [flexGrow]="3" [prop]="'ACTION'"> + <ng-template ngx-datatable-cell-template let-row="row"> + {{row.ACTION}} + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column name="Comment" [flexGrow]="5" [prop]="'COMMENT'"> + <ng-template ngx-datatable-cell-template let-row="row"> + <span sdc-tooltip [tooltip-text]="row.COMMENT">{{ row.COMMENT }}</span> + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column name="Modifier" [flexGrow]="3" [prop]="'MODIFIER'"> + <ng-template ngx-datatable-cell-template let-row="row"> + {{ row.MODIFIER }} + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column name="Status" [flexGrow]="1" [prop]="'STATUS'"> + <ng-template ngx-datatable-cell-template let-row="row"> + <svg-icon-label + [name]="row.STATUS <= 399 ? 'success' : 'icons_close'" + [mode]="row.STATUS <= 399 ? 'success' : 'error'" + [size]="'medium'" + [label]="row.STATUS" + [labelPlacement]="'left'" + [labelClassName]="'label'" + > + </svg-icon-label> + </ng-template> + </ngx-datatable-column> + </ngx-datatable> + +</div>
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/workspace/activity-log/activity-log.component.less b/catalog-ui/src/app/ng2/pages/workspace/activity-log/activity-log.component.less new file mode 100644 index 0000000000..4845f4f606 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/activity-log/activity-log.component.less @@ -0,0 +1,8 @@ +.sdc-filter-bar-wrapper { + sdc-filter-bar { + flex: 0 0 30%; + } + display: flex; + justify-content: flex-end; + margin-bottom: 10px; +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/workspace/activity-log/activity-log.component.spec.ts b/catalog-ui/src/app/ng2/pages/workspace/activity-log/activity-log.component.spec.ts new file mode 100644 index 0000000000..25651e0c1f --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/activity-log/activity-log.component.spec.ts @@ -0,0 +1,84 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { async, ComponentFixture } from '@angular/core/testing'; +import { NgxDatatableModule } from '@swimlane/ngx-datatable'; +import { SdcUiServices } from 'onap-ui-angular'; +import 'rxjs/add/observable/of'; +import { Observable } from 'rxjs/Observable'; +import { ConfigureFn, configureTests } from '../../../../../jest/test-config.helper'; +import { ComponentMetadata } from '../../../../models/component-metadata'; +import { ActivityLogService } from '../../../services/activity-log.service'; +import { WorkspaceService } from '../workspace.service'; +import { ActivityLogComponent } from './activity-log.component'; + +describe('activity log component', () => { + + let fixture: ComponentFixture<ActivityLogComponent>; + let activityLogServiceMock: Partial<ActivityLogService>; + let workspaceServiceMock: Partial<WorkspaceService>; + let loaderServiceMock: Partial<SdcUiServices.LoaderService>; + let componentMetadataMock: ComponentMetadata; + + const mockLogs = '[' + + '{"MODIFIER":"Carlos Santana(m08740)","COMMENT":"comment","STATUS":"200","ACTION":"Checkout","TIMESTAMP":"2018-11-19 13:00:02.388 UTC"},' + + '{"MODIFIER":"John Doe(m08741)","COMMENT":"comment","STATUS":"200","ACTION":"Checkin","TIMESTAMP":"2018-11-20 13:00:02.388 UTC"},' + + '{"MODIFIER":"Jane Doe(m08742)","COMMENT":"comment","STATUS":"200","ACTION":"Checkout","TIMESTAMP":"2018-11-21 13:00:02.388 UTC"}' + + ']'; + + beforeEach( + async(() => { + + componentMetadataMock = new ComponentMetadata(); + componentMetadataMock.uniqueId = 'fake'; + componentMetadataMock.componentType = 'SERVICE'; + + activityLogServiceMock = { + getActivityLog : jest.fn().mockImplementation((type, id) => Observable.of(JSON.parse(mockLogs)) ) + }; + + workspaceServiceMock = { + metadata : componentMetadataMock + }; + + loaderServiceMock = { + activate : jest.fn(), + deactivate: jest.fn() + }; + + const configure: ConfigureFn = (testBed) => { + testBed.configureTestingModule({ + declarations: [ActivityLogComponent], + imports: [NgxDatatableModule], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + { provide: WorkspaceService, useValue: workspaceServiceMock }, + { provide: ActivityLogService, useValue: activityLogServiceMock }, + { provide: SdcUiServices.LoaderService, useValue: loaderServiceMock } + ], + }); + }; + + configureTests(configure).then((testBed) => { + fixture = testBed.createComponent(ActivityLogComponent); + }); + }) + ); + + it('should see exactly 3 activity logs', () => { + fixture.componentInstance.ngOnInit(); + expect(fixture.componentInstance.activities.length).toBe(3); + }); + + it('should filter out 1 element when searching', () => { + fixture.componentInstance.ngOnInit(); + + const event = { + target : { + value : 'Checkin' + } + }; + + expect(fixture.componentInstance.activities.length).toBe(3); + fixture.componentInstance.updateFilter(event); + expect(fixture.componentInstance.activities.length).toBe(1); + }); +}); diff --git a/catalog-ui/src/app/ng2/pages/workspace/activity-log/activity-log.component.ts b/catalog-ui/src/app/ng2/pages/workspace/activity-log/activity-log.component.ts new file mode 100644 index 0000000000..84fb81a1ef --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/activity-log/activity-log.component.ts @@ -0,0 +1,48 @@ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { SdcUiServices } from 'onap-ui-angular'; +import { Activity } from '../../../../models/activity'; +import { ActivityLogService } from '../../../services/activity-log.service'; +import { WorkspaceService } from '../workspace.service'; + +@Component({ + selector: 'activity-log', + templateUrl: './activity-log.component.html', + styleUrls: ['./activity-log.component.less', '../../../../../assets/styles/table-style.less'] +}) +export class ActivityLogComponent implements OnInit { + + activities: Activity[] = []; + temp: Activity[] = []; + + constructor(private workspaceService: WorkspaceService, + private activityLogService: ActivityLogService, + private loaderService: SdcUiServices.LoaderService) { + } + + ngOnInit(): void { + this.loaderService.activate(); + const componentId: string = this.workspaceService.metadata.uniqueId; + const componentType: string = this.workspaceService.metadata.componentType; + this.activityLogService.getActivityLog(componentType, componentId).subscribe((logs) => { + this.activities = logs; + this.temp = [...logs]; + this.loaderService.deactivate(); + }, (error) => { this.loaderService.deactivate(); }); + } + + updateFilter(event) { + const val = event.target.value.toLowerCase(); + + // filter our data + const temp = this.temp.filter((activity: Activity) => { + return !val || + activity.COMMENT.toLowerCase().indexOf(val) !== -1 || + activity.STATUS.toLowerCase().indexOf(val) !== -1 || + activity.ACTION.toLowerCase().indexOf(val) !== -1 || + activity.MODIFIER.toLowerCase().indexOf(val) !== -1; + }); + + // update the rows + this.activities = temp; + } +} diff --git a/catalog-ui/src/app/ng2/pages/workspace/activity-log/activity-log.module.ts b/catalog-ui/src/app/ng2/pages/workspace/activity-log/activity-log.module.ts new file mode 100644 index 0000000000..39334d8cde --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/activity-log/activity-log.module.ts @@ -0,0 +1,28 @@ +import {CommonModule} from "@angular/common"; +import {NgModule} from "@angular/core"; +import {SdcUiComponentsModule} from "onap-ui-angular"; +import {GlobalPipesModule} from "../../../pipes/global-pipes.module"; +import {ActivityLogComponent} from "./activity-log.component"; +import {ActivityLogService} from "../../../services/activity-log.service"; +import {NgxDatatableModule} from "@swimlane/ngx-datatable"; + +@NgModule({ + declarations: [ + ActivityLogComponent + ], + imports: [ + CommonModule, + SdcUiComponentsModule, + GlobalPipesModule, + NgxDatatableModule + ], + exports: [ + ActivityLogComponent + ], + entryComponents: [ + ActivityLogComponent + ], + providers: [ ActivityLogService ] +}) +export class ActivityLogModule { +} diff --git a/catalog-ui/src/app/ng2/pages/workspace/attributes/attribute-modal.component.html b/catalog-ui/src/app/ng2/pages/workspace/attributes/attribute-modal.component.html new file mode 100644 index 0000000000..bd30a469e0 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/attributes/attribute-modal.component.html @@ -0,0 +1,104 @@ +<form> + <div class="attr-container"> + + <div class="attr-col"> + <!-- ATTRIBUTE NAME - MANDATORY --> + <div> + <sdc-input + #attributeName + label="Name" + [required]="true" + [(value)]="attributeToEdit.name" + [disabled]="isEdit" + name="attributeName" + testId="attributeName" + [maxLength]="255"> + </sdc-input> + <sdc-validation [validateElement]="attributeName" (validityChanged)="onValidityChange($event, 'name')"> + <sdc-required-validator message="{{'VALIDATION_ERROR_REQUIRED' | translate : { 'field' : 'Name' } }}"></sdc-required-validator> + <sdc-regex-validator message="{{'VALIDATION_ERROR_SPECIAL_CHARS_NOT_ALLOWED' | translate }}" [pattern]="validationPatterns.propertyName"></sdc-regex-validator> + </sdc-validation> + </div> + + <!-- ATTRIBUTE DESCRIPTION - OPTIONAL --> + <div> + <sdc-textarea #attributeDescription + [(value)]="attributeToEdit.description" + [required]="false" + testId="description" + [maxLength]="256" + label="Description"> + </sdc-textarea> + </div> + </div> + + <div class="attr-col"> + + <div class="attributeType"> + <!-- ATTRIBUTE TYPE - MANDATORY --> + <sdc-dropdown #attributeType [disabled]="false" label="Type" [required]="true" + [selectedOption]="toDropDownOption(this.attributeToEdit.type)" placeHolder="Choose Type" + [options]="types" (changed)="onTypeSelected($event)"> + <sdc-validation [validateElement]="attributeType" (validityChanged)="onValidityChange($event, 'type')"> + <sdc-required-validator message="'required field'"></sdc-required-validator> + </sdc-validation> + </sdc-dropdown> + </div> + + <!-- ATTRIBUTE DEFAULT VALUE TEXT - OPTIONAL --> + <div *ngIf="attributeToEdit.type != 'boolean'"> + <sdc-input + #defaultValue + [required]="false" + label="Default Value" + [(value)]="attributeToEdit.defaultValue" + [disabled]="false" + name="defaultValue" + testId="defaultValue" + [maxLength]="255" + (valueChange)="defaultValueChanged()"> + </sdc-input> + + <sdc-validation [validateElement]="defaultValue" (validityChanged)="onValidityChange($event, 'defaultValue')"> + <sdc-regex-validator *ngIf="this.attributeToEdit.defaultValue && this.attributeToEdit.defaultValue.length > 0" message="{{ this.defaultValueErrorMessage }}" + [pattern]="defaultValuePattern"></sdc-regex-validator> + <sdc-custom-validator *ngIf="this.attributeToEdit.type == 'map' && this.attributeToEdit.schema.property.type" message="{{ 'PROPERTY_EDIT_MAP_UNIQUE_KEYS' | translate }}" + [callback]="isMapUnique" [disabled]="false"></sdc-custom-validator> + </sdc-validation> + </div> + + <!-- ATTRIBUTE DEFAULT VALUE BOOLEAN- OPTIONAL --> + <div *ngIf="attributeToEdit.type == 'boolean'"> + <sdc-dropdown [disabled]="false" label="Default Value" + [required]="false" + [selectedOption]="toDropDownOption(this.attributeToEdit.defaultValue)" placeHolder="Choose Default Value" + [options]="booleanValues" (changed)="onBooleanDefaultValueSelected($event)"> + + </sdc-dropdown> + </div> + + <div *ngIf="attributeToEdit.type == 'list' || attributeToEdit.type == 'map'"> + <!-- ATTRIBUTE ENTRY SCHEMA - MANDATORY --> + <sdc-dropdown #entrySchema + [disabled]="false" label="Entry Schema" [required]="true" + [selectedOption]="toDropDownOption(this.attributeToEdit.schema.property.type)" placeHolder="Choose Schema Type" + [options]="entrySchemaValues" (changed)="onEntrySchemaTypeSelected($event)"> + <sdc-validation [validateElement]="entrySchema" (validityChanged)="onValidityChange($event, 'entrySchema')"> + <sdc-required-validator message="'required !TODO - CHANGE MESSAGE'"></sdc-required-validator> + </sdc-validation> + </sdc-dropdown> + </div> + + <!-- ATTRIBUTE HIDDEN - OPTIONAL --> + <sdc-checkbox + label="Hidden" + [checked]="attributeToEdit.hidden" + [disabled]="false" + testId="hidden" + (checkedChange)="this.onHiddenCheckboxClicked($event)" + > + </sdc-checkbox> + </div> + </div> + +</form>
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/workspace/attributes/attribute-modal.component.ts b/catalog-ui/src/app/ng2/pages/workspace/attributes/attribute-modal.component.ts new file mode 100644 index 0000000000..c703869ad2 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/attributes/attribute-modal.component.ts @@ -0,0 +1,138 @@ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { IDropDownOption } from 'onap-ui-angular/dist/form-elements/dropdown/dropdown-models'; +import { InputComponent } from 'onap-ui-angular/dist/form-elements/text-elements/input/input.component'; +import { Subject } from 'rxjs/Subject'; +import { AttributeModel } from '../../../../models/attributes'; +import { ValidationUtils } from '../../../../utils/validation-utils'; +import { CacheService } from '../../../services/cache.service'; +import { TranslateService } from '../../../shared/translator/translate.service'; +import { AttributeOptions } from './attributes-options'; + +@Component({ + selector: 'attribute-modal', + templateUrl: './attribute-modal.component.html', + styleUrls: ['./attributes.component.less'] +}) +export class AttributeModalComponent implements OnInit { + + @ViewChild('defaultValue') validatedInput: InputComponent; + + public readonly types = AttributeOptions.types; // integer, string, boolean etc. + + public readonly booleanValues = AttributeOptions.booleanValues; // true / false + + public readonly entrySchemaValues = AttributeOptions.entrySchemaValues; // integer, string, boolean, float + + public onValidationChange: Subject<boolean> = new Subject(); + + public validationPatterns: any; + public readonly listPattern = ValidationUtils.getPropertyListPatterns(); + public readonly mapPattern = ValidationUtils.getPropertyMapPatterns(); + + // The current effective default value pattern + public defaultValuePattern: string; + public defaultValueErrorMessage: string; + + // Attribute being Edited + public attributeToEdit: AttributeModel; + + constructor(private translateService: TranslateService, private cacheService: CacheService) { + this.validationPatterns = this.cacheService.get('validation').validationPatterns; + } + + ngOnInit() { + this.revalidateDefaultValue(); + } + + onHiddenCheckboxClicked(event: boolean) { + this.attributeToEdit.hidden = event; + } + + onTypeSelected(selectedElement: IDropDownOption) { + if (this.attributeToEdit.type !== selectedElement.value && selectedElement.value === 'boolean') { + this.attributeToEdit.defaultValue = ''; // Clean old value in case we choose change type to boolean + } + this.attributeToEdit.type = selectedElement.value; + this.revalidateDefaultValue(); + } + + onBooleanDefaultValueSelected(selectedElement: IDropDownOption) { + if (this.attributeToEdit.type === 'boolean') { + this.attributeToEdit.defaultValue = selectedElement.value; + } + } + + onEntrySchemaTypeSelected(selectedElement: IDropDownOption) { + this.attributeToEdit.schema.property.type = selectedElement.value; + this.revalidateDefaultValue(); + } + + onValidityChange(isValid: boolean, field: string) { + const typeIsValid = this.attributeToEdit.type && this.attributeToEdit.type.length > 0; // Make sure type is defined + + // Make sure name is defined when other fields are changed + let nameIsValid = true; + if (field !== 'name') { + nameIsValid = this.attributeToEdit.name && this.attributeToEdit.name.length > 0; + } + this.onValidationChange.next(isValid && nameIsValid && typeIsValid); + } + + defaultValueChanged() { + this.revalidateDefaultValue(); + } + + /** + * Utility function for UI that converts a simple value to IDropDownOption + * @param val + * @returns {{value: any; label: any}} + */ + toDropDownOption(val: string) { + return { value : val, label: val }; + } + + public isMapUnique = () => { + if (this.attributeToEdit && this.attributeToEdit.type === 'map' && this.attributeToEdit.defaultValue) { + return ValidationUtils.validateUniqueKeys(this.attributeToEdit.defaultValue); + } + return true; + } + + private revalidateDefaultValue() { + this.setDefaultValuePattern(this.attributeToEdit.type); + setTimeout(() => { + if (this.validatedInput) { + this.validatedInput.onKeyPress(this.attributeToEdit.defaultValue); + } }, 250); + } + + private setDefaultValuePattern(valueType: string) { + const selectedSchemaType = this.attributeToEdit.schema.property.type; + this.defaultValuePattern = '.*'; + switch (valueType) { + case 'float': + this.defaultValuePattern = this.validationPatterns.number; + this.defaultValueErrorMessage = this.translateService.translate('VALIDATION_ERROR_TYPE', { type : 'float' }); + break; + case 'integer': + this.defaultValuePattern = this.validationPatterns.integerNoLeadingZero; + this.defaultValueErrorMessage = this.translateService.translate('VALIDATION_ERROR_TYPE', { type : 'integer' }); + break; + case 'list': + if (selectedSchemaType != undefined) { + this.defaultValuePattern = this.listPattern[selectedSchemaType]; + const listTypeStr = `list of ${selectedSchemaType}s (v1, v2, ...) `; + this.defaultValueErrorMessage = this.translateService.translate('VALIDATION_ERROR_TYPE', { type : listTypeStr }); + } + break; + case 'map': + if (selectedSchemaType != undefined) { + this.defaultValuePattern = this.mapPattern[selectedSchemaType]; + const mapTypeStr = `map of ${selectedSchemaType}s (k1:v1, k2:v2, ...)`; + this.defaultValueErrorMessage = this.translateService.translate('VALIDATION_ERROR_TYPE', { type : mapTypeStr }); + } + break; + } + } + +} diff --git a/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes-modal.component.spec.ts b/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes-modal.component.spec.ts new file mode 100644 index 0000000000..99aa140dd1 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes-modal.component.spec.ts @@ -0,0 +1,128 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { async, ComponentFixture } from '@angular/core/testing'; +import { ConfigureFn, configureTests } from '../../../../../jest/test-config.helper'; +import { AttributeModel } from '../../../../models/attributes'; +import { ValidationUtils } from '../../../../utils/validation-utils'; +import { CacheService } from '../../../services/cache.service'; +import { TranslatePipe } from '../../../shared/translator/translate.pipe'; +import { TranslateService } from '../../../shared/translator/translate.service'; +import { AttributeModalComponent } from './attribute-modal.component'; + +describe('attributes modal component', () => { + + let fixture: ComponentFixture<AttributeModalComponent>; + + // Mocks + let translateServiceMock: Partial<TranslateService>; + let cacheServiceMock: Partial<CacheService>; + + const validationPatterns = { + integerNoLeadingZero : 'int_regx', + number : 'number_regx' + }; + + const newAttribute = { + uniqueId: '1', name: 'attr1', description: 'description1', type: 'string', hidden: false, defaultValue: 'val1', schema: null + }; + + beforeEach( + async(() => { + + translateServiceMock = { + translate: jest.fn() + }; + + cacheServiceMock = { + get: jest.fn().mockImplementation((k) => { + return { validationPatterns}; + } ) + }; + + const configure: ConfigureFn = (testBed) => { + testBed.configureTestingModule({ + declarations: [AttributeModalComponent, TranslatePipe], + imports: [], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + {provide: TranslateService, useValue: translateServiceMock}, + {provide: CacheService, useValue: cacheServiceMock}, + ] + }); + }; + + configureTests(configure).then((testBed) => { + fixture = testBed.createComponent(AttributeModalComponent); + }); + }) + ); + + it('test that when hidden is clicked, hidden attribute is set', async () => { + fixture.componentInstance.attributeToEdit = new AttributeModel(); + const hidden = fixture.componentInstance.attributeToEdit.hidden; + fixture.componentInstance.ngOnInit(); + + expect(hidden).toBe(false); + fixture.componentInstance.onHiddenCheckboxClicked(true); + expect(fixture.componentInstance.attributeToEdit.hidden).toBe(true); + }); + + it('test that when type is set to boolean default value is cleared', async () => { + const component = fixture.componentInstance; + component.attributeToEdit = new AttributeModel(); + component.ngOnInit(); + + component.onTypeSelected({ value : 'string', label : 'string'}); + component.attributeToEdit.defaultValue = 'some_value'; + component.onTypeSelected({ value : 'boolean', label : 'boolean'}); + expect(component.attributeToEdit.defaultValue).toBe(''); + + component.onBooleanDefaultValueSelected({ value : 'true', label : 'true'}); + expect(component.attributeToEdit.defaultValue).toBe('true'); + }); + + it('test that when certain type is selected, the correct regex pattern is chosen', async () => { + const component = fixture.componentInstance; + component.attributeToEdit = new AttributeModel(); + component.ngOnInit(); + + // integer + component.onTypeSelected({ value : 'integer', label : 'integer'}); + expect(component.defaultValuePattern).toBe(validationPatterns.integerNoLeadingZero); + + // float + component.onTypeSelected({ value : 'float', label : 'float'}); + expect(component.defaultValuePattern).toBe(validationPatterns.number); + + // list is chosen with no schema, regex pattern is set to default + component.onTypeSelected({ value : 'list', label : 'list'}); + expect(component.defaultValuePattern).toEqual('.*'); + + // schema is set to list of int + component.onEntrySchemaTypeSelected({ value : 'integer', label : 'integer' }); + expect(component.defaultValuePattern).toEqual(ValidationUtils.getPropertyListPatterns().integer); + + // schema is set to list of float + component.onEntrySchemaTypeSelected({ value : 'float', label : 'float' }); + expect(component.defaultValuePattern).toEqual(ValidationUtils.getPropertyListPatterns().float); + + // map is selected (float schema is still selected from previous line) + component.onTypeSelected({ value : 'map', label : 'map'}); + expect(component.defaultValuePattern).toEqual(ValidationUtils.getPropertyMapPatterns().float); + + // change schema type to boolean + component.onEntrySchemaTypeSelected({ value : 'boolean', label : 'boolean' }); + }); + + it('should detect map with non-unique keys', async () => { + const component = fixture.componentInstance; + component.attributeToEdit = new AttributeModel(); + component.ngOnInit(); + expect(component.isMapUnique()).toBe(true); // map is not selected so return true by default + component.onTypeSelected({ value : 'map', label : 'map'}); + component.onEntrySchemaTypeSelected({ value : 'boolean', label : 'boolean' }); + component.attributeToEdit.defaultValue = '"1":true,"2":false'; + expect(component.isMapUnique()).toBe(true); + component.attributeToEdit.defaultValue = '"1":true,"1":false'; + expect(component.isMapUnique()).toBe(false); + }); +}); diff --git a/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes-options.ts b/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes-options.ts new file mode 100644 index 0000000000..2a6924bc5e --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes-options.ts @@ -0,0 +1,60 @@ +import { IDropDownOption } from 'onap-ui-angular/dist/form-elements/dropdown/dropdown-models'; + +export class AttributeOptions { + public static readonly types: IDropDownOption[] = [ + { + label: 'integer', + value: 'integer', + }, + { + label: 'string', + value: 'string', + }, + { + label: 'float', + value: 'float' + }, + { + label: 'boolean', + value: 'boolean' + }, + { + label: 'list', + value: 'list' + }, + { + label: 'map', + value: 'map' + } + ]; + + public static readonly booleanValues: IDropDownOption[] = [ + { + label: 'true', + value: 'true', + }, + { + label: 'false', + value: 'false', + } + ]; + + public static readonly entrySchemaValues: IDropDownOption[] = [ + { + label: 'integer', + value: 'integer', + }, + { + label: 'string', + value: 'string', + }, + { + label: 'float', + value: 'float' + }, + { + label: 'boolean', + value: 'boolean' + } + ]; +} diff --git a/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.component.html b/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.component.html new file mode 100644 index 0000000000..00a7a5cec0 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.component.html @@ -0,0 +1,93 @@ +<!-- + ~ Copyright (C) 2018 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. +--> +<div class="workspace-attributes"> + + <div class="action-bar-wrapper"> + <svg-icon-label + *ngIf="!(this.isViewOnly$ | async)" + class="add-attr-icon" + [name]="'plus'" + [mode]="'primary'" + [size]="'medium'" + [label]="'Add'" + [labelPlacement]="'right'" + [labelClassName]="'externalActionLabel'" + (click)="onAddAttribute()"> + </svg-icon-label> + </div> + + <ngx-datatable + columnMode="flex" + [footerHeight]="0" + [limit]="50" + [headerHeight]="40" + [rowHeight]="35" + [rows]="attributes" + #componentAttributesTable + (activate)="onExpandRow($event)"> + + <ngx-datatable-row-detail [rowHeight]="80"> + <ng-template let-row="row" let-expanded="expanded" ngx-datatable-row-detail-template> + <div>{{row.description}}</div> + </ng-template> + </ngx-datatable-row-detail> + + <ngx-datatable-column [resizeable]="false" name="Name" [flexGrow]="2"> + + <ng-template ngx-datatable-cell-template let-row="row" let-expanded="expanded"> + <div class="expand-collapse-cell"> + <svg-icon [clickable]="true" class="expand-collapse-icon" + [name]="expanded ? 'caret1-up-o': 'caret1-down-o'" [mode]="'primary'" + [size]="'medium'"></svg-icon> + <span>{{ row.name }}</span> + </div> + </ng-template> + + </ngx-datatable-column> + + <ngx-datatable-column [resizeable]="false" name="Type" [flexGrow]="1"> + <ng-template ngx-datatable-cell-template let-row="row"> + {{row.type}} + </ng-template> + </ngx-datatable-column> + + <ngx-datatable-column [resizeable]="false" name="Default Value" [flexGrow]="3"> + <ng-template ngx-datatable-cell-template let-row="row"> + {{row.defaultValue}} + </ng-template> + </ngx-datatable-column> + + <ngx-datatable-column *ngIf="!(this.isViewOnly$ | async)" [resizeable]="false" name="Action" [flexGrow]="1"> + <ng-template ngx-datatable-cell-template let-row="row" let-rowIndex="rowIndex"> + <div class="actionColumn"> + <svg-icon [clickable]="true" + [mode]="'primary2'" + [name]="'edit-o'" + [size]="'medium'" + (click)="onEditAttribute($event, row)"> + </svg-icon> + <svg-icon [clickable]="true" + [mode]="'primary2'" + [name]="'trash-o'" + (click)="onDeleteAttribute($event, row)" + [size]="'medium'"> + </svg-icon> + </div> + </ng-template> + </ngx-datatable-column> + + </ngx-datatable> +</div>
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.component.less b/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.component.less new file mode 100644 index 0000000000..3e91ae4689 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.component.less @@ -0,0 +1,36 @@ +.action-bar-wrapper { + flex: 0 0 30%; + display: flex; + justify-content: flex-end; + margin-bottom: 10px; +} + +.add-attr-icon{ + cursor: pointer; +} + +.attr-container { + display: flex; + justify-content: space-between; + + .attr-col { + display: flex; + flex-direction: column; + max-width: 275px; + flex-grow: 1; + } + +} + +.attributeType { + margin-bottom: 10px; +} + +sdc-checkbox { + margin-top: 20px; +} + +.actionColumn { + text-align: center; + padding: 5px; +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.component.spec.ts b/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.component.spec.ts new file mode 100644 index 0000000000..f676e2b4d9 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.component.spec.ts @@ -0,0 +1,182 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { async, ComponentFixture } from '@angular/core/testing'; +import { NgxDatatableModule } from '@swimlane/ngx-datatable'; +import { SdcUiCommon, SdcUiComponents, SdcUiServices } from 'onap-ui-angular'; +import 'rxjs/add/observable/of'; +import { Observable } from 'rxjs/Rx'; +import { ConfigureFn, configureTests } from '../../../../../jest/test-config.helper'; +import { ComponentMetadata } from '../../../../models/component-metadata'; +import { ModalsHandler } from '../../../../utils'; +import { TopologyTemplateService } from '../../../services/component-services/topology-template.service'; +import { TranslateService } from '../../../shared/translator/translate.service'; +import { WorkspaceService } from '../workspace.service'; +import { AttributesComponent } from './attributes.component'; + +describe('attributes component', () => { + + let fixture: ComponentFixture<AttributesComponent>; + + // Mocks + let workspaceServiceMock: Partial<WorkspaceService>; + let topologyTemplateServiceMock: Partial<TopologyTemplateService>; + let loaderServiceMock: Partial<SdcUiServices.LoaderService>; + let componentMetadataMock: ComponentMetadata; + let modalServiceMock: Partial<SdcUiServices.ModalService>; + + const mockAttributesList = [ + { uniqueId: '1', name: 'attr1', description: 'description1', type: 'string', hidden: false, defaultValue: 'val1', schema: null }, + { uniqueId : '2', name : 'attr2', description: 'description2', type : 'int', hidden : false, defaultValue : 1, schema : null}, + { uniqueId : '3', name : 'attr3', description: 'description3', type : 'double', hidden : false, defaultValue : 1.0, schema : null}, + { uniqueId : '4', name : 'attr4', description: 'description4', type : 'boolean', hidden : false, defaultValue : true, schema : null}, + ]; + + const newAttribute = { + uniqueId : '5', name : 'attr5', description: 'description5', type : 'string', hidden : false, defaultValue : 'val5', schema : null + }; + const updatedAttribute = { + uniqueId : '2', name : 'attr2', description: 'description_new', type : 'string', hidden : false, defaultValue : 'new_val2', schema : null + }; + const errorAttribute = { + uniqueId : '99', name : 'attr99', description: 'description_error', type : 'string', hidden : false, defaultValue : 'error', schema : null + }; + + beforeEach( + async(() => { + + componentMetadataMock = new ComponentMetadata(); + componentMetadataMock.uniqueId = 'fake'; + componentMetadataMock.componentType = 'VL'; + + topologyTemplateServiceMock = { + getComponentAttributes: jest.fn().mockResolvedValue({ attributes : mockAttributesList }), + addAttributeAsync: jest.fn().mockImplementation( + (compType, cUid, attr) => { + if (attr === errorAttribute) { + return Observable.throwError('add_error').toPromise(); + } else { + return Observable.of(newAttribute).toPromise(); + } + } + ), + updateAttributeAsync: jest.fn().mockImplementation( + (compType, cUid, attr) => { + if (attr === errorAttribute) { + return Observable.throwError('update_error').toPromise(); + } else { + return Observable.of(updatedAttribute).toPromise(); + } + } + ), + deleteAttributeAsync: jest.fn().mockImplementation((cid, ctype, attr) => Observable.of(attr)) + }; + + workspaceServiceMock = { + metadata: componentMetadataMock + }; + + const customModalInstance = { innerModalContent: { instance: { onValidationChange: { subscribe: jest.fn()}}}}; + + modalServiceMock = { + openInfoModal: jest.fn(), + openCustomModal: jest.fn().mockImplementation(() => customModalInstance) + }; + + loaderServiceMock = { + activate: jest.fn(), + deactivate: jest.fn() + }; + + const configure: ConfigureFn = (testBed) => { + testBed.configureTestingModule({ + declarations: [AttributesComponent], + imports: [NgxDatatableModule], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + {provide: WorkspaceService, useValue: workspaceServiceMock}, + {provide: TopologyTemplateService, useValue: topologyTemplateServiceMock}, + {provide: ModalsHandler, useValue: {}}, + {provide: TranslateService, useValue: { translate: jest.fn() }}, + {provide: SdcUiServices.ModalService, useValue: modalServiceMock }, + {provide: SdcUiServices.LoaderService, useValue: loaderServiceMock } + ], + }); + }; + + configureTests(configure).then((testBed) => { + fixture = testBed.createComponent(AttributesComponent); + }); + }) + ); + + it('should see exactly 1 attributes on init', async () => { + await fixture.componentInstance.asyncInitComponent(); + expect(fixture.componentInstance.getAttributes().length).toEqual(4); + }); + + it('should see exactly 5 attributes when adding', async () => { + await fixture.componentInstance.asyncInitComponent(); + expect(fixture.componentInstance.getAttributes().length).toEqual(4); + + await fixture.componentInstance.addOrUpdateAttribute(newAttribute, false); + expect(fixture.componentInstance.getAttributes().length).toEqual(5); + }); + + it('should see exactly 3 attributes when deleting', async () => { + await fixture.componentInstance.asyncInitComponent(); + expect(fixture.componentInstance.getAttributes().length).toEqual(4); + const attrToDelete = mockAttributesList[0]; + expect(fixture.componentInstance.getAttributes().filter((attr) => attr.uniqueId === attrToDelete.uniqueId).length).toEqual(1); + await fixture.componentInstance.deleteAttribute(attrToDelete); + expect(fixture.componentInstance.getAttributes().length).toEqual(3); + expect(fixture.componentInstance.getAttributes().filter((attr) => attr.uniqueId === attrToDelete.uniqueId).length).toEqual(0); + }); + + it('should see updated attribute', async () => { + await fixture.componentInstance.asyncInitComponent(); + + await fixture.componentInstance.addOrUpdateAttribute(updatedAttribute, true); + expect(fixture.componentInstance.getAttributes().length).toEqual(4); + const attribute = fixture.componentInstance.getAttributes().filter( (attr) => { + return attr.uniqueId === updatedAttribute.uniqueId; + })[0]; + expect(attribute.description).toEqual( 'description_new'); + }); + + it('Add fails, make sure loader is deactivated and attribute is not added', async () => { + await fixture.componentInstance.asyncInitComponent(); + const numAttributes = fixture.componentInstance.getAttributes().length; + await fixture.componentInstance.addOrUpdateAttribute(errorAttribute, false); // Add + expect(loaderServiceMock.deactivate).toHaveBeenCalled(); + expect(fixture.componentInstance.getAttributes().length).toEqual(numAttributes); + }); + + it('Update fails, make sure loader is deactivated', async () => { + await fixture.componentInstance.asyncInitComponent(); + const numAttributes = fixture.componentInstance.getAttributes().length; + await fixture.componentInstance.addOrUpdateAttribute(errorAttribute, true); // Add + expect(loaderServiceMock.deactivate).toHaveBeenCalled(); + expect(fixture.componentInstance.getAttributes().length).toEqual(numAttributes); + }); + + it('on delete modal shell be opened', async () => { + await fixture.componentInstance.asyncInitComponent(); + const event = { stopPropagation: jest.fn() }; + fixture.componentInstance.onDeleteAttribute(event, fixture.componentInstance.getAttributes()[0]); + expect(event.stopPropagation).toHaveBeenCalled(); + expect(modalServiceMock.openInfoModal).toHaveBeenCalled(); + }); + + it('on add modal shell be opened', async () => { + await fixture.componentInstance.asyncInitComponent(); + fixture.componentInstance.onAddAttribute(); + expect(modalServiceMock.openCustomModal).toHaveBeenCalled(); + }); + + it('on edit modal shell be opened', async () => { + await fixture.componentInstance.asyncInitComponent(); + const event = { stopPropagation: jest.fn() }; + fixture.componentInstance.onEditAttribute(event, fixture.componentInstance.getAttributes()[0]); + expect(event.stopPropagation).toHaveBeenCalled(); + expect(modalServiceMock.openCustomModal).toHaveBeenCalled(); + }); +}); diff --git a/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.component.ts b/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.component.ts new file mode 100644 index 0000000000..bc47f1456b --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.component.ts @@ -0,0 +1,188 @@ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { Select } from '@ngxs/store'; +import { IAttributeModel } from 'app/models'; +import * as _ from 'lodash'; +import { SdcUiCommon, SdcUiComponents, SdcUiServices } from 'onap-ui-angular'; +import { ModalComponent } from 'onap-ui-angular/dist/modals/modal.component'; +import { AttributeModel } from '../../../../models'; +import { Resource } from '../../../../models'; +import { ModalsHandler } from '../../../../utils'; +import { TopologyTemplateService } from '../../../services/component-services/topology-template.service'; +import { TranslateService } from '../../../shared/translator/translate.service'; +import { WorkspaceState } from '../../../store/states/workspace.state'; +import { WorkspaceService } from '../workspace.service'; +import { AttributeModalComponent } from './attribute-modal.component'; + +@Component({ + selector: 'attributes', + templateUrl: './attributes.component.html', + styleUrls: ['./attributes.component.less', '../../../../../assets/styles/table-style.less'] +}) +export class AttributesComponent implements OnInit { + + @Select(WorkspaceState.isViewOnly) + isViewOnly$: boolean; + + @ViewChild('componentAttributesTable') + private table: any; + + private componentType: string; + private componentUid: string; + + private attributes: IAttributeModel[] = []; + private temp: IAttributeModel[] = []; + private customModalInstance: ModalComponent; + + constructor(private workspaceService: WorkspaceService, + private topologyTemplateService: TopologyTemplateService, + private modalsHandler: ModalsHandler, + private modalService: SdcUiServices.ModalService, + private loaderService: SdcUiServices.LoaderService, + private translateService: TranslateService) { + + this.componentType = this.workspaceService.metadata.componentType; + this.componentUid = this.workspaceService.metadata.uniqueId; + } + + ngOnInit(): void { + this.asyncInitComponent(); + } + + async asyncInitComponent() { + this.loaderService.activate(); + const response = await this.topologyTemplateService.getComponentAttributes(this.componentType, this.componentUid); + this.attributes = response.attributes; + this.temp = [...response.attributes]; + this.loaderService.deactivate(); + } + + getAttributes(): IAttributeModel[] { + return this.attributes; + } + + addOrUpdateAttribute = async (attribute: AttributeModel, isEdit: boolean) => { + this.loaderService.activate(); + let attributeFromServer: AttributeModel; + this.temp = [...this.attributes]; + + const deactivateLoader = () => { + this.loaderService.deactivate(); + return undefined; + }; + + if (isEdit) { + attributeFromServer = await this.topologyTemplateService + .updateAttributeAsync(this.componentType, this.componentUid, attribute) + .catch(deactivateLoader); + if (attributeFromServer) { + const indexOfUpdatedAttribute = _.findIndex(this.temp, (e) => e.uniqueId === attributeFromServer.uniqueId); + this.temp[indexOfUpdatedAttribute] = attributeFromServer; + } + } else { + attributeFromServer = await this.topologyTemplateService + .addAttributeAsync(this.componentType, this.componentUid, attribute) + .catch(deactivateLoader); + if (attributeFromServer) { + this.temp.push(attributeFromServer); + } + } + this.attributes = this.temp; + this.loaderService.deactivate(); + } + + deleteAttribute = async (attributeToDelete: AttributeModel) => { + this.loaderService.activate(); + this.temp = [...this.attributes]; + const res = await this.topologyTemplateService.deleteAttributeAsync(this.componentType, this.componentUid, attributeToDelete); + _.remove(this.temp, (attr) => attr.uniqueId === attributeToDelete.uniqueId); + this.attributes = this.temp; + this.loaderService.deactivate(); + }; + + openAddEditModal(selectedRow: AttributeModel, isEdit: boolean) { + const component = new Resource(undefined, undefined, undefined); + component.componentType = this.componentType; + component.uniqueId = this.componentUid; + + const title: string = this.translateService.translate('ATTRIBUTE_DETAILS_MODAL_TITLE'); + const attributeModalConfig = { + title, + size: 'md', + type: SdcUiCommon.ModalType.custom, + buttons: [ + { + id: 'save', + text: 'Save', + // spinner_position: Placement.left, + size: 'sm', + callback: () => this.modalCallBack(isEdit), + closeModal: true, + disabled: false, + } + ] as SdcUiCommon.IModalButtonComponent[] + }; + + this.customModalInstance = this.modalService.openCustomModal(attributeModalConfig, AttributeModalComponent, { attributeToEdit: selectedRow }); + this.customModalInstance.innerModalContent.instance. + onValidationChange.subscribe((isValid) => this.customModalInstance.getButtonById('save').disabled = !isValid); + } + + /*********************** + * Call Backs from UI * + ***********************/ + + /** + * Called when 'Add' is clicked + */ + onAddAttribute() { + this.openAddEditModal(new AttributeModel(), false); + } + + /** + * Called when 'Edit' button is clicked + */ + onEditAttribute(event, row) { + event.stopPropagation(); + + const attributeToEdit: AttributeModel = new AttributeModel(row); + this.openAddEditModal(attributeToEdit, true); + } + + /** + * Called when 'Delete' button is clicked + */ + onDeleteAttribute(event, row: AttributeModel) { + event.stopPropagation(); + const onOk = () => { + this.deleteAttribute(row); + }; + + const title: string = this.translateService.translate('ATTRIBUTE_VIEW_DELETE_MODAL_TITLE'); + const message: string = this.translateService.translate('ATTRIBUTE_VIEW_DELETE_MODAL_TEXT'); + const okButton = new SdcUiComponents.ModalButtonComponent(); + okButton.testId = 'OK'; + okButton.text = 'OK'; + okButton.type = SdcUiCommon.ButtonType.info; + okButton.closeModal = true; + okButton.callback = onOk; + + this.modalService.openInfoModal(title, message, 'delete-modal', [okButton]); + } + + onExpandRow(event) { + if (event.type === 'click') { + this.table.rowDetail.toggleExpandRow(event.row); + } + } + + /** + * Callback from Modal after "Save" is clicked + * + * @param {boolean} isEdit - Whether modal is edit or add attribute + */ + modalCallBack = (isEdit: boolean) => { + const attribute: AttributeModel = this.customModalInstance.innerModalContent.instance.attributeToEdit; + this.addOrUpdateAttribute(attribute, isEdit); + } + +} diff --git a/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.module.ts b/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.module.ts new file mode 100644 index 0000000000..5abb952e37 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/attributes/attributes.module.ts @@ -0,0 +1,32 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { SdcUiComponentsModule } from 'onap-ui-angular'; +import { GlobalPipesModule } from '../../../pipes/global-pipes.module'; +import { AttributesComponent } from './attributes.component'; +import { NgxDatatableModule } from '@swimlane/ngx-datatable'; +import { TopologyTemplateService } from '../../../services/component-services/topology-template.service'; +import { AttributeModalComponent } from './attribute-modal.component'; +import { TranslateModule } from '../../../shared/translator/translate.module'; + +@NgModule({ + declarations: [ + AttributesComponent, + AttributeModalComponent + ], + imports: [ + CommonModule, + SdcUiComponentsModule, + GlobalPipesModule, + NgxDatatableModule, + TranslateModule + ], + exports: [ + AttributesComponent + ], + entryComponents: [ + AttributesComponent, AttributeModalComponent + ], + providers: [TopologyTemplateService] +}) +export class AttributesModule { +} diff --git a/catalog-ui/src/app/ng2/pages/workspace/deployment-artifacts/__snapshots__/deployment-artifacts-page.spec.ts.snap b/catalog-ui/src/app/ng2/pages/workspace/deployment-artifacts/__snapshots__/deployment-artifacts-page.spec.ts.snap new file mode 100644 index 0000000000..b53674497c --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/deployment-artifacts/__snapshots__/deployment-artifacts-page.spec.ts.snap @@ -0,0 +1,24 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`deployment artifacts page should match current snapshot of informational artifact pages component 1`] = ` +<deployment-artifact-page + addOrUpdateArtifact={[Function Function]} + artifactsService={[Function Object]} + cacheService={[Function Object]} + deleteArtifact={[Function Function]} + getEnvArtifact={[Function Function]} + modalService={[Function Object]} + openGenericArtifactBrowserModal={[Function Function]} + openPopOver={[Function Function]} + popoverContentComponent="undefined" + popoverService={[Function Object]} + sortArtifacts={[Function Function]} + store={[Function Store]} + table="undefined" + translateService={[Function Object]} + updateEnvParams={[Function Function]} + workspaceService={[Function Object]} +> + +</deployment-artifact-page> +`; diff --git a/catalog-ui/src/app/ng2/pages/workspace/deployment-artifacts/deployment-artifacts-page.component.html b/catalog-ui/src/app/ng2/pages/workspace/deployment-artifacts/deployment-artifacts-page.component.html new file mode 100644 index 0000000000..35592d846a --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/deployment-artifacts/deployment-artifacts-page.component.html @@ -0,0 +1,73 @@ +<div class="deployment-artifact-page" *ngIf="(workspaceState$ | async) as state"> + <svg-icon-label *ngIf="!state.isViewOnly" class="add-artifact-btn" [clickable]="true" [mode]="'primary'" [labelPlacement]="'right'" + [label]="'Add'" [name]="'plus'" + (click)="addOrUpdateArtifact()"></svg-icon-label> + <ngx-datatable + columnMode="flex" + [headerHeight]="40" + [footerHeight]="'undefined'" + [reorderable]="false" + [swapColumns]="false" + [rows]="deploymentArtifacts$ | async" + #deploymentArtifactsTable> + <ngx-datatable-column [resizeable]="false" name="Name" [flexGrow]="1"> + <ng-template ngx-datatable-cell-template let-row="row"> + <div *ngIf="row.generatedFromId" class="env-artifact-container"> + <div class="env-artifact"></div> + </div> + <span sdc-tooltip [tooltip-text]="row.artifactDisplayName" [tooltip-placement]="3" [attr.data-tests-id]="'artifactDisplayName_' + row.artifactDisplayName">{{row.artifactDisplayName}}</span> + <span *ngIf="row.description.length > 0" class="info"> + <svg-icon [clickable]="true" [name]="'comment'" [mode]="'primary2'" (click)="openPopOver('Description',row.description,{x:$event.pageX , y:$event.pageY },'bottom')"></svg-icon> + </span> + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column [resizeable]="false" name="Type" [flexGrow]="0.6"> + <ng-template ngx-datatable-cell-template let-row="row"> + <span sdc-tooltip [tooltip-text]="row.artifactType" [tooltip-placement]="3" [attr.data-tests-id]="'artifactType_' + row.artifactDisplayName">{{row.artifactType}}</span> + </ng-template> + </ngx-datatable-column> exactly 2 tosca artifacts + <ngx-datatable-column [resizeable]="false" name="Version" [flexGrow]="0.3"> + <ng-template ngx-datatable-cell-template let-row="row"> + <span [attr.data-tests-id]="'artifactVersion_' + row.artifactDisplayName">{{ row.artifactVersion }}</span> + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column [resizeable]="false" name="UUID" [flexGrow]="1"> + <ng-template ngx-datatable-cell-template let-row="row"> + <span sdc-tooltip [tooltip-text]="row.artifactUUID" [tooltip-placement]="3">{{ row.artifactUUID }}</span> + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column [resizeable]="false" [flexGrow]="0.6"> + <ng-template ngx-datatable-cell-template let-row="row"> + <div class="download-artifact-button"> + <svg-icon *ngIf="!row.heatParameters?.length && !state.isViewOnly" class="action-icon" [mode]="'primary2'" [name]="'edit-o'" + testId="edit_{{row.artifactDisplayName}}" clickable="true" size="medium" + (click)="addOrUpdateArtifact(row, state.isViewOnly)"></svg-icon> + <svg-icon *ngIf="row.heatParameters?.length && !state.isViewOnly" class="action-icon" [mode]="'primary2'" [name]="'indesign_status'" + testId="update_heat_params_{{row.artifactDisplayName}}" clickable="true" size="medium" + (click)="updateEnvParams(row, state.isViewOnly)"></svg-icon> + <svg-icon *ngIf="!row.isFromCsar && !state.isViewOnly" class="action-icon" [mode]="'primary2'" [name]="'trash-o'" + testId="delete_{{row.artifactDisplayName}}" clickable="true" size="medium" (click)="deleteArtifact(row)"></svg-icon> + <svg-icon *ngIf="row.isGenericBrowseable()" class="action-icon" [mode]="'primary2'" [name]="'search-o'" + testId="gab-{{row.artifactDisplayName}}" clickable="true" size="medium" (click)="openGenericArtifactBrowserModal(row)"></svg-icon> + + <!--Download--> + </div> + </ng-template> + </ngx-datatable-column> + + <ngx-datatable-footer> + <ng-template ngx-datatable-footer-template> + <div class="table-footer-container"> + <sdc-button *ngIf="!state.isViewOnly" [type]="'secondary'" + [testId]="'add_artifact_btn'" + [text]="'DEPLOYMENT_ARTIFACT_BUTTON_ADD_OTHER' | translate" + [icon_name]="'plus-circle-o'" + [icon_mode] = "'secondary'" + [icon_position]="'left'" + (click)="addOrUpdateArtifact()"> + </sdc-button> + </div> + </ng-template> + </ngx-datatable-footer> + </ngx-datatable> +</div> diff --git a/catalog-ui/src/app/ng2/pages/workspace/deployment-artifacts/deployment-artifacts-page.component.less b/catalog-ui/src/app/ng2/pages/workspace/deployment-artifacts/deployment-artifacts-page.component.less new file mode 100644 index 0000000000..22ceb96653 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/deployment-artifacts/deployment-artifacts-page.component.less @@ -0,0 +1,55 @@ +.deployment-artifact-page { + + + .env-artifact-container { + margin-left: -25px; + margin-top: -21px; + padding-left: 10px; + position: absolute; + background-color: white; + .env-artifact { + border-left: 1px #848586 solid; + height: 33px; + + border-top: 1px #848586 solid; + border-bottom: 1px #848586 solid; + width: 10px; + float: left; + + } + } + .add-artifact-btn { + display: flex; + cursor: pointer; + justify-content: flex-end; + margin-bottom: 10px; + } + .download-artifact-button { + display: flex; + justify-content: center; + + .action-icon { + margin-right: 10px; + } + } + + .table-footer-container { + display: flex; + align-items: center; + width: 100%; + justify-content: center; + margin: 20px 0px; + } +} + +:host ::ng-deep { + + .ngx-datatable { + //border: 1px solid red; + .datatable-body-cell { + .info { + float: right; + } + } + } +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/workspace/deployment-artifacts/deployment-artifacts-page.component.ts b/catalog-ui/src/app/ng2/pages/workspace/deployment-artifacts/deployment-artifacts-page.component.ts new file mode 100644 index 0000000000..53b21b34b6 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/deployment-artifacts/deployment-artifacts-page.component.ts @@ -0,0 +1,155 @@ +import { Component, OnInit, ViewChild } from '@angular/core'; +import { Select, Store } from '@ngxs/store'; +import { ArtifactModel } from 'app/models'; +import * as _ from 'lodash'; +import { SdcUiCommon, SdcUiComponents, SdcUiServices } from 'onap-ui-angular'; +import { Observable } from 'rxjs/index'; +import { map } from 'rxjs/operators'; +import { GabConfig } from '../../../../models/gab-config'; +import { PathsAndNamesDefinition } from '../../../../models/paths-and-names'; +import { GenericArtifactBrowserComponent } from '../../../../ng2/components/logic/generic-artifact-browser/generic-artifact-browser.component'; +import { ArtifactGroupType, ArtifactType } from '../../../../utils/constants'; +import { ArtifactsService } from '../../../components/forms/artifacts-form/artifacts.service'; +import { PopoverContentComponent } from '../../../components/ui/popover/popover-content.component'; +import { CacheService } from '../../../services/cache.service'; +import { TranslateService } from '../../../shared/translator/translate.service'; +import { GetArtifactsByTypeAction } from '../../../store/actions/artifacts.action'; +import { ArtifactsState } from '../../../store/states/artifacts.state'; +import { WorkspaceState, WorkspaceStateModel } from '../../../store/states/workspace.state'; +import { WorkspaceService } from '../workspace.service'; +import { ModalService } from 'app/ng2/services/modal.service'; + +export interface IPoint { + x: number; + y: number; +} + +@Component({ + selector: 'deployment-artifact-page', + templateUrl: './deployment-artifacts-page.component.html', + styleUrls: ['./deployment-artifacts-page.component.less', '../../../../../assets/styles/table-style.less'] +}) +export class DeploymentArtifactsPageComponent implements OnInit { + + public componentId: string; + public componentType: string; + public deploymentArtifacts$: Observable<ArtifactModel[]>; + public isComponentInstanceSelected: boolean; + + @Select(WorkspaceState) workspaceState$: Observable<WorkspaceStateModel>; + @ViewChild('informationArtifactsTable') table: any; + @ViewChild('popoverForm') popoverContentComponent: PopoverContentComponent; + + constructor(private workspaceService: WorkspaceService, + private artifactsService: ArtifactsService, + private store: Store, + private popoverService: SdcUiServices.PopoverService, + private cacheService: CacheService, + private modalService: SdcUiServices.ModalService, + private translateService: TranslateService) { + } + + private getEnvArtifact = (heatArtifact: ArtifactModel, artifacts: ArtifactModel[]): ArtifactModel => { + return _.find(artifacts, (item: ArtifactModel) => { + return item.generatedFromId === heatArtifact.uniqueId; + }); + }; + + // we need to sort the artifact in a way that the env artifact is always under the artifact he is connected to- this is cause of the way the ngx databale work + private sortArtifacts = ((artifacts) => { + const sortedArtifacts = []; + _.forEach(artifacts, (artifact: ArtifactModel): void => { + const envArtifact = this.getEnvArtifact(artifact, artifacts); + if (!artifact.generatedFromId) { + sortedArtifacts.push(artifact); + } + if (envArtifact) { + sortedArtifacts.push(envArtifact); + } + }); + return sortedArtifacts; + }) + + ngOnInit(): void { + this.componentId = this.workspaceService.metadata.uniqueId; + this.componentType = this.workspaceService.metadata.componentType; + + this.store.dispatch(new GetArtifactsByTypeAction({ + componentType: this.componentType, + componentId: this.componentId, + artifactType: ArtifactGroupType.DEPLOYMENT + })); + this.deploymentArtifacts$ = this.store.select(ArtifactsState.getArtifactsByType).pipe(map((filterFn) => filterFn(ArtifactType.DEPLOYMENT))).pipe(map(artifacts => { + return this.sortArtifacts(artifacts); + })); + } + + onActivate(event) { + if (event.type === 'click') { + this.table.rowDetail.toggleExpandRow(event.row); + } + } + + public addOrUpdateArtifact = (artifact: ArtifactModel, isViewOnly: boolean) => { + this.artifactsService.openArtifactModal(this.componentId, this.componentType, artifact, ArtifactGroupType.DEPLOYMENT, isViewOnly); + } + + public deleteArtifact = (artifactToDelete) => { + this.artifactsService.deleteArtifact(this.componentType, this.componentId, artifactToDelete); + } + + private openPopOver = (title: string, content: string, positionOnPage: IPoint, location: string) => { + this.popoverService.createPopOver(title, content, positionOnPage, location); + } + + public updateEnvParams = (artifact: ArtifactModel, isViewOnly: boolean) => { + this.artifactsService.openUpdateEnvParams(this.componentType, this.componentId, artifact ); + } + + private openGenericArtifactBrowserModal = (artifact: ArtifactModel): void => { + const titleStr = 'Generic Artifact Browser'; + const modalConfig = { + size: 'sdc-xl', + title: titleStr, + type: SdcUiCommon.ModalType.custom, + buttons: [{ + id: 'closeGABButton', + text: 'Close', + size: 'sm', + closeModal: true + }] as SdcUiCommon.IModalButtonComponent[] + }; + + const uiConfiguration: any = this.cacheService.get('UIConfiguration'); + let noConfig: boolean = false; + let pathsandnamesArr: PathsAndNamesDefinition[] = []; + + if (typeof uiConfiguration.gab === 'undefined') { + noConfig = true; + } else { + const gabConfig: GabConfig = uiConfiguration.gab + .find((config) => config.artifactType === artifact.artifactType); + if (typeof gabConfig === 'undefined') { + noConfig = true; + } else { + pathsandnamesArr = gabConfig.pathsAndNamesDefinitions; + } + } + + + if (noConfig) { + const msg = this.translateService.translate('DEPLOYMENT_ARTIFACT_GAB_NO_CONFIG'); + this.modalService.openAlertModal(titleStr, msg); + } + + const modalInputs = { + pathsandnames: pathsandnamesArr, + artifactid: artifact.esId, + resourceid: this.componentId + }; + + this.modalService.openCustomModal(modalConfig, GenericArtifactBrowserComponent, modalInputs); + + } + +} diff --git a/catalog-ui/src/app/ng2/pages/workspace/deployment-artifacts/deployment-artifacts-page.module.ts b/catalog-ui/src/app/ng2/pages/workspace/deployment-artifacts/deployment-artifacts-page.module.ts new file mode 100644 index 0000000000..398e9d3f4d --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/deployment-artifacts/deployment-artifacts-page.module.ts @@ -0,0 +1,35 @@ +import {CommonModule} from "@angular/common"; +import {NgModule} from "@angular/core"; +import {SdcUiComponentsModule} from "onap-ui-angular"; +import {NgxDatatableModule} from "@swimlane/ngx-datatable"; +import {UiElementsModule} from "../../../components/ui/ui-elements.module"; +import {ArtifactFormModule} from "../../../components/forms/artifacts-form/artifact-form.module"; +import {ArtifactsService} from "../../../components/forms/artifacts-form/artifacts.service"; +import {DeploymentArtifactsPageComponent} from "./deployment-artifacts-page.component"; +import {TranslatePipe} from "../../../shared/translator/translate.pipe"; +import {TranslateModule} from "../../../shared/translator/translate.module"; +import {GenericArtifactBrowserModule} from "../../../components/logic/generic-artifact-browser/generic-artifact-browser.module"; + +@NgModule({ + declarations: [ + DeploymentArtifactsPageComponent + ], + imports: [ + TranslateModule, + CommonModule, + SdcUiComponentsModule, + NgxDatatableModule, + UiElementsModule, + ArtifactFormModule, + GenericArtifactBrowserModule + ], + exports: [ + DeploymentArtifactsPageComponent + ], + entryComponents: [ + DeploymentArtifactsPageComponent + ], + providers:[ArtifactsService] +}) +export class DeploymentArtifactsPageModule { +} diff --git a/catalog-ui/src/app/ng2/pages/workspace/deployment-artifacts/deployment-artifacts-page.spec.ts b/catalog-ui/src/app/ng2/pages/workspace/deployment-artifacts/deployment-artifacts-page.spec.ts new file mode 100644 index 0000000000..056efdc5d4 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/deployment-artifacts/deployment-artifacts-page.spec.ts @@ -0,0 +1,86 @@ +// import ' rxjs/add/observable/of'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { async, ComponentFixture } from '@angular/core/testing'; +import { NgxsModule, Store } from '@ngxs/store'; +import { NgxDatatableModule } from '@swimlane/ngx-datatable'; +import { SdcUiServices } from 'onap-ui-angular'; +import { Observable } from 'rxjs/Observable'; +import { deploymentArtifactMock } from '../../../../../jest/mocks/artifacts-mock'; +import { ConfigureFn, configureTests } from '../../../../../jest/test-config.helper'; +import { ComponentMetadata } from '../../../../models/component-metadata'; +import { ArtifactsService } from '../../../components/forms/artifacts-form/artifacts.service'; +import { CacheService } from '../../../services/cache.service'; +import { TopologyTemplateService } from '../../../services/component-services/topology-template.service'; +import { TranslateModule } from '../../../shared/translator/translate.module'; +import { TranslateService } from '../../../shared/translator/translate.service'; +import { ArtifactsState } from '../../../store/states/artifacts.state'; +import { WorkspaceService } from '../workspace.service'; +import { DeploymentArtifactsPageComponent } from './deployment-artifacts-page.component'; +import {ModalService} from "../../../services/modal.service"; + +describe('deployment artifacts page', () => { + + let fixture: ComponentFixture<DeploymentArtifactsPageComponent>; + let topologyTemplateServiceMock: Partial<TopologyTemplateService>; + let workspaceServiceMock: Partial<WorkspaceService>; + let loaderServiceMock: Partial<SdcUiServices.LoaderService>; + let store: Store; + + beforeEach( + async(() => { + + topologyTemplateServiceMock = { + getArtifactsByType: jest.fn().mockImplementation((componentType, id, artifactType) => Observable.of(deploymentArtifactMock)) + }; + workspaceServiceMock = { + metadata: <ComponentMetadata>{ + uniqueId: 'service_unique_id', + componentType: 'SERVICE' + } + } + + loaderServiceMock = { + activate: jest.fn(), + deactivate: jest.fn() + } + const configure: ConfigureFn = (testBed) => { + testBed.configureTestingModule({ + declarations: [DeploymentArtifactsPageComponent], + imports: [NgxDatatableModule, TranslateModule, NgxsModule.forRoot([ArtifactsState])], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + {provide: WorkspaceService, useValue: workspaceServiceMock}, + {provide: TopologyTemplateService, useValue: topologyTemplateServiceMock}, + {provide: SdcUiServices.LoaderService, useValue: loaderServiceMock}, + {provide: ArtifactsService, useValue: {}}, + {provide: SdcUiServices.PopoverService, useValue: {}}, + {provide: CacheService, useValue: {}}, + {provide: SdcUiServices.ModalService, useValue: {}}, + {provide: ModalService, useValue: {}}, + {provide: TranslateService, useValue: {}} + ], + }); + }; + + configureTests(configure).then((testBed) => { + fixture = testBed.createComponent(DeploymentArtifactsPageComponent); + store = testBed.get(Store); + }); + }) + ); + + it('should match current snapshot of informational artifact pages component', () => { + expect(fixture).toMatchSnapshot(); + }); + + it('should see exactly 2 tosca artifacts', () => { + fixture.componentInstance.ngOnInit(); + fixture.componentInstance.deploymentArtifacts$.subscribe((artifacts) => { + expect(artifacts.length).toEqual(8); + }) + store.selectOnce((state) => state.artifacts.deploymentArtifacts).subscribe((artifacts) => { + expect(artifacts.length).toEqual(8); + }); + }); + +}); diff --git a/catalog-ui/src/app/ng2/pages/workspace/deployment/deployment-page.component.html b/catalog-ui/src/app/ng2/pages/workspace/deployment/deployment-page.component.html new file mode 100644 index 0000000000..885277217d --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/deployment/deployment-page.component.html @@ -0,0 +1,11 @@ +<div class="deployment-page"> + <deployment-graph></deployment-graph> + <panel-wrapper-component> + <sdc-tabs class="deployment-tabs" [iconsSize]="'large'" [isVertical]="true"> + <sdc-tab *ngFor="let tab of tabs" [titleIcon]="tab.titleIcon" [active]="tab.isActive" + [tooltipText]="tab.tooltipText"> + <hierarchy-tab *ngIf="isDataAvailable" [isViewOnly]="(isViewOnly$ | async)"></hierarchy-tab> + </sdc-tab> + </sdc-tabs> + </panel-wrapper-component> +</div>
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/workspace/deployment/deployment-page.component.less b/catalog-ui/src/app/ng2/pages/workspace/deployment/deployment-page.component.less new file mode 100644 index 0000000000..4b7a1e7e9f --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/deployment/deployment-page.component.less @@ -0,0 +1,24 @@ +@import './../../../../../assets/styles/variables.less'; +@import './../../../../../assets/styles/override.less'; +.deployment-page { + width: 100%; + height: 100%; + + /deep/ .sdc-tabs { + height: 100%; + } + /deep/ .sdc-tabs-list { + position: absolute; + top: 22px; + right: 303px; + background-color: @sdcui_color_silver; + + svg-icon-label { + vertical-align: middle; + } + } + /deep/ .sdc-tab-content { + height: 100%; + } +} + diff --git a/catalog-ui/src/app/ng2/pages/workspace/deployment/deployment-page.component.ts b/catalog-ui/src/app/ng2/pages/workspace/deployment/deployment-page.component.ts new file mode 100644 index 0000000000..12bd5369c7 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/deployment/deployment-page.component.ts @@ -0,0 +1,78 @@ +/*- + * ============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========================================================= + */ + +import {Component} from "@angular/core"; +import {HierarchyTabComponent} from "./panel/panel-tabs/hierarchy-tab/hierarchy-tab.component"; +import {ComponentGenericResponse} from "../../../services/responses/component-generic-response"; +import {TopologyTemplateService} from "../../../services/component-services/topology-template.service"; +import {WorkspaceService} from "../workspace.service"; +import {Module} from "app/models"; +import {SdcUiServices} from "onap-ui-angular"; +import {Select} from "@ngxs/store"; +import {WorkspaceState} from "../../../store/states/workspace.state"; +import {DeploymentGraphService} from "../../composition/deployment/deployment-graph.service"; + +const tabs = + { + hierarchyTab: { + titleIcon: 'composition-o', + component: HierarchyTabComponent, + input: {}, + isActive: true, + tooltipText: 'Hierarchy' + } + }; + +@Component({ + selector: 'deployment-page', + templateUrl: './deployment-page.component.html', + styleUrls: ['deployment-page.component.less'] +}) + +export class DeploymentPageComponent { + public tabs: Array<any>; + public resourceType: string; + public modules: Array<Module>; + public isDataAvailable: boolean; + + @Select(WorkspaceState.isViewOnly) isViewOnly$: boolean; + + constructor(private topologyTemplateService: TopologyTemplateService, + private workspaceService: WorkspaceService, + private deploymentService: DeploymentGraphService, + private loaderService: SdcUiServices.LoaderService) { + this.tabs = []; + this.isDataAvailable = false; + } + + ngOnInit(): void { + this.topologyTemplateService.getDeploymentGraphData(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId).subscribe((response: ComponentGenericResponse) => { + this.deploymentService.componentInstances = response.componentInstances; + this.deploymentService.componentInstancesRelations = response.componentInstancesRelations; + this.deploymentService.modules = response.modules; + this.isDataAvailable = true; + this.loaderService.deactivate(); + }); + + this.loaderService.activate(); + this.resourceType = this.workspaceService.getMetadataType(); + this.tabs.push(tabs.hierarchyTab); + } +} diff --git a/catalog-ui/src/app/ng2/pages/workspace/deployment/deployment-page.module.ts b/catalog-ui/src/app/ng2/pages/workspace/deployment/deployment-page.module.ts new file mode 100644 index 0000000000..3635e8f2cf --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/deployment/deployment-page.module.ts @@ -0,0 +1,30 @@ +/** + * Created by ob0695 on 6/4/2018. + */ +import {NgModule} from "@angular/core"; +import {CommonModule} from "@angular/common"; +import {DeploymentPageComponent} from "./deployment-page.component"; +import {SdcUiComponentsModule} from "onap-ui-angular"; +import {UiElementsModule} from "../../../components/ui/ui-elements.module"; +import {TranslateModule} from "../../../shared/translator/translate.module"; +import {GlobalPipesModule} from "../../../pipes/global-pipes.module"; +import {HierarchyTabModule} from "./panel/panel-tabs/hierarchy-tab/hierarchy-tab.module"; +import {DeploymentGraphService} from "../../composition/deployment/deployment-graph.service"; +import {DeploymentGraphModule} from "../../composition/deployment/deployment-graph.module"; + +@NgModule({ + declarations: [DeploymentPageComponent], + imports: [CommonModule, + DeploymentGraphModule, + SdcUiComponentsModule, + UiElementsModule, + TranslateModule, + GlobalPipesModule, + HierarchyTabModule + ], + exports: [DeploymentPageComponent], + entryComponents: [DeploymentPageComponent], + providers: [DeploymentGraphService] +}) +export class DeploymentPageModule { +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/edit-module-name/edit-module-name.component.html b/catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/edit-module-name/edit-module-name.component.html new file mode 100644 index 0000000000..d5b9d9e9b2 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/edit-module-name/edit-module-name.component.html @@ -0,0 +1,29 @@ +<div class="edit-module-name"> + <div class="edit-module-name-label vfInstance-name" data-tests-id="popover-vfinstance-name" sdc-tooltip [tooltip-text]="selectModule.vfInstanceName">{{selectModule.vfInstanceName}}</div> + <div class="edit-module-name-heatName"> + <sdc-input #heatName [maxLength]="50" + [(value)]="selectModule.heatName" + [testId]="'popover-heat-name'" + [placeHolder]="'Enter Name'"> + </sdc-input> + <sdc-validation [validateElement]="heatName"> + <sdc-regex-validator [message]="'Special characters not allowed.'" [pattern]="pattern"></sdc-regex-validator> + </sdc-validation> + </div> + <div class="edit-module-name-label module-name" data-tests-id="'popover-module-name'" sdc-tooltip [tooltip-text]="selectModule.moduleName">{{selectModule.moduleName}}</div> + <sdc-button class="edit-module-name-btn cancel-button" + [text]="'Cancel'" + [testId]="'popover-close-button'" + [type]="'primary'" + [size] = "'small'" + (click)="clickButton(false)"> + </sdc-button> + <sdc-button class="edit-module-name-btn save-button" + [text]="'Save'" + [testId]="'popover-save-button'" + [type]="'primary'" + [size] = "'small'" + (click)="clickButton(true)" + [disabled]="selectModule.heatName == originalName"> + </sdc-button> +</div>
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/edit-module-name/edit-module-name.component.less b/catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/edit-module-name/edit-module-name.component.less new file mode 100644 index 0000000000..721ad53bc3 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/edit-module-name/edit-module-name.component.less @@ -0,0 +1,20 @@ +.edit-module-name-btn{ + float:right; + margin-left: 10px; + margin-bottom: 20px; +} +.save-button { + margin-left: 30px; +} +.cancel-button { + margin-left: 20px; +} +.edit-module-name-heatName { + margin-bottom: 15px; +} +.edit-module-name-label { + text-overflow: ellipsis; + display: block; + white-space: nowrap; + margin-bottom: 10px; +} diff --git a/catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/edit-module-name/edit-module-name.component.ts b/catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/edit-module-name/edit-module-name.component.ts new file mode 100644 index 0000000000..819182c75f --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/edit-module-name/edit-module-name.component.ts @@ -0,0 +1,24 @@ +import { Component, Input, Output, OnInit } from "@angular/core"; +import { EventEmitter } from "@angular/core"; +import { DisplayModule } from "../../../../../../../models/modules/base-module"; +import { ValidationConfiguration } from "../../../../../../../models/validation-config"; + +@Component({ + selector: 'edit-module-name', + templateUrl: './edit-module-name.component.html', + styleUrls: ['edit-module-name.component.less'] +}) +export class EditModuleName implements OnInit{ + @Input() selectModule:DisplayModule; + @Output() clickButtonEvent: EventEmitter<String> = new EventEmitter(); + private pattern = ValidationConfiguration.validation.validationPatterns.stringOrEmpty; + private originalName: string; + constructor(){} + public ngOnInit(): void { + this.originalName = this.selectModule.heatName; + } + + private clickButton(saveOrCancel: boolean) : void { + this.clickButtonEvent.emit(saveOrCancel ? this.selectModule.heatName : null); + } +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/hierarchy-tab/hierarchy-tab.component.html b/catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/hierarchy-tab/hierarchy-tab.component.html new file mode 100644 index 0000000000..7c0e60b814 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/hierarchy-tab/hierarchy-tab.component.html @@ -0,0 +1,119 @@ +<div class="sdc-hierarchy-tab" ng-class=""> + <sdc-loader [global]="false" [testId]="'hierarchy-tab-loader'" [active]="isLoading" [relative]="true" [size]="'medium'"></sdc-loader> + <div class="sdc-hierarchy-tab-title" + [attr.data-tests-id]="'tab-header'">{{'DEPLOYMENT_TAB_TITLE' | translate }}</div> + <div [ngClass]="{'scroll-module-list': selectedModule}"> + <div *ngIf="topologyTemplateType != 'SERVICE'; else isService" class="modules-list"> + <div> + <div class="sdc-hierarchy-tab-sub-title" data-tests-id="tab-sub-header">{{topologyTemplateName}}</div> + <div *ngFor="let module of modules; index as i"> + <sdc-accordion [title]="module.name" [arrow-direction]="'left'" + [css-class]="'expand-collapse-container'" + [ngClass]="{'selected': selectedModule !== undefined && selectedModule.uniqueId === module.uniqueId}" + [testId]="'hierarchy-module-' + i + '-title'" tooltip="{{module.name}}" + (click)="onModuleSelected(module)"> + <div *ngFor="let memberId of getKeys(module.members)"> + <div class="expand-collapse-sub-title" tooltip="{{memberId}}">{{memberId}}</div> + </div> + </sdc-accordion> + </div> + </div> + </div> + + <ng-template #isService> + <div class="module-list"> + <div *ngFor="let instance of componentInstances; index as instanceIndex"> + <sdc-accordion [title]="instance.name" [arrow-direction]="'left'" + [css-class]="'expand-collapse-container outer-container'" + [testId]="'hierarchy-instance-' + instanceIndex + '-title'" + tooltip="{{instance.name}}"> + <div *ngFor="let module of instance.groupInstances; index as moduleIndex"> + <sdc-accordion [title]="module.name" [arrow-direction]="'left'" + [css-class]="'expand-collapse-container inner-container'" + [ngClass]="{'selected': selectedModule && selectedModule.groupInstanceUniqueId === module.uniqueId}" + [testId]="'hierarchy-module-' + moduleIndex + '-title'" + tooltip="{{module.uniqueId}}" + (click)="onModuleSelected(module, instance.uniqueId)"> + <div *ngFor="let memberId of getKeys(module.members)"> + <div class="expand-collapse-sub-title" tooltip="{{memberId}}">{{memberId}}</div> + </div> + </sdc-accordion> + </div> + </sdc-accordion> + </div> + </div> + </ng-template> + + <!--TODO: Add Resizable--> + <div *ngIf="selectedModule"class="module-data-container" [attr.data-tests-id]="'selected-module-data'"> + <div class="module-data"> + <div class="module-name-container"> + <div class="module-name module-text-overflow" [attr.data-tests-id]="'selected-module-name'" + tooltip="{{selectedModule.name}}">{{selectedModule.name}}</div> + <div class="edit-name-container" *ngIf="topologyTemplateType != 'SERVICE'"> + <svg-icon name="edit-o" [size]="'medium'" [ngClass]="{'hand-pointer': !isViewOnly}" (click)="openEditModuleNamePopup($event)"></svg-icon> + </div> + </div> + <div [attr.data-tests-id]="'selected-module-group-uuid'" tooltip="{{selectedModule.groupUUID}}"> + <div class="selected-module-property-header">Module ID:</div> + <div class="selected-module-property-value small-font">{{selectedModule.groupUUID}}</div> + </div> + <div [attr.data-tests-id]="'selected-module-group-customization-uuid'" + *ngIf="topologyTemplateType == 'SERVICE' && isViewOnly" + tooltip="{{selectedModule.customizationUUID}}"> + <div class="selected-module-property-header">Customization ID:</div> + <div class="selected-module-property-value small-font">{{selectedModule.customizationUUID}}</div> + </div> + <div [attr.data-tests-id]="'selected-module-group-invariant-uuid'" + tooltip="{{selectedModule.invariantUUID}}"> + <div class="selected-module-property-header">Invariant UUID:</div> + <div class="selected-module-property-value small-font">{{selectedModule.invariantUUID}}</div> + </div> + <div [attr.data-tests-id]="'selected-module-version'" class="selected-module-property-container"> + <div class="selected-module-property-header">Version:</div> + <div class="selected-module-property-value same-row">{{selectedModule.version}}</div> + </div> + <div data-tests-id="selected-module-is-base" class="selected-module-property-container"> + <div class="selected-module-property-header">IsBase:</div> + <div class="selected-module-property-value same-row">{{selectedModule.isBase}}</div> + </div> + + </div> + <sdc-accordion [title]="'Properties'" [arrow-direction]="'right'" + [css-class]="'expand-collapse-module-data-container'"> + <div *ngFor="let property of selectedModule.properties | orderBy:['name']:['asc']"> + <div class="module-data-list-item"> + <div class="module-data-list-item-value property-name" + [attr.data-tests-id]="'selected-module-property-name'"> + <span tooltip="{{property.name}}" [ngClass]="{'hand-pointer': !isViewOnly}" + (click)="!isViewOnly && openEditPropertyModal(property)">{{property.name}}</span> + </div> + <div class="module-data-list-item-value property-info" + [attr.data-tests-id]="'selected-module-property-type'"> Type: {{property.type}}</div> + <div class="module-data-list-item-value property-info" + [attr.data-tests-id]="'selected-module-property-schema-type'"> + Value: {{property.value}}</div> + </div> + </div> + </sdc-accordion> + <sdc-accordion [title]="'Artifacts'" [arrow-direction]="'right'" + [css-class]="'expand-collapse-module-data-container'"> + <div *ngFor="let artifact of selectedModule.artifacts | orderBy:['artifactName']:['asc']"> + <div class="module-data-list-item"> + <div class="artifact-list-item"> + <div class="module-data-list-item-value" + [attr.data-tests-id]="'selected-module-artifact-name'" + tooltip="{{artifact.artifactName}}">{{artifact.artifactName}}</div> + <div class="module-data-list-item-value artifact-info" + [attr.data-tests-id]="'selected-module-artifact-uuid'" + tooltip="{{artifact.artifactUUID}}">UUID: {{artifact.artifactUUID}}</div> + <div class="module-data-list-item-value artifact-info" + [attr.data-tests-id]="'selected-module-artifact-version'"> + Version: {{artifact.artifactVersion}}</div> + </div> + </div> + </div> + </sdc-accordion> + </div> + </div> +</div> diff --git a/catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/hierarchy-tab/hierarchy-tab.component.less b/catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/hierarchy-tab/hierarchy-tab.component.less new file mode 100644 index 0000000000..269ca0aee0 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/hierarchy-tab/hierarchy-tab.component.less @@ -0,0 +1,222 @@ +@import './../../../../../../../../assets/styles/variables.less'; +.sdc-hierarchy-tab { + padding: 15px 0 0 0; + background-color: #f8f8f8; + height: 100%; + box-shadow: 0.3px 1px 3px rgba(24, 24, 25, 0.42); + display: flex; + flex-flow: column; + + .sdc-hierarchy-tab-title { + color: @main_color_a; + padding: 0 0 15px 20px; + border-bottom: 1px solid #d2d2d2; + } + + .sdc-hierarchy-tab-sub-title { + color: @main_color_a; + padding: 15px 20px 15px 20px; + } + + .scroll-module-list { + overflow-y: auto; + display: flex; + height: 100%; + flex-direction: column; + } + + /deep/ .expand-collapse-container { + margin-bottom: 0; + + .sdc-accordion-header { + white-space: nowrap; + line-height: 22px; + background-color: @tlv_color_u; + padding: 8px 20px 8px 8px; + box-shadow: inset 0px -1px 0px 0px rgba(255, 255, 255, 0.7); + height: 40px; + + .title { + overflow: hidden; + text-overflow: ellipsis; + max-width: 215px; + } + } + + .sdc-accordion-body.open { + padding: 0 0 5px 0; + } + + .sdc-accordion-header:hover { + background-color: @main_color_o; + } + + &.outer-container { + .sdc-accordion-body { + padding-left: 0; + } + } + + &.inner-container { + margin-bottom: 0; + + .sdc-accordion-header { + padding: 8px 20px 8px 30px; + background-color: @tlv_color_t + } + } + } + + sdc-accordion.selected { + /deep/ .expand-collapse-container { + .sdc-accordion-header { + background-color: @main_color_a; + color: @main_color_p; + + .svg-icon { + fill: @main_color_p; + } + } + } + } + + .expand-collapse-sub-title { + max-width: 225px; + display: inline-block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + padding: 10px 0 0 33px; + } + + .expand-collapse-content { + .expand-collapse-title { + padding: 0 10px 0 30px; + } + } + + .module-data-container { + width: 100%; + overflow-y: overlay; + background-color: @tlv_color_v; + border: 1px solid @main_color_a; + border-top: 4px solid @main_color_a; + box-shadow: 0.3px 1px 2px rgba(24, 24, 25, 0.32); + .module-data { + color: @main_color_a; + padding: 10px 0 10px 0; + margin: 0 20px 0 20px; + + .selected-module-property-header { + font-weight: bold; + } + + .selected-module-property-value { + font-family: @font-opensans-regular; + + &.small-font { + font-size: 12px; + } + } + + .module-name-container { + + display: flex; + flex-direction: row; + + .module-name { + font-size: 14px; + width: 75%; + max-width: 240px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .edit-name-container { + float: right; + border-left: 1px solid @main_color_a; + height: 20px; + padding-left: 12px; + + svg-icon { + padding-top: 3px; + fill: @main_color_s; + + &.hand-pointer { + cursor: pointer; + } + + } + } + } + } + + .selected-module-property-container { + flex-direction: row; + display: flex; + + .selected-module-property-value { + text-indent: 2px; + } + } + + /deep/ .expand-collapse-module-data-container { + margin-bottom: 0; + + .sdc-accordion-header { + white-space: nowrap; + line-height: 22px; + padding: 8px 20px 8px 16px; + height: 40px; + background-color: @tlv_color_w; + color: @main_color_l; + border-top: 1px solid @main_color_a; + border-bottom: 1px solid @main_color_a; + width: 100%; + } + + } + + .module-data-list-item { + padding-bottom: 10px; + margin: 0 20px 0 20px; + + .artifact-list-item { + color: @main_color_m; + } + + .module-data-list-item-value { + width: 100%; + max-width: 240px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + &.artifact-info { + font-family: @font-opensans-regular; + font-size: 12px; + } + + &.property-name { + font-weight: 400; + color: @main_color_a; + + .hand-pointer { + cursor: pointer; + } + } + + &.property-info { + color: @func_color_s; + font-family: @font-opensans-regular; + } + } + } + } +} + +.modules-list { + overflow-y: overlay; + flex-grow: 1; +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/hierarchy-tab/hierarchy-tab.component.spec.ts b/catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/hierarchy-tab/hierarchy-tab.component.spec.ts new file mode 100644 index 0000000000..ab88867cc0 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/hierarchy-tab/hierarchy-tab.component.spec.ts @@ -0,0 +1,133 @@ +import {async, ComponentFixture} from "@angular/core/testing"; +import {HierarchyTabComponent} from "./hierarchy-tab.component"; +import {ConfigureFn, configureTests} from "../../../../../../../../jest/test-config.helper"; +import {NO_ERRORS_SCHEMA} from "@angular/core"; +import {TranslateModule} from "../../../../../../shared/translator/translate.module"; +import {TopologyTemplateService} from "../../../../../../services/component-services/topology-template.service"; +import {WorkspaceService} from "../../../../workspace.service"; +import {ModulesService} from "../../../../../../services/modules.service"; +import {GlobalPipesModule} from "../../../../../../pipes/global-pipes.module"; +import {TranslateService} from "../../../../../../shared/translator/translate.service"; +import {ModalsHandler} from "../../../../../../../utils/modals-handler"; +import {ComponentFactory} from "../../../../../../../utils/component-factory"; +import {NgxsModule} from "@ngxs/store"; +import { SdcUiServices } from "onap-ui-angular"; +import {Observable} from "rxjs"; +import {DisplayModule, Module} from "../../../../../../../models/modules/base-module"; +import {DeploymentGraphService} from "../../../../../composition/deployment/deployment-graph.service"; +import {ComponentMetadata} from "../../../../../../../models/component-metadata"; + +describe('HierarchyTabComponent', () => { + + let fixture: ComponentFixture<HierarchyTabComponent>; + let workspaceService: Partial<WorkspaceService>; + let popoverServiceMock: Partial<SdcUiServices.PopoverService>; + let modulesServiceMock: Partial<ModulesService>; + + let editModuleNameInstanceMock = {innerPopoverContent:{instance: { clickButtonEvent: Observable.of("new heat name")}}, + closePopover: jest.fn()}; + let eventMock = {x: 1650, y: 350}; + let moduleMock: Array<Module> = [{name: "NewVf2..base_vepdg..module-0", uniqueId: '1'}]; + let selectedModuleMock: DisplayModule = {name: "NewVf2..base_vepdg..module-0", vfInstanceName: "NewVf2", moduleName:"module-0", + heatName: "base_vepdg", uniqueId: '1', updateName: jest.fn().mockImplementation(() => { + selectedModuleMock.name = selectedModuleMock.vfInstanceName + '..' + selectedModuleMock.heatName + '..' + + selectedModuleMock.moduleName;})} + let updateSelectedModuleMock = () => { + selectedModuleMock.heatName = "base_vepdg"; + selectedModuleMock.name = "NewVf2..base_vepdg..module-0"; + fixture.componentInstance.selectedModule = selectedModuleMock; + fixture.componentInstance.modules = moduleMock; + } + beforeEach( + async(() => { + + workspaceService ={ + metadata: <ComponentMetadata> { + name: '', + componentType: '' + } + } + popoverServiceMock = { + createPopOverWithInnerComponent: jest.fn().mockImplementation(() => {return editModuleNameInstanceMock}) + } + modulesServiceMock = { + updateModuleMetadata: jest.fn().mockReturnValue(Observable.of({})) + } + + const configure: ConfigureFn = testBed => { + testBed.configureTestingModule({ + declarations: [HierarchyTabComponent], + schemas: [NO_ERRORS_SCHEMA], + imports: [TranslateModule, NgxsModule.forRoot([]), GlobalPipesModule], + providers: [ + {provide: DeploymentGraphService, useValue: {}}, + {provide: ComponentFactory, useValue: {}}, + {provide: TopologyTemplateService, useValue: {}}, + {provide: WorkspaceService, useValue: workspaceService}, + {provide: ModulesService, useValue: modulesServiceMock}, + {provide: TranslateService, useValue: {}}, + {provide: ModalsHandler, useValue: {}}, + {provide: SdcUiServices.PopoverService, useValue: popoverServiceMock} + ] + }); + }; + + configureTests(configure).then(testBed => { + fixture = testBed.createComponent(HierarchyTabComponent); + }); + }) + ); + + it('expected heirarchy component to be defined', () => { + expect(fixture).toBeDefined(); + }); + + it('Update heat name and name sucessfully', () => { + updateSelectedModuleMock(); + fixture.componentInstance.openEditModuleNamePopup(eventMock); + expect(fixture.componentInstance.selectedModule.updateName).toHaveBeenCalled(); + expect(modulesServiceMock.updateModuleMetadata).toHaveBeenCalled(); + expect(fixture.componentInstance.selectedModule.name).toEqual('NewVf2..new heat name..module-0'); + expect(fixture.componentInstance.modules[0].name).toEqual('NewVf2..new heat name..module-0'); + expect(fixture.componentInstance.selectedModule.heatName).toEqual('new heat name'); + }) + it('Try to update heat name and name and get error from server', () => { + updateSelectedModuleMock(); + modulesServiceMock.updateModuleMetadata.mockImplementation(() => Observable.throwError({})); + fixture.componentInstance.openEditModuleNamePopup(eventMock); + expect(fixture.componentInstance.selectedModule.updateName).toHaveBeenCalled(); + expect(modulesServiceMock.updateModuleMetadata).toHaveBeenCalled(); + expect(fixture.componentInstance.modules[0].name).toEqual('NewVf2..base_vepdg..module-0'); + expect(fixture.componentInstance.selectedModule.heatName).toEqual('base_vepdg'); + expect(fixture.componentInstance.selectedModule.name).toEqual('NewVf2..base_vepdg..module-0'); + }) + it('Try to update heat name and name but not find the module with the same uniqueId', () => { + selectedModuleMock.uniqueId = '2' + updateSelectedModuleMock(); + fixture.componentInstance.openEditModuleNamePopup(eventMock); + expect(fixture.componentInstance.selectedModule.updateName).toHaveBeenCalled(); + expect(modulesServiceMock.updateModuleMetadata).not.toHaveBeenCalled(); + expect(fixture.componentInstance.modules[0].name).toEqual('NewVf2..base_vepdg..module-0'); + expect(fixture.componentInstance.selectedModule.heatName).toEqual('base_vepdg'); + expect(fixture.componentInstance.selectedModule.name).toEqual('NewVf2..base_vepdg..module-0'); + selectedModuleMock.uniqueId = '1' + }) + it('Open edit module name popover and change the heat name', () => { + updateSelectedModuleMock(); + spyOn(fixture.componentInstance, 'updateHeatName'); + spyOn(fixture.componentInstance, 'updateOriginalHeatName'); + fixture.componentInstance.openEditModuleNamePopup(eventMock); + expect(popoverServiceMock.createPopOverWithInnerComponent).toHaveBeenCalled(); + expect(fixture.componentInstance.selectedModule.heatName).toEqual("new heat name"); + expect(fixture.componentInstance.updateHeatName).toHaveBeenCalled(); + }) + + + it('Open edit module name popover and not change the heat name', () => { + updateSelectedModuleMock(); + editModuleNameInstanceMock.innerPopoverContent.instance.clickButtonEvent = Observable.of(null); + fixture.componentInstance.openEditModuleNamePopup(eventMock); + expect(popoverServiceMock.createPopOverWithInnerComponent).toHaveBeenCalled(); + expect(fixture.componentInstance.selectedModule.heatName).toEqual("base_vepdg"); + }) +});
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/hierarchy-tab/hierarchy-tab.component.ts b/catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/hierarchy-tab/hierarchy-tab.component.ts new file mode 100644 index 0000000000..604b194283 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/hierarchy-tab/hierarchy-tab.component.ts @@ -0,0 +1,139 @@ +import {Component, Input} from "@angular/core"; +import {Component as TopologyTemplate, ComponentInstance, DisplayModule, Module, PropertyModel} from "app/models"; +import {TranslateService} from "app/ng2/shared/translator/translate.service"; +import {ComponentType} from "app/utils/constants"; +import {WorkspaceService} from "../../../../workspace.service"; +import {ModulesService} from "../../../../../../services/modules.service"; +import * as _ from "lodash"; +import {ModalsHandler} from "../../../../../../../utils/modals-handler"; +import {ComponentFactory} from "../../../../../../../utils/component-factory"; +import {Select, Store} from "@ngxs/store"; +import { SdcUiServices } from "onap-ui-angular"; +import { EditModuleName } from "../edit-module-name/edit-module-name.component"; +import {GraphState} from "../../../../../composition/common/store/graph.state"; +import {DeploymentGraphService} from "../../../../../composition/deployment/deployment-graph.service"; +import {OnSidebarOpenOrCloseAction} from "../../../../../composition/common/store/graph.actions"; + +@ Component({ + selector: 'hierarchy-tab', + templateUrl: './hierarchy-tab.component.html', + styleUrls: ['./hierarchy-tab.component.less'], +}) +export class HierarchyTabComponent { + + @Select(GraphState.withSidebar) withSidebar$: boolean; + @Input() isViewOnly: boolean; + public selectedIndex: number; + public selectedModule: DisplayModule; + public isLoading: boolean; + public topologyTemplateName: string; + public topologyTemplateType: string; + public modules: Array<Module> = []; + public componentInstances: Array<ComponentInstance> = []; + private editPropertyModalTopologyTemplate: TopologyTemplate; + + constructor(private translateService: TranslateService, + private workspaceService: WorkspaceService, + private deploymentService: DeploymentGraphService, + private modulesService: ModulesService, + private ModalsHandler: ModalsHandler, + private componentFactory: ComponentFactory, + private store: Store, + private popoverService: SdcUiServices.PopoverService) { + this.isLoading = false; + this.topologyTemplateName = this.workspaceService.metadata.name; + this.topologyTemplateType = this.workspaceService.metadata.componentType; + } + + ngOnInit() { + this.modules = this.deploymentService.modules; + this.componentInstances = this.deploymentService.componentInstances; + this.editPropertyModalTopologyTemplate = this.componentFactory.createEmptyComponent(this.topologyTemplateType); + this.editPropertyModalTopologyTemplate.componentInstances = this.deploymentService.componentInstances; + } + + onModuleSelected(module: Module, componentInstanceId?: string): void { + + let onSuccess = (module: DisplayModule) => { + console.log("Module Loaded: ", module); + this.selectedModule = module; + this.isLoading = false; + }; + + let onFailed = () => { + this.isLoading = false; + }; + + if (!this.selectedModule || (this.selectedModule && this.selectedModule.uniqueId != module.uniqueId)) { + this.isLoading = true; + if (this.topologyTemplateType == ComponentType.SERVICE) { + // this.selectedInstanceId = componentInstanceId; + this.modulesService.getComponentInstanceModule(this.topologyTemplateType, this.workspaceService.metadata.uniqueId, componentInstanceId, module.uniqueId).subscribe((resultModule: DisplayModule) => { + onSuccess(resultModule); + }, () => { + onFailed(); + }); + } else { + this.modulesService.getModuleForDisplay(this.topologyTemplateType, this.workspaceService.metadata.uniqueId, module.uniqueId).subscribe((resultModule: DisplayModule) => { + onSuccess(resultModule); + }, () => { + onFailed(); + }); + } + } + } + + updateHeatName(): void { + this.isLoading = true; + let originalName: string = this.selectedModule.name; + + this.selectedModule.updateName(); + let moduleIndex: number = _.indexOf(this.modules, _.find(this.modules, (module: Module) => { + return module.uniqueId === this.selectedModule.uniqueId; + })); + + if (moduleIndex !== -1) { + this.modules[moduleIndex].name = this.selectedModule.name; + this.modulesService.updateModuleMetadata(this.topologyTemplateType, this.workspaceService.metadata.uniqueId, this.modules[moduleIndex]).subscribe(() => { + this.isLoading = false; + }, () => { + this.updateOriginalHeatName(originalName, moduleIndex); + this.modules[moduleIndex].name = originalName; + }); + } else { + this.updateOriginalHeatName(originalName, moduleIndex); + } + }; + + private updateOriginalHeatName(originalName: string, moduleIndex: number){ + this.isLoading = false; + this.selectedModule.name = originalName; + this.selectedModule.heatName = this.selectedModule.name.split('..')[1]; + } + + openEditPropertyModal(property: PropertyModel): void { + this.editPropertyModalTopologyTemplate.setComponentMetadata(this.workspaceService.metadata); + this.ModalsHandler.openEditModulePropertyModal(property, this.editPropertyModalTopologyTemplate, this.selectedModule, this.selectedModule.properties).then(() => { + }); + } + + private getKeys(map: Map<any, any>) { + return _.keys(map); + } + + private toggleSidebarDisplay = () => { + // this.withSidebar = !this.withSidebar; + this.store.dispatch(new OnSidebarOpenOrCloseAction()); + } + + public openEditModuleNamePopup($event) { + const editModuleNameInstance = this.popoverService.createPopOverWithInnerComponent('Edit Module Name', '', {x:$event.x , y:$event.y }, EditModuleName, {selectModule: _.cloneDeep(this.selectedModule)}, 'top'); + editModuleNameInstance.innerPopoverContent.instance.clickButtonEvent.subscribe((newHeatName) => { + if(newHeatName != null){ + this.selectedModule.heatName = newHeatName; + this.updateHeatName(); + } + editModuleNameInstance.closePopover(); + }) + } +} diff --git a/catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/hierarchy-tab/hierarchy-tab.module.ts b/catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/hierarchy-tab/hierarchy-tab.module.ts new file mode 100644 index 0000000000..048ca0c65f --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/deployment/panel/panel-tabs/hierarchy-tab/hierarchy-tab.module.ts @@ -0,0 +1,24 @@ +/** + * Created by ob0695 on 6/4/2018. + */ +import {NgModule} from "@angular/core"; +import {SdcUiComponentsModule} from "onap-ui-angular"; +import {HierarchyTabComponent} from "./hierarchy-tab.component"; +import {UiElementsModule} from "../../../../../../components/ui/ui-elements.module"; +import {TranslateModule} from "../../../../../../shared/translator/translate.module"; +import {CommonModule} from "@angular/common"; +import {GlobalPipesModule} from "../../../../../../pipes/global-pipes.module"; +import { EditModuleName } from "../edit-module-name/edit-module-name.component"; + +@NgModule({ + declarations: [HierarchyTabComponent, EditModuleName], + imports: [CommonModule, + UiElementsModule, + SdcUiComponentsModule, + TranslateModule, + GlobalPipesModule], + entryComponents: [HierarchyTabComponent, EditModuleName], + exports: [HierarchyTabComponent, EditModuleName], +}) +export class HierarchyTabModule { +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-artifact-table/distribution-component-artifact-table.component.html b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-artifact-table/distribution-component-artifact-table.component.html new file mode 100644 index 0000000000..574f2d1bb4 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-artifact-table/distribution-component-artifact-table.component.html @@ -0,0 +1,62 @@ +<div class="status-page"> + <ngx-datatable + class="material" + [columnMode]="'standard'" + [rowHeight]="'auto'" + [reorderable]="false" + [swapColumns]="false" + [rows]="artifacts" + [scrollbarH]="true" + #statusTable> + <ngx-datatable-row-detail [rowHeight]="'auto'"> + <ng-template let-row="row" let-expanded="expanded" ngx-datatable-row-detail-template> + <div *ngFor="let status of row.statuses"> + <span class = "status" [attr.data-tests-id]="generateDataTestID('statusTimeStamp_',componentName, row.name, status.status)">{{ status.timeStamp | date:'short':'UTC'}}</span> + <span class = "status" [attr.data-tests-id]="generateDataTestID('statusValue_',componentName, row.name, status.status)">{{ status.status }}</span> + </div> + </ng-template> + </ngx-datatable-row-detail> + <ngx-datatable-column name="Component ID" [resizeable]="false" [width]="250"> + <ng-template ngx-datatable-cell-template let-row="row" let-expanded="expanded" > + <div> + <span class="urlValue"> + <svg-icon [clickable]="true" class="expand-collapse-icon" + [name]="expanded ? 'caret1-up-o': 'caret1-down-o'" [mode]="'primary'" + [size]="'medium'" (click)="expandRow(row)" [attr.data-tests-id]="generateDataTestID('expandIcon_compID_', componentName, row.name)"></svg-icon> + </span> + <span class="urlValue ellipsisCell" [attr.data-tests-id]="generateDataTestID('compID_',componentName, row.name)" sdc-tooltip [tooltip-placement]="3" [tooltip-text]="componentName"> + {{ componentName }} + </span> + </div> + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column [resizeable]="false" [width]="280" name="Artifact Name"> + <ng-template ngx-datatable-cell-template let-row="row"> + <div class = "distributionRowValue ellipsisCell" [attr.data-tests-id]="generateDataTestID('artName_',componentName, row.name)" sdc-tooltip [tooltip-placement]="3" [tooltip-text]="row.name">{{ row.name }}</div> + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column [resizeable]="false" [width]="380" name="URL"> + <ng-template ngx-datatable-cell-template let-row="row"> + <div> + <span class="urlValue ellipsisCell" id="urlCell" [attr.data-tests-id]="generateDataTestID('url_',componentName, row.name)">{{ row.url }}</span> + <span class="urlCopyIcon" title="Copy URL"> + <svg-icon-label [clickable]="true" [mode]="'primary'" [labelPlacement]="'right'" + [label]="" [name]="'copy-o'" [testId]="'copyToClipboard'" + (click)="copyToClipboard(row.url)"> + </svg-icon-label> + </span> + </div> + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column [resizeable]="false" [width]="180" name="Time(UTC)"> + <ng-template ngx-datatable-cell-template let-row="row"> + <div class = "distributionRowValue ellipsisCell" [attr.data-tests-id]="generateDataTestID('time_',componentName, row.name)" sdc-tooltip [tooltip-placement]="3" [tooltip-text]="getLatestArtifact(row.name).timeStamp | date:'short':'UTC'">{{ getLatestArtifact(row.name).timeStamp | date:'short':'UTC'}}</div> + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column [resizeable]="false" [width]="280" name="Status"> + <ng-template ngx-datatable-cell-template let-row="row"> + <div class = "distributionRowValue ellipsisCell" [attr.data-tests-id]="generateDataTestID('status_',componentName, row.name)" sdc-tooltip [tooltip-placement]="3" [tooltip-text]="getLatestArtifact(row.name).status">{{ getLatestArtifact(row.name).status }}</div> + </ng-template> + </ngx-datatable-column> + </ngx-datatable> +</div>
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-artifact-table/distribution-component-artifact-table.component.less b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-artifact-table/distribution-component-artifact-table.component.less new file mode 100644 index 0000000000..81b8805792 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-artifact-table/distribution-component-artifact-table.component.less @@ -0,0 +1,78 @@ +:host ::ng-deep { + .ngx-datatable { + > div { + min-height: 5px; + } + } +} + +.datatable-header-cell { + text-align: left; + color: red; +} + +.statusHeaderTable { + color: #000000; + font-family: OpenSans-Bold, sans-serif; + font-size: 12px; + font-weight: bold; + float: left; +} + +.status { + padding-right: 30px; + color: #5a5a5a; + font-family: OpenSans-Regular, sans-serif; + font-size: 12px; +} + +.distributionIDBlock { + display: inline-block; +} + +.distributionRowContainer{ + background-color: #eaeaea; + text-align: center; +} + +.distributionRowLabel { + overflow: hidden; + padding-top: 10px; + color: #000000; + font-family: OpenSans-Semibold, sans-serif; + font-size: 12px; + font-weight: bold; +} + +.distributionRowValue { + color: #263d4d; + font-family: OpenSans-Regular, sans-serif; + font-size: 14px; +} + +.urlValue { + float: left; + color: #263d4d; + font-family: OpenSans-Regular, sans-serif; + font-size: 14px; +} + +.urlCopyIcon { + float: right; + width: 8%; +} + +.ellipsisCell { + width: 92%; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} + + + + + + + + diff --git a/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-artifact-table/distribution-component-artifact-table.component.spec.ts b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-artifact-table/distribution-component-artifact-table.component.spec.ts new file mode 100644 index 0000000000..72b930b6b8 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-artifact-table/distribution-component-artifact-table.component.spec.ts @@ -0,0 +1,90 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture } from '@angular/core/testing'; +import { NgxDatatableModule } from '@swimlane/ngx-datatable'; +import { SdcUiServices } from 'onap-ui-angular'; +import { ConfigureFn, configureTests } from '../../../../../../../jest/test-config.helper'; +import { DistributionService } from '../../distribution.service'; +import { DistributionComponentArtifactTableComponent } from './distribution-component-artifact-table.component'; + +describe('DistributionComponentArtifactTableComponent', () => { + let fixture: ComponentFixture<DistributionComponentArtifactTableComponent>; + let distibutionServiceMock: Partial<DistributionService>; + + const mockArtifactsForDistributionAndComponentName = [ + { + name: 'Artifact1', + statuses: [ + {timeStamp: '7/25/2019 12:48AM', status: 'DEPLOY_OK'}, + {timeStamp: '7/25/2019 12:48AM', status: 'DOWNLOAD_OK'}, + {timeStamp: '7/25/2019 12:48AM', status: 'NOTIFIED'} + ], + url: 'URL1', + }, + { + name: 'Artifact2', + statuses: [ + {timeStamp: '7/26/2019 12:48AM', status: 'STATUS_TO_DISPLAY'}, + {timeStamp: '7/25/2019 12:48AM', status: 'DOWNLOAD_OK'}, + {timeStamp: '7/25/2019 12:48AM', status: 'NOTIFIED'} + ], + url: 'URL2', + }, + { + name: 'ArtifactWithNoStatuses', + url: 'URL2', + } + ]; + + beforeEach(() => { + + distibutionServiceMock = { + getArtifactstByDistributionIDAndComponentsName: jest.fn().mockReturnValue(mockArtifactsForDistributionAndComponentName), + }; + + const configure: ConfigureFn = (testBed) => { + testBed.configureTestingModule({ + declarations: [DistributionComponentArtifactTableComponent], + imports: [NgxDatatableModule], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + {provide: DistributionService, useValue: distibutionServiceMock} + ], + }); + }; + + configureTests(configure).then((testBed) => { + fixture = testBed.createComponent(DistributionComponentArtifactTableComponent); + }); + + }); + + it('Get Latest Artifact (status and timeStamp) - So the Component Table will display the last time stamp of the notification', async () => { + await fixture.componentInstance.ngOnInit(); + expect(fixture.componentInstance.getLatestArtifact('Artifact2')).toEqual({status: 'STATUS_TO_DISPLAY', timeStamp: '7/26/2019 12:48AM'}); + expect(fixture.componentInstance.getLatestArtifact('ArtifactWithNoStatuses')).toEqual(null); + }); + + it('Once the Distribution Component Artifact Table Component is created - artifacts will keep the relevant artifacts for a specific distributionID and Component Name', async () => { + await fixture.componentInstance.ngOnInit(); + // tslint:disable:no-string-literal + expect(fixture.componentInstance.artifacts.length).toBe(3); + expect(fixture.componentInstance.artifacts[0].name).toBe('Artifact1'); + expect(fixture.componentInstance.artifacts[0].url).toBe('URL1'); + expect(fixture.componentInstance.artifacts[0].statuses.length).toBe(3); + + expect(fixture.componentInstance.artifacts[1].name).toBe('Artifact2'); + }); + + it('Once the Distribution Component Artifact Table Component is created for Modal- artifacts will keep the relevant artifacts for a ' + + 'specific distributionID and Component Name filtered by Status', async () => { + fixture.componentInstance.statusFilter = 'DOWNLOAD_OK'; + await fixture.componentInstance.ngOnInit(); + expect(fixture.componentInstance.artifacts.length).toBe(3); + expect(fixture.componentInstance.artifacts[0].name).toBe('Artifact1'); + expect(fixture.componentInstance.artifacts[0].url).toBe('URL1'); + + expect(fixture.componentInstance.artifacts[0].statuses.length).toBe(1); + expect(fixture.componentInstance.artifacts[0].statuses[0]).toEqual({status: 'DOWNLOAD_OK', timeStamp: '7/25/2019 12:48AM'}); + + }); +}); diff --git a/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-artifact-table/distribution-component-artifact-table.component.ts b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-artifact-table/distribution-component-artifact-table.component.ts new file mode 100644 index 0000000000..af9aef5c64 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-artifact-table/distribution-component-artifact-table.component.ts @@ -0,0 +1,68 @@ +import { Component, Input, OnInit, ViewChild } from '@angular/core'; +import * as _ from 'lodash'; +import { DistributionService } from '../../distribution.service'; + +// tslint:disable:no-string-literal + +@Component({ + selector: 'app-distribution-component-artifact-table', + templateUrl: './distribution-component-artifact-table.component.html', + styleUrls: ['./distribution-component-artifact-table.component.less'] +}) +export class DistributionComponentArtifactTableComponent implements OnInit { + + @ViewChild('statusTable', {}) table: any; + + @Input() componentName: string; + @Input() rowDistributionID: string; + @Input() statusFilter: string; + + public artifacts = []; + + constructor(private distributionService: DistributionService) { + } + + ngOnInit() { + this.artifacts = this.distributionService.getArtifactstByDistributionIDAndComponentsName(this.rowDistributionID, this.componentName); + if (this.statusFilter) { + this.artifacts.forEach( + (artifact) => { + artifact.statuses = _.filter(artifact.statuses, {status: this.statusFilter}); + }); + } + } + + public getLatestArtifact(artifactName: string) { + const selectedArtifact = this.artifacts.filter((artifact) => artifact.name === artifactName); + if (selectedArtifact && selectedArtifact[0] && selectedArtifact[0]['statuses'] && selectedArtifact[0]['statuses'][0]) { + return selectedArtifact[0]['statuses'][0]; + } else { + return null; + } + } + + private copyToClipboard(urlToCopy: any) { + + const inputForCopyToClipboard = document.getElementById('inputForCopyToClipboard') as HTMLInputElement; + inputForCopyToClipboard.value = urlToCopy; + /* Select the text field */ + inputForCopyToClipboard.select(); + + /* Copy the text inside the text field */ + document.execCommand('copy'); + + } + + private generateDataTestID(preFix: string, componentName: string, artifactName: string, status?: string) { + if (!status) { + return preFix + componentName + '_' + artifactName; + } else { + return preFix + status + '_' + componentName + '_' + artifactName; + } + } + + private expandRow(row: any) { + this.table.rowDetail.toggleExpandRow(row); + } + +} diff --git a/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-table.component.html b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-table.component.html new file mode 100644 index 0000000000..fa5a9ad7fb --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-table.component.html @@ -0,0 +1,47 @@ +<div > + <div class="distributionSummary" *ngIf="!isModal"> + <span class= "rightVerticalSeperator titleSummaryFontSettings" data-tests-id="totalDistributionArtifactsLabel">Total Artifacts <span class="blue" data-tests-id="totalDistributionArtifactsValue">{{ getTotalArtifactsForDistributionID(rowDistributionID) }} </span></span> + <span class="blue rightVerticalSeperator" (click)="openModal(rowDistributionID,'NOTIFIED')" data-tests-id="totalDistributionNotifiedArtifactsLabel">Notified <span data-tests-id="totalDistributionNotifiedArtifactsValue">{{ getLengthArtifactsForDistributionIDByStatus(rowDistributionID, 'NOTIFIED') }}</span></span> + <span class="blue rightVerticalSeperator" (click)="openModal(rowDistributionID,'DOWNLOAD_OK')" data-tests-id="totalDistributionDownloadedArtifactsLabel">Downloaded <span data-tests-id="totalDistributionDownloadedArtifactsValue">{{ getLengthArtifactsForDistributionIDByStatus(rowDistributionID, 'DOWNLOAD_OK') }}</span></span> + <span class="blue rightVerticalSeperator" (click)="openModal(rowDistributionID,'DEPLOY_OK')" data-tests-id="totalDistributionDeployedArtifactsLabel">Deployed <span data-tests-id="totalDistributionDeployedArtifactsValue">{{ getLengthArtifactsForDistributionIDByStatus(rowDistributionID, 'DEPLOY_OK') }}</span></span> + <span class="blue rightVerticalSeperator" (click)="openModal(rowDistributionID,'NOT_NOTIFIED')" data-tests-id="totalDistributionNotNotifiedArtifactsLabel">Not Notified <span data-tests-id="totalDistributionNotNotifiedArtifactsValue">{{ getLengthArtifactsForDistributionIDByStatus(rowDistributionID, 'NOT_NOTIFIED') }}</span></span> + <span class="blue rightVerticalSeperator floatRight" (click)="openModal(rowDistributionID,'DEPLOY_ERROR')" data-tests-id="totalDistributionDeployErrorArtifactsLabel">Deploy Errors <span class="red" data-tests-id="totalDistributionDeployErrorArtifactsValue">{{ getLengthArtifactsForDistributionIDByStatus(rowDistributionID, 'DEPLOY_ERROR') }}</span></span> + <span class="blue rightVerticalSeperator floatRight" (click)="openModal(rowDistributionID,'DOWNLOAD_ERROR ')" data-tests-id="totalDistributionDownloadErrorArtifactsLabel">Download Errors <span class="red" data-tests-id="totalDistributionDownloadErrorArtifactsValue">{{ getLengthArtifactsForDistributionIDByStatus(rowDistributionID, 'DOWNLOAD_ERROR') }}</span></span> + </div> + + <div class="distributionSummary" *ngIf="isModal"> + <span data-tests-id="modalStatusLabel"><a>Status {{ statusFilter }} <span class="blue" data-tests-id="statusValue">{{ getLengthArtifactsForDistributionIDByStatus(rowDistributionID, statusFilter) }}</span></a></span> + </div> + + + <div class="componentShiftLeft" *ngFor="let component of components"> + <div class="componentSummary" *ngIf="!isModal"> + <svg-icon [clickable]="true" class="expand-collapse-icon" + [name]="isExpanded(component) ? 'caret1-up-o': 'caret1-down-o'" [mode]="'primary'" + [size]="'medium'" [attr.data-tests-id]="generateExpandDataTestID(component)" (click)="expandRow(component)"></svg-icon> + + + <span class="rightVerticalSeperatorComponent titleSummaryFontSettings" [attr.data-tests-id]="generateTotalComponentArtifactsLabel(component, '')">{{ component }} <span class="blue" data-tests-id="totalComponentArtifactsValue">{{ getTotalArtifactsForDistributionID(rowDistributionID, component) }}</span></span> + <span class="rightVerticalSeperatorComponent titleSummaryFontSettings" [attr.data-tests-id]="generateTotalComponentArtifactsLabel(component, 'Notified')">Notified <span class="blue" data-tests-id="totalComponentNotifiedArtifactsValue">{{ getLengthArtifactsForDistributionIDByStatus(rowDistributionID, 'NOTIFIED', component) }}</span></span> + <span class="rightVerticalSeperatorComponent titleSummaryFontSettings" [attr.data-tests-id]="generateTotalComponentArtifactsLabel(component, 'Downloaded')">Downloaded <span class="blue" data-tests-id="totalComponentDownloadedArtifactsValue">{{ getLengthArtifactsForDistributionIDByStatus(rowDistributionID, 'DOWNLOAD_OK', component) }}</span></span> + <span class="rightVerticalSeperatorComponent titleSummaryFontSettings" [attr.data-tests-id]="generateTotalComponentArtifactsLabel(component, 'Deployed')">Deployed <span class="blue" data-tests-id="totalComponentDeployedArtifactsValue">{{ getLengthArtifactsForDistributionIDByStatus(rowDistributionID, 'DEPLOY_OK', component) }}</span></span> + <span class="rightVerticalSeperatorComponent titleSummaryFontSettings" [attr.data-tests-id]="generateTotalComponentArtifactsLabel(component, 'NotNotified')">Not Notified <span class="blue" data-tests-id="totalComponentNotNotifiedArtifactsValue">{{ getLengthArtifactsForDistributionIDByStatus(rowDistributionID, 'NOT_NOTIFIED', component) }}</span></span> + <span class="msoStatus" [ngClass]="{'red': getMSOStatus (rowDistributionID, component) === 'COMPONENT_DONE_ERROR', 'green': getMSOStatus (rowDistributionID, component) === 'COMPONENT_DONE_OK'}">{{ getMSOStatus (rowDistributionID, component) }}</span> + <span class="rightVerticalSeperatorComponent floatRight titleSummaryFontSettings" [attr.data-tests-id]="generateTotalComponentArtifactsLabel(component, 'DeployErrors')">Deploy Errors <span class="red" data-tests-id="totalComponentDeployErrorArtifactsValue">{{ getLengthArtifactsForDistributionIDByStatus(rowDistributionID, 'DEPLOY_ERROR', component) }}</span></span> + <span class="rightVerticalSeperatorComponent floatRight titleSummaryFontSettings" [attr.data-tests-id]="generateTotalComponentArtifactsLabel(component, 'DownloadErrors')">Download Errors <span class="red" data-tests-id="totalComponentDownloadErrorArtifactsValue">{{ getLengthArtifactsForDistributionIDByStatus(rowDistributionID, 'DOWNLOAD_ERROR', component) }}</span></span> + </div> + + <div class="componentSummary" *ngIf="isModal"> + <svg-icon [clickable]="true" class="expand-collapse-icon" + [name]="isExpanded(component) ? 'caret1-up-o': 'caret1-down-o'" [mode]="'primary'" + [size]="'medium'" [attr.data-tests-id]="generateExpandDataTestID(component+'_ForModal')" (click)="expandRow(component)"></svg-icon> + <span data-tests-id="modalComponentLabel"><a>{{ component }} <span class="blue" data-tests-id="modalComponentValue">{{ getLengthArtifactsForDistributionIDByStatus(rowDistributionID, statusFilter, component) }} </span></a></span> + </div> + + <div *ngIf="isExpanded(component)"> + <app-distribution-component-artifact-table [rowDistributionID]= rowDistributionID [componentName]=component + [statusFilter]="statusFilter"></app-distribution-component-artifact-table> + </div> + </div> +</div> + diff --git a/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-table.component.less b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-table.component.less new file mode 100644 index 0000000000..3eab18ca14 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-table.component.less @@ -0,0 +1,66 @@ +.red { + color: red; +} + +.green { + color: green; +} + +.msoStatus { + padding-left: 5px; +} + +.blue { + color: #009fdb; + font-family: OpenSans-Semibold, sans-serif; + font-size: 14px; +} + +.rightVerticalSeperator { + border-right: 1px solid #d2d2d2; + padding-left: 5px; + padding-right: 5px; + cursor: pointer; +} + +.rightVerticalSeperatorComponent { + border-right: 1px solid #d2d2d2; + padding-left: 5px; + padding-right: 5px; +} + +.floatRight{ + float: right; +} + +.distributionSummary { + padding-top: 5px; + padding-bottom: 5px; + background-color: #eaeaea; + padding-left: 25px; + padding-right: 25px; +} + +.componentSummary { + margin-top: 5px; + margin-bottom: 5px; + padding-top: 5px; + padding-bottom: 5px; + background-color: #eaeaea; + padding-left: 25px; + padding-right: 25px; +} + +.componentShiftLeft { + margin-left: 15px; +} + +.titleSummaryFontSettings { + color: #191919; + font-family: OpenSans-Regular, sans-serif; + font-size: 14px; +} + + + + diff --git a/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-table.component.spec.ts b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-table.component.spec.ts new file mode 100644 index 0000000000..ff89b92fd8 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-table.component.spec.ts @@ -0,0 +1,47 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture } from '@angular/core/testing'; +import { NgxDatatableModule } from '@swimlane/ngx-datatable'; +import { SdcUiServices } from 'onap-ui-angular'; +import { ConfigureFn, configureTests } from '../../../../../../jest/test-config.helper'; +import { DistributionService } from '../distribution.service'; +import { DistributionComponentTableComponent } from './distribution-component-table.component'; + +describe('DistributionComponentTableComponent', () => { + let fixture: ComponentFixture<DistributionComponentTableComponent>; + let distibutionServiceMock: Partial<DistributionService>; + + const mockComponentsForDistribution = ['Consumer1', 'Consumer2']; + + beforeEach(() => { + + distibutionServiceMock = { + getComponentsByDistributionID: jest.fn().mockReturnValue(mockComponentsForDistribution), + getArtifactstByDistributionIDAndComponentsName: jest.fn(), + getArtifactsForDistributionIDAndComponentByStatus: jest.fn() + }; + + const configure: ConfigureFn = (testBed) => { + testBed.configureTestingModule({ + declarations: [DistributionComponentTableComponent], + imports: [NgxDatatableModule], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + {provide: DistributionService, useValue: distibutionServiceMock}, + {provide: SdcUiServices.ModalService, useValue: {}} + ], + }); + }; + + configureTests(configure).then((testBed) => { + fixture = testBed.createComponent(DistributionComponentTableComponent); + }); + + }); + + it('Once the Distribution Component Table Component is created - components will keep the relevant components for a specific distributionID', async () => { + await fixture.componentInstance.ngOnInit(); + expect(fixture.componentInstance.components.length).toBe(2); + expect(fixture.componentInstance.components[0]).toBe('Consumer1'); + expect(fixture.componentInstance.components[1]).toBe('Consumer2'); + }); +}); diff --git a/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-table.component.ts b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-table.component.ts new file mode 100644 index 0000000000..e3aaf9d639 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution-component-table/distribution-component-table.component.ts @@ -0,0 +1,104 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { SdcUiCommon, SdcUiComponents, SdcUiServices } from 'onap-ui-angular'; +import { ModalComponent } from 'onap-ui-angular/dist/modals/modal.component'; +import { DistributionComponent } from '../distribution.component'; +import { DistributionService } from '../distribution.service'; + +@Component({ + selector: 'app-distribution-component-table', + templateUrl: './distribution-component-table.component.html', + styleUrls: ['./distribution-component-table.component.less'] +}) +export class DistributionComponentTableComponent implements OnInit { + + @Input() rowDistributionID: string; + @Input() isModal: boolean = false; + @Input() statusFilter: string; + public components = []; + private customModalInstance: ModalComponent; + private expanded = []; + constructor(private distributionService: DistributionService, + private modalService: SdcUiServices.ModalService) { + } + + ngOnInit() { + this.initComponents(); + } + + private generateTotalComponentArtifactsLabel(componentName: any, status: string): string { + return 'total' + componentName + status + 'ArtifactsLabel'; + } + + private generateExpandDataTestID(componentName: string) { + return 'expandIcon_' + componentName; + } + + private initComponents() { + this.components = this.distributionService.getComponentsByDistributionID(this.rowDistributionID); + this.components.map((component) => this.expanded.push({componentName: component, expanded: false})); + } + + private getTotalArtifactsForDistributionID(distributionID: string, componentName?: string): number { + return this.distributionService.getArtifactstByDistributionIDAndComponentsName(distributionID, componentName).length; + } + + private getLengthArtifactsForDistributionIDByStatus(distributionID: string, statusToSerach: string, componentName?: string): number { + if (componentName) { + return this.distributionService.getArtifactsForDistributionIDAndComponentByStatus(distributionID, statusToSerach, componentName).length; + } else { + return this.distributionService.getArtifactsForDistributionIDAndComponentByStatus(distributionID, statusToSerach).length; + } + } + + private openModal(rowDistributionID: string, statusFilter: string) { + + const title: string = 'Distribution by Status'; + const attributeModalConfig = { + title, + size: 'sdc-xl', + type: SdcUiCommon.ModalType.custom, + buttons: [ + { + id: 'close', + text: 'Close', + size: 'sm', + closeModal: true, + disabled: false, + } + ] as SdcUiCommon.IModalButtonComponent[] + }; + + this.customModalInstance = this.modalService.openCustomModal(attributeModalConfig, DistributionComponent, { + // inputs + rowDistributionID, + statusFilter, + isModal: true, + }); + } + + private expandRow(componentName: string) { + console.log('Should expand componentSummary for componentName = ' + componentName); + const selectedComponent = this.expanded.find((component) => component.componentName === componentName); + // tslint:disable:no-string-literal + const selectedComponentExpandedVal = selectedComponent['expanded']; + // this.expanded = !this.expanded; + for (const i in this.expanded) { + if (this.expanded[i].componentName === componentName) { + this.expanded[i].expanded = !this.expanded[i].expanded; + break; //Stop this loop, we found it! + } + } + const selectedComponentAfter = this.expanded.find((component) => component.componentName === componentName); + const selectedComponentExpandedValAfter = selectedComponentAfter['expanded']; + } + + private isExpanded(componentName: string) { + const selectedComponent = this.expanded.find((component) => component.componentName === componentName); + return selectedComponent['expanded']; + } + + + private getMSOStatus(rowDistributionID: string, componentName: string): string { + return this.distributionService.getMSOStatus(rowDistributionID, componentName); + } +} diff --git a/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution.component.html b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution.component.html new file mode 100644 index 0000000000..d0cacb054e --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution.component.html @@ -0,0 +1,80 @@ +<div *ngIf="!isModal"> + <div *ngIf="serviceHasDistibutions" class="w-sdc-distribution-view-header"> + <div class="w-sdc-distribution-view-title" data-tests-id="DistributionsLabel">DISTRIBUTION <span class="blue-font" data-tests-id="totalArtifacts">[{{distributions.length}}]</span></div> + <div class="header-spacer"></div> + <input type="text" value="GeeksForGeeks" id="inputForCopyToClipboard" [ngStyle]="{'z-index': '-2', 'width': '25px'}"> + <div class="top-search"> + <input type="text" + style="width: auto;" + class="search-text" + data-tests-id="searchTextbox" + placeholder="Search" + data-ng-model="searchBind" + ng-model-options="{ debounce: 500 }" + (keyup)="updateFilter($event)"/> + </div> + <div class="sprite-new refresh-btn" data-tests-id="refreshButton" (click)="refreshDistributions()" title="Refresh"></div> + </div> + <div class="w-sdc-distribution-view-header w-sdc-distribution-view-title" data-tests-id="noDistributionsLabel" *ngIf="!serviceHasDistibutions">No Distributions To Present</div> +</div> + +<div *ngIf="serviceHasDistibutions"> + <ngx-datatable + [columnMode]="'flex'" + [rowHeight]="'auto'" + [reorderable]="false" + [swapColumns]="false" + [scrollbarV]="false" + [rows]="distributions" + [sorts]="[{prop: 'timestamp', dir: 'desc'}]" + + #distributionTable> + <ngx-datatable-row-detail [rowHeight]="'auto'"> + <ng-template let-row="row" let-expanded="expanded" ngx-datatable-row-detail-template> + <app-distribution-component-table [rowDistributionID]=row.distributionID [isModal]="isModal" + [statusFilter]="statusFilter"></app-distribution-component-table> + </ng-template> + </ngx-datatable-row-detail> + <ngx-datatable-column [resizeable]="false" [flexGrow]="2" name="Distribution ID"> + <ng-template ngx-datatable-cell-template let-row="row" let-expanded="expanded" > + <div class="expand-collapse-cell"> + <a><svg-icon [clickable]="true" class="expand-collapse-icon" + [name]="expanded ? 'caret1-up-o': 'caret1-down-o'" [mode]="'primary'" + [size]="'medium'" (click)="expandRow(row, expanded)" [attr.data-tests-id]="generateDataTestID('expandIcon_', row.distributionID, isModal)"></svg-icon></a> + + </div> + <div class="distributionIDBlock"> + <div class = "distributionRowValue" [attr.data-tests-id]="generateDataTestID('distID_', row.distributionID, isModal)">{{ row.distributionID }}</div> + </div> + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column [resizeable]="false" [flexGrow]="1" name="User id"> + <ng-template ngx-datatable-cell-template let-row="row"> + <div class = "distributionRowValue ellipsisCell" [attr.data-tests-id]="generateDataTestID('userID_', row.distributionID)" sdc-tooltip [tooltip-placement]="3" [tooltip-text]="row.userId">{{ row.userId }}</div> + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column [resizeable]="false" [flexGrow]="1" name="Time[UTC]"> + <ng-template ngx-datatable-cell-template let-row="row"> + <div class = "distributionRowValue" [attr.data-tests-id]="generateDataTestID('timeStamp_', row.distributionID)">{{ row.timestamp }} </div> + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column [resizeable]="false"[flexGrow]="1" name="Status" > + <ng-template ngx-datatable-cell-template let-row="row"> + <div> + <span class="statusIcon"> + <svg-icon [clickable]="true" class="expand-collapse-icon" + [name]= "getIconName(row.deployementStatus)" [mode]="'primary'" + [size]="'medium'"></svg-icon> + </span> + <span class = "distributionRowValue" [attr.data-tests-id]="generateDataTestID('status_', row.distributionID)"> + {{ row.deployementStatus }} + </span> + <span class="btnMarkAsDistributed" (click)="markDeploy(row.distributionID, row.deployementStatus)"> + <svg-icon [clickable]="true" [name]= "'success'" [mode]="getIconMode(row.deployementStatus)" + [size]="'medium'"></svg-icon> + </span> + </div> + </ng-template> + </ngx-datatable-column> + </ngx-datatable> +</div> diff --git a/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution.component.less b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution.component.less new file mode 100644 index 0000000000..b630881fdc --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution.component.less @@ -0,0 +1,92 @@ +:host ::ng-deep { + .ngx-datatable { + > div { + min-height: 500px; + datatable-body { + max-height: max-content; + } + } + } +} + +.w-sdc-distribution-view-header { + display: flex; + -webkit-justify-content: space-between; + margin: 0 25px 5px 40px; + + .header-spacer { + flex-grow: 5; + } + + .w-sdc-distribution-view-title{ + color: #191919; + font-family: OpenSans-Regular, sans-serif; + font-size: 14px; + line-height: 30px; + + + .blue-font { + color: #009fdb; + font-family: OpenSans-Semibold, sans-serif; + font-size: 14px; + } + } + +} + +.distribution-page { + max-height: 150px; +} + + .distributionIDBlock { + display: inline-block; + } + + .expand-collapse-cell { + display: inline-block; + } + + .statusIcon { + display: inline-block; + margin-right: 10px; + } + + .btnMarkAsDistributed { + float: right; + background-color: #E5F3FF; + border: 1px solid #8DCCD5; + width: 55px; + height: 21px; + text-align: center; + } + + .distributionRowContainer{ + background-color: #eaeaea; + text-align: center; + } + + .distributionRowLabel { + overflow: hidden; + padding-top: 10px; + color: #000000; + font-family: OpenSans-Semibold, sans-serif; + font-size: 12px; + font-weight: bold; + } + + .distributionRowValue { + color: #263d4d; + font-family: OpenSans-Regular, sans-serif; + font-size: 14px; + } + +.ellipsisCell { + width: 92%; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; +} + + + + diff --git a/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution.component.spec.ts b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution.component.spec.ts new file mode 100644 index 0000000000..e6c9c239e1 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution.component.spec.ts @@ -0,0 +1,92 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture } from '@angular/core/testing'; +import { NgxDatatableModule } from '@swimlane/ngx-datatable'; +import { SdcUiServices } from 'onap-ui-angular'; +import { ConfigureFn, configureTests } from '../../../../../jest/test-config.helper'; +import { ComponentMetadata } from '../../../../models/component-metadata'; +import { AuthenticationService } from '../../../services/authentication.service'; +import { WorkspaceService } from '../workspace.service'; +import { DistributionComponent } from './distribution.component'; +import { DistributionService } from './distribution.service'; +import {EventListenerService} from "../../../../services/event-listener-service"; + +describe('DistributionComponent', () => { + let fixture: ComponentFixture<DistributionComponent>; + let distibutionServiceMock: Partial<DistributionService>; + let workspaceServiceMock: Partial<WorkspaceService>; + let loaderServiceMock: Partial<SdcUiServices.LoaderService>; + let authenticationServiceMock: Partial <AuthenticationService>; + let eventListenerService: Partial <EventListenerService>; + + const mockDistributionListFromService = [ + { + deployementStatus: 'Distributed', + distributionID: '1', + timestamp: '2019-07-21 08:37:02.834 UTC', + userId: 'Aretha Franklin(op0001)' + }, { + deployementStatus: 'Distributed', + distributionID: '2', + timestamp: '2019-07-21 09:37:02.834 UTC', + userId: 'Aretha Franklin(op0001)' + }]; + + beforeEach(() => { + + distibutionServiceMock = { + initDistributionsList: jest.fn(), + getDistributionList: jest.fn().mockReturnValue(mockDistributionListFromService), + initDistributionsStatusForDistributionID: jest.fn() + }; + + const componentMetadata = new ComponentMetadata(); + componentMetadata.uuid = '111'; + + workspaceServiceMock = { + metadata : componentMetadata + }; + + authenticationServiceMock = { + getLoggedinUser: jest.fn().mockReturnValue({role: 'designer'}) + }; + + eventListenerService = { + registerObserverCallback: jest.fn(), + unRegisterObserver: jest.fn() + } + + loaderServiceMock = { + activate: jest.fn(), + deactivate: jest.fn() + }; + + const configure: ConfigureFn = (testBed) => { + testBed.configureTestingModule({ + declarations: [DistributionComponent], + imports: [NgxDatatableModule], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + {provide: DistributionService, useValue: distibutionServiceMock}, + {provide: WorkspaceService, useValue: workspaceServiceMock}, + {provide: SdcUiServices.LoaderService, useValue: loaderServiceMock}, + {provide: AuthenticationService, useValue: authenticationServiceMock}, + {provide: EventListenerService, useValue: eventListenerService} + ], + }); + }; + + configureTests(configure).then((testBed) => { + fixture = testBed.createComponent(DistributionComponent); + }); + + }); + + it('Once the Distribution Component is created - distributionsResponseFromServer save all the distributions from the Service', async () => { + fixture.componentInstance.componentUuid = 'componentUid'; + fixture.componentInstance.rowDistributionID = null; + fixture.componentInstance.isModal = false; + + await fixture.componentInstance.ngOnInit(); + expect(fixture.componentInstance.distributions.length).toBe(2); + }); +}); diff --git a/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution.component.ts b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution.component.ts new file mode 100644 index 0000000000..ca1b6292d3 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution.component.ts @@ -0,0 +1,117 @@ +import { Component, Input, OnInit, ViewChild } from '@angular/core'; +import { SdcUiCommon, SdcUiServices } from 'onap-ui-angular'; +import { EventListenerService } from '../../../../services/event-listener-service'; +import { AuthenticationService } from '../../../services/authentication.service'; +import { WorkspaceService } from '../workspace.service'; +import { DistributionService } from './distribution.service'; +import { EVENTS } from '../../../../utils/constants'; + +@Component({ + selector: 'distribution', + templateUrl: './distribution.component.html', + styleUrls: ['../../../../../assets/styles/table-style.less', './distribution.component.less'] +}) +export class DistributionComponent implements OnInit { + + @ViewChild('distributionTable', { }) table: any; + + @Input() isModal: boolean = false; + @Input() statusFilter: string; + @Input() rowDistributionID: string; + public componentUuid: string; + public distributions = []; + private expanded: any = {}; + private serviceHasDistibutions: boolean = false; + private readonly uniqueId: string; + private userRole: string; + + constructor(private workspaceService: WorkspaceService, + private distributionService: DistributionService, + private loaderService: SdcUiServices.LoaderService, + private authService: AuthenticationService, + private eventListenerService: EventListenerService) { + this.componentUuid = this.workspaceService.metadata.uuid; + this.uniqueId = this.workspaceService.metadata.uniqueId; + } + + + + async ngOnInit() { + this.userRole = this.authService.getLoggedinUser().role; + this.eventListenerService.registerObserverCallback(EVENTS.ON_DISTRIBUTION_SUCCESS, async () => { + await this.refreshDistributions(); + }); + await this.initDistributions(this.componentUuid, this.rowDistributionID); + } + + ngOnDestroy(): void { + this.eventListenerService.unRegisterObserver(EVENTS.ON_DISTRIBUTION_SUCCESS); + } + + async initDistributions(componentUuid: string, specificDistributionID?: string) { + this.loaderService.activate(); + await this.distributionService.initDistributionsList(componentUuid); + this.distributions = this.distributionService.getDistributionList(); + this.distributions.length > 0 ? this.serviceHasDistibutions = true : this.serviceHasDistibutions = false; + if (specificDistributionID) { + this.distributions = this.distributionService.getDistributionList(specificDistributionID); + } + this.loaderService.deactivate(); + } + + getIconName(rowStatus: string ) { + if (rowStatus === 'Distributed') { + return 'distributed'; + } + if (rowStatus === 'Deployed') { + return 'v-circle'; + } + } + + getIconMode(rowStatus: string) { + if (rowStatus === 'Distributed') { + return 'primary'; + } + if (rowStatus === 'Deployed') { + return 'secondary'; + } + } + + private async markDeploy(distributionID: string, status: string) { + if (status === 'Distributed') { + console.log('Should send MarkDeploy POST Request ServiceID:' + this.uniqueId + ' DISTID:' + distributionID); + await this.distributionService.markDeploy(this.uniqueId, distributionID); + this.refreshDistributions(); + } + } + + private async refreshDistributions() { + await this.initDistributions(this.componentUuid); + } + + private updateFilter(event) { + const val = event.target.value.toLowerCase(); + + // filter our data + this.distributions = this.distributionService.getDistributionList().filter((distribution: any[]) => { + return !val || + // tslint:disable:no-string-literal + distribution['distributionID'].toLowerCase().indexOf(val) !== -1; + }); + } + + private generateDataTestID(preFix: string, distributionID: string, isModal?: boolean ): string { + if (isModal) { + return preFix + distributionID.substring(0, 5) + '_Modal'; + } else { + return preFix + distributionID.substring(0, 5); + } + } + + private async expandRow(row: any, expanded: boolean) { + if (!expanded) { + await this.distributionService.initDistributionsStatusForDistributionID(row.distributionID); + } + this.table.rowDetail.toggleExpandRow(row); + } +} diff --git a/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution.module.ts b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution.module.ts new file mode 100644 index 0000000000..723a6d8c0a --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution.module.ts @@ -0,0 +1,34 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { NgxDatatableModule } from '@swimlane/ngx-datatable'; +import { SdcUiComponentsModule } from 'onap-ui-angular'; +import { DistributionComponentArtifactTableComponent } from './distribution-component-table/distribution-component-artifact-table/distribution-component-artifact-table.component'; +import { DistributionComponentTableComponent } from './distribution-component-table/distribution-component-table.component'; +import { DistributionComponent } from './distribution.component'; +import { DistributionService } from './distribution.service'; + +@NgModule({ + declarations: [ + DistributionComponent, + DistributionComponentTableComponent, + DistributionComponentArtifactTableComponent, + ], + imports: [ + // TranslateModule, + CommonModule, + SdcUiComponentsModule, + NgxDatatableModule, + ], + exports: [ + DistributionComponent, + DistributionComponentTableComponent + ], + entryComponents: [ + DistributionComponent, + DistributionComponentTableComponent, + DistributionComponentArtifactTableComponent + ], + providers: [DistributionService] +}) +export class DistributionModule { +} diff --git a/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution.service.ts b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution.service.ts new file mode 100644 index 0000000000..ed6791c5c1 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/disribution/distribution.service.ts @@ -0,0 +1,233 @@ +import { HttpClient } from '@angular/common/http'; +import { Inject, Injectable } from '@angular/core'; +import { tap } from 'rxjs/operators'; +import { Distribution } from '../../../../models/distribution'; +import { ISdcConfig, SdcConfigToken } from '../../../config/sdc-config.config'; + +@Injectable() +export class DistributionService { + protected baseUrl; + private distributionList = []; + private distributionStatusesMap = {}; + + // tslint:disable:no-string-literal + + constructor(protected http: HttpClient, @Inject(SdcConfigToken) sdcConfig: ISdcConfig) { + this.baseUrl = sdcConfig.api.root + sdcConfig.api.component_api_root; + } + + // Once the distribution page is loaded or when the user wants to refresh the list + async initDistributionsList(componentUuid: string): Promise<object> { + const distributionsListURL = this.baseUrl + 'services/' + componentUuid + '/distribution'; + const res = this.http.get<Distribution[]>(distributionsListURL).pipe(tap( (result) => { + this.distributionList = result['distributionStatusOfServiceList']; + this.insertDistrbutionsToMap(); + } )); + return res.toPromise(); + } + + // Once the user click on the relevant distribution ID in the distribution table (open and close) + async initDistributionsStatusForDistributionID(distributionID: string): Promise<object> { + const distributionStatus = this.baseUrl + 'services/distribution/' + distributionID; + const res = this.http.get<object>(distributionStatus).pipe(tap( (result) => { + this.insertDistributionStatusToDistributionsMap(distributionID, result['distributionStatusList']); + } )); + return res.toPromise(); + } + + public getDistributionList(specificDistributionID?: string) { + if (specificDistributionID) { + return this.distributionList.filter((distribution) => { + return distribution['distributionID'] === specificDistributionID; + }); + } else { + return this.distributionList; + } + } + + public getComponentsByDistributionID(distributionID: string) { + const components = []; + const distributionStatusMap = this.getStatusMapForDistributionID(distributionID); + if (distributionStatusMap) { + distributionStatusMap.forEach((component) => components.push(component.componentID)); + } + return components; + } + + // get array of artifacts per distributionID w/o componentName, sliced by artifact status + public getArtifactsForDistributionIDAndComponentByStatus(distributionID: string, statusToSearch: string, componentName?: string) { + const filteredArtifactsByStatus = []; + + if (componentName) { + this.getArtifactstByDistributionIDAndComponentsName(distributionID, componentName).forEach ( (artifact) => { + if (this.artifactStatusHasMatch(artifact, statusToSearch)) { + filteredArtifactsByStatus.push(artifact); + } + } ); + } else { + this.getArtifactstByDistributionIDAndComponentsName(distributionID).forEach ( (artifact) => { + if (this.artifactStatusHasMatch(artifact, statusToSearch)) { + filteredArtifactsByStatus.push(artifact); + } + } ); + } + return filteredArtifactsByStatus; + } + + public getArtifactstByDistributionIDAndComponentsName(distributionID: string, componentName?: string): any[] { + const artifacts = []; + if (this.getStatusMapForDistributionID(distributionID)) { + if (componentName) { + if (this.getStatusMapForDistributionID(distributionID).filter((component) => component.componentID === componentName).length > 0) { + const artifactsArr = this.getStatusMapForDistributionID(distributionID).filter((component) => component.componentID === componentName)[0]['artifacts'] + if (artifactsArr.length > 0) { + artifactsArr.forEach((artifact) => { + const artifactObj = { + url: artifact.artifactUrl, + name: artifact.artifactName, + statuses: artifact.statuses + }; + artifacts.push(artifactObj); + }); + } + } + } else { + const components = this.getComponentsByDistributionID(distributionID); + components.forEach((componentName) => { + if (this.getStatusMapForDistributionID(distributionID).filter((component) => component.componentID === componentName).length > 0) { + const artifactsArr = this.getStatusMapForDistributionID(distributionID).filter((component) => component.componentID === componentName)[0]['artifacts'] + if (artifactsArr.length > 0) { + artifactsArr.forEach((artifact) => { + const artifactObj = { + url: artifact.artifactUrl, + name: artifact.artifactName, + statuses: artifact.statuses + }; + artifacts.push(artifactObj); + }); + } + } + }); + } + } + return artifacts; + } + + public getStatusMapForDistributionID(distributionID: string) { + return this.distributionStatusesMap[distributionID]; + } + + public markDeploy(uniqueId: string, distributionID: string): Promise<object> { + const distributionStatus = this.baseUrl + 'services/' + uniqueId + '/distribution/' + distributionID + '/markDeployed'; + const res = this.http.post<object>(distributionStatus, {}).pipe(tap( (result) => { + console.log(result); + } )); + return res.toPromise(); + } + + public getMSOStatus(distributionID: string, componentName: string): string { + const msoStatus = this.distributionStatusesMap[distributionID].filter((component) => component.componentID === componentName)[0].msoStatus; + return msoStatus ? msoStatus : ''; + } + + private artifactStatusHasMatch(artifact: any, statusToSerach: string) { + for (let i = 0; i < artifact.statuses.length; i++) { + if (artifact.statuses[i].status === statusToSerach) { + return true; + } + } + return false; + } + + private insertDistributionStatusToDistributionsMap(distributionID: string, distributionStatusMapResponseFromServer: object[]) { + + // // Clear the Distribution ID array - to avoid statuses duplications + const distribution = this.distributionStatusesMap[distributionID]; + distribution.length = 0; + + // Sort the response of statuses from Server, so it will be easy to pop the latest status when it will be required + const sortedResponseByTimeStamp = distributionStatusMapResponseFromServer.sort((a, b) => b['timestamp'] - a['timestamp']) + + sortedResponseByTimeStamp.map((distributionStatus) => { + const formattedDate = this.formatDate(distributionStatus['timestamp']); + + // if (distributionStatus['url'] === null) { + // distributionStatus['url'] = ""; + // } + + const detailedArtifactStatus = { + componentID: distributionStatus['omfComponentID'], + artifactName: distributionStatus['url']? distributionStatus['url'].split('/').pop() : '', + url: distributionStatus['url'], + time: distributionStatus['timestamp'], + status: distributionStatus['status'], + }; + + + + // Add Component to this.distributionStatusesMap in case not exist. + let componentPosition = _.findIndex(distribution, {componentID: detailedArtifactStatus.componentID}) + + if (componentPosition === -1) { + this.addComponentIdToDistributionStatusMap(distributionID, detailedArtifactStatus.componentID); + componentPosition = distribution.length - 1; + } + + const component = distribution[componentPosition]; + + + // Add Artifact to this.distributionStatusesMap[componentID] in case not exist. + let artifactPosition = _.findIndex(component.artifacts, {artifactUrl: detailedArtifactStatus.url}) + + if (artifactPosition === -1) { + this.addArtifactToComponentId(distributionID, componentPosition, detailedArtifactStatus.artifactName, detailedArtifactStatus.url); + artifactPosition = component.artifacts.length - 1; + } + + + // Add status to relevat artifact in relevent componentID. + if (detailedArtifactStatus.url) { + // Case where there is a url -> should add its status + component.artifacts[artifactPosition].statuses.push({ + timeStamp: detailedArtifactStatus.time, + status: detailedArtifactStatus.status + }); + } else { + // Should update the Component -> status from MSO + this.distributionStatusesMap[distributionID][componentPosition].msoStatus = detailedArtifactStatus.status; + } + + + }); + } + + private addComponentIdToDistributionStatusMap(distributionID: string, componentIDValue: string) { + this.distributionStatusesMap[distributionID].push({ + componentID: componentIDValue, + msoStatus: null, + artifacts: [] + }); + } + + private addArtifactToComponentId(distributionID: string, componentPosition: number, artifactNameValue: string, artifactURLValue: any) { + if (artifactNameValue) { + this.distributionStatusesMap[distributionID][componentPosition].artifacts.push({ + artifactName: artifactNameValue, + artifactUrl: artifactURLValue, + statuses: [] + }); + } + } + + private insertDistrbutionsToMap() { + this.distributionList.map((distribution) => this.distributionStatusesMap[distribution.distributionID] = []); + } + + private formatDate(epochTime: string) { + const intEpochTime = new Date(parseInt(epochTime, 10)); + const amOrPm = (intEpochTime.getHours() + 24) % 24 > 12 ? 'PM' : 'AM'; + const formattedDate = (intEpochTime.getMonth() + 1) + '/' + intEpochTime.getDate() + '/' + intEpochTime.getFullYear() + ' ' + intEpochTime.getHours() + ':' + + intEpochTime.getMinutes() + amOrPm; + return formattedDate; + } +} diff --git a/catalog-ui/src/app/ng2/pages/workspace/information-artifact/__snapshots__/informational-artifact-page.spec.ts.snap b/catalog-ui/src/app/ng2/pages/workspace/information-artifact/__snapshots__/informational-artifact-page.spec.ts.snap new file mode 100644 index 0000000000..1a19b36cfb --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/information-artifact/__snapshots__/informational-artifact-page.spec.ts.snap @@ -0,0 +1,40 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`informational artifacts page should match current snapshot of informational artifact pages component 1`] = ` +<information-artifact-page + addOrUpdateArtifact={[Function Function]} + artifactsService={[Function Object]} + deleteArtifact={[Function Function]} + store={[Function Store]} + table={[Function DatatableComponent]} + workspaceService={[Function Object]} +> + <div + class="information-artifact-page" + > + <svg-icon-label + class="add-artifact-btn" + /> + <ngx-datatable + class="ngx-datatable" + columnmode="flex" + > + <div + visibilityobserver="" + > + + <datatable-body + class="datatable-body" + > + <datatable-selection> + + + + </datatable-selection> + </datatable-body> + + </div> + </ngx-datatable> + </div> +</information-artifact-page> +`; diff --git a/catalog-ui/src/app/ng2/pages/workspace/information-artifact/information-artifact-page.component.html b/catalog-ui/src/app/ng2/pages/workspace/information-artifact/information-artifact-page.component.html new file mode 100644 index 0000000000..cff33258ae --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/information-artifact/information-artifact-page.component.html @@ -0,0 +1,82 @@ +<div class="information-artifact-page"> + <svg-icon-label class="add-artifact-btn" [clickable]="true" [mode]="'primary'" [labelPlacement]="'right'" + [label]="'Add'" [name]="'plus'" [testId]="'add-information-artifact-button'" + (click)="addOrUpdateArtifact()"></svg-icon-label> + <ngx-datatable + columnMode="flex" + [headerHeight]="40" + [reorderable]="false" + [swapColumns]="false" + [rows]="informationArtifacts$ | async" + [footerHeight]="'undefined'" + [sorts]="[{prop: 'artifactDisplayName', dir: 'desc'}]" + #informationArtifactsTable + (activate)="onActivate($event)"> + <ngx-datatable-row-detail [rowHeight]="80"> + <ng-template let-row="row" let-expanded="expanded" ngx-datatable-row-detail-template> + <div [attr.data-tests-id]="row.artifactDisplayName+'Description'">{{row.description}}</div> + </ng-template> + </ngx-datatable-row-detail> + <ngx-datatable-column [resizeable]="false" name="Name" [flexGrow]="3" + [prop]="'artifactDisplayName'"> + <ng-template ngx-datatable-cell-template let-row="row" let-expanded="expanded"> + <div class="expand-collapse-cell"> + <svg-icon [clickable]="true" class="expand-collapse-icon" + [name]="expanded ? 'caret1-up-o': 'caret1-down-o'" [mode]="'primary'" + [size]="'medium'"></svg-icon> + <span [attr.data-tests-id]="'artifactDisplayName_' + row.artifactDisplayName">{{row.artifactDisplayName }}</span> + </div> + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column [resizeable]="false" name="Type" [flexGrow]="1"> + <ng-template ngx-datatable-cell-template let-row="row"> + {{row.artifactType}} + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column [resizeable]="false" name="Version" [flexGrow]="1"> + <ng-template ngx-datatable-cell-template let-row="row"> + {{ row.artifactVersion }} + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column [resizeable]="false" name="UUID" [flexGrow]="2"> + <ng-template ngx-datatable-cell-template let-row="row"> + {{ row.artifactUUID }} + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column [resizeable]="false" [flexGrow]="1"> + <ng-template ngx-datatable-cell-template let-row="row"> + <div class="download-artifact-button"> + <svg-icon class="action-icon" *ngIf="!row.isThirdParty()" [mode]="'primary2'" + [disabled]="isViewOnly$ | async" [name]="'edit-o'" + testId="edit_{{row.artifactDisplayName}}" clickable="true" size="medium" + (click)="addOrUpdateArtifact(row)"></svg-icon> + <svg-icon class="action-icon" *ngIf="!row.isThirdParty()" [mode]="'primary2'" + [disabled]="isViewOnly$ | async" [name]="'trash-o'" + testId="delete_{{row.artifactDisplayName}}" clickable="true" size="medium" (click)="deleteArtifact(row)"></svg-icon> + <download-artifact class="action-icon" [disabled]="isViewOnly$ | async" [artifact]="row" + [componentId]="componentId" + [componentType]="componentType" + testId="download_{{row.artifactDisplayName}}"></download-artifact> + </div> + </ng-template> + </ngx-datatable-column> + + <ngx-datatable-footer> + <ng-template ngx-datatable-footer-template> + <div class="add-artifacts-dynamic-btn-list"> + <sdc-button *ngFor="let artifact of informationArtifactsAsButtons$ | async" + class="add-artifacts-dynamic-btn" + testId="add_artifact_{{artifact.artifactDisplayName}}" + [type]="'secondary'" + [size]="'xx-large'" + [text]="'ADD ' + artifact.artifactDisplayName" + [icon_name]="'plus-circle-o'" + [icon_mode] = "'secondary'" + [icon_position]="'left'" + (click)="addOrUpdateArtifact(artifact)"> + </sdc-button> + </div> + </ng-template> + </ngx-datatable-footer> + </ngx-datatable> +</div>
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/workspace/information-artifact/information-artifact-page.component.less b/catalog-ui/src/app/ng2/pages/workspace/information-artifact/information-artifact-page.component.less new file mode 100644 index 0000000000..b69e511f70 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/information-artifact/information-artifact-page.component.less @@ -0,0 +1,29 @@ +.information-artifact-page { + + .add-artifact-btn { + display: flex; + cursor: pointer; + justify-content: flex-end; + margin-bottom: 10px; + } + .download-artifact-button { + display: flex; + justify-content: center; + + .action-icon{ + margin-right: 10px; + } + } + + .add-artifacts-dynamic-btn-list { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + margin: 20px 0px; + .add-artifacts-dynamic-btn{ + width: 350px; + margin-top: 15px; + } + } +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/workspace/information-artifact/information-artifact-page.component.ts b/catalog-ui/src/app/ng2/pages/workspace/information-artifact/information-artifact-page.component.ts new file mode 100644 index 0000000000..a6804a43c6 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/information-artifact/information-artifact-page.component.ts @@ -0,0 +1,69 @@ +import {Component, OnInit, ViewChild} from "@angular/core"; +import {WorkspaceService} from "../workspace.service"; +import {SdcUiCommon, SdcUiComponents, SdcUiServices} from "onap-ui-angular"; +import {TopologyTemplateService} from "../../../services/component-services/topology-template.service"; +import * as _ from "lodash"; +import {ArtifactGroupType, ArtifactType} from "../../../../utils/constants"; +import {ArtifactsService} from "../../../components/forms/artifacts-form/artifacts.service"; +import {DeleteArtifactAction, GetArtifactsByTypeAction} from "../../../store/actions/artifacts.action"; +import {Select, Store} from "@ngxs/store"; +import {Observable} from "rxjs/index"; +import {ArtifactsState} from "../../../store/states/artifacts.state"; +import {map} from "rxjs/operators"; +import {WorkspaceState} from "../../../store/states/workspace.state"; +import {ArtifactModel} from "../../../../models/artifacts"; + +@Component({ + selector: 'information-artifact-page', + templateUrl: './information-artifact-page.component.html', + styleUrls: ['./information-artifact-page.component.less', '../../../../../assets/styles/table-style.less'] +}) +export class InformationArtifactPageComponent implements OnInit { + + public componentId: string; + public componentType: string; + public informationArtifacts$: Observable<ArtifactModel[]>; + public informationArtifactsAsButtons$: Observable<ArtifactModel[]>; + @Select(WorkspaceState.isViewOnly) isViewOnly$: boolean; + @ViewChild('informationArtifactsTable') table: any; + + constructor(private workspaceService: WorkspaceService, + private artifactsService: ArtifactsService, + private store: Store) { + } + + ngOnInit(): void { + this.componentId = this.workspaceService.metadata.uniqueId; + this.componentType = this.workspaceService.metadata.componentType; + + this.store.dispatch(new GetArtifactsByTypeAction({ + componentType: this.componentType, + componentId: this.componentId, + artifactType: ArtifactGroupType.INFORMATION + })); + + let artifacts = this.store.select(ArtifactsState.getArtifactsByType).pipe(map(filterFn => filterFn(ArtifactType.INFORMATION))); + this.informationArtifacts$ = artifacts.pipe(map(artifacts => _.filter(artifacts, (artifact) => { + return artifact.esId; + }))); + + this.informationArtifactsAsButtons$ = artifacts.pipe(map(artifacts => _.filter(artifacts, (artifact) => { + return !artifact.esId; + }))); + } + + onActivate(event) { + if (event.type === 'click') { + this.table.rowDetail.toggleExpandRow(event.row); + } + } + + public addOrUpdateArtifact = (artifact: ArtifactModel, isViewOnly?: boolean) => { + this.artifactsService.openArtifactModal(this.componentId, this.componentType, artifact, ArtifactGroupType.INFORMATION, isViewOnly); + } + + public deleteArtifact = (artifactToDelete) => { + this.artifactsService.deleteArtifact(this.componentType, this.componentId, artifactToDelete) + } + +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/workspace/information-artifact/information-artifact-page.module.ts b/catalog-ui/src/app/ng2/pages/workspace/information-artifact/information-artifact-page.module.ts new file mode 100644 index 0000000000..5eb9e5851b --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/information-artifact/information-artifact-page.module.ts @@ -0,0 +1,30 @@ +import {CommonModule} from "@angular/common"; +import {NgModule} from "@angular/core"; +import {SdcUiComponentsModule} from "onap-ui-angular"; +import {NgxDatatableModule} from "@swimlane/ngx-datatable"; +import {UiElementsModule} from "../../../components/ui/ui-elements.module"; +import {InformationArtifactPageComponent} from "./information-artifact-page.component"; +import {ArtifactFormModule} from "../../../components/forms/artifacts-form/artifact-form.module"; +import {ArtifactsService} from "../../../components/forms/artifacts-form/artifacts.service"; + +@NgModule({ + declarations: [ + InformationArtifactPageComponent + ], + imports: [ + CommonModule, + SdcUiComponentsModule, + NgxDatatableModule, + UiElementsModule, + ArtifactFormModule + ], + exports: [ + InformationArtifactPageComponent + ], + entryComponents: [ + InformationArtifactPageComponent + ], + providers:[ArtifactsService] +}) +export class InformationArtifactPageModule { +} diff --git a/catalog-ui/src/app/ng2/pages/workspace/information-artifact/informational-artifact-page.spec.ts b/catalog-ui/src/app/ng2/pages/workspace/information-artifact/informational-artifact-page.spec.ts new file mode 100644 index 0000000000..10fd14739b --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/information-artifact/informational-artifact-page.spec.ts @@ -0,0 +1,77 @@ +import {async, ComponentFixture, TestBed} from "@angular/core/testing"; +import {NO_ERRORS_SCHEMA} from "@angular/core"; +import {ConfigureFn, configureTests} from "../../../../../jest/test-config.helper"; +import {NgxDatatableModule} from "@swimlane/ngx-datatable"; +import {WorkspaceService} from "../workspace.service"; +import {SdcUiServices} from "onap-ui-angular"; +import {TopologyTemplateService} from "../../../services/component-services/topology-template.service"; +import {Observable} from "rxjs/Observable"; +import {ComponentMetadata} from "../../../../models/component-metadata"; +import 'rxjs/add/observable/of'; +import {NgxsModule, Store} from "@ngxs/store"; +import {ArtifactsState} from "../../../store/states/artifacts.state"; +import {InformationArtifactPageComponent} from "./information-artifact-page.component"; +import { informationalArtifactsMock} from "../../../../../jest/mocks/artifacts-mock"; +import {ArtifactsService} from "../../../components/forms/artifacts-form/artifacts.service"; + +describe('informational artifacts page', () => { + + let fixture: ComponentFixture<InformationArtifactPageComponent>; + let topologyTemplateServiceMock: Partial<TopologyTemplateService>; + let workspaceServiceMock: Partial<WorkspaceService>; + let loaderServiceMock: Partial<SdcUiServices.LoaderService>; + let store: Store; + + beforeEach( + async(() => { + + topologyTemplateServiceMock = { + getArtifactsByType: jest.fn().mockImplementation((componentType, id, artifactType) => Observable.of(informationalArtifactsMock)) + }; + workspaceServiceMock = {metadata: <ComponentMetadata>{uniqueId: 'service_unique_id', componentType: 'SERVICE'}} + + loaderServiceMock = { + activate : jest.fn(), + deactivate: jest.fn() + } + const configure: ConfigureFn = testBed => { + testBed.configureTestingModule({ + declarations: [InformationArtifactPageComponent], + imports: [NgxDatatableModule, NgxsModule.forRoot([ArtifactsState])], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + {provide: WorkspaceService, useValue: workspaceServiceMock}, + {provide: TopologyTemplateService, useValue: topologyTemplateServiceMock}, + {provide: SdcUiServices.LoaderService, useValue: loaderServiceMock }, + {provide: ArtifactsService, useValue: {}}, + ], + }); + }; + + configureTests(configure).then(testBed => { + fixture = testBed.createComponent(InformationArtifactPageComponent); + store = testBed.get(Store); + }); + }) + ); + + it('should match current snapshot of informational artifact pages component', () => { + expect(fixture).toMatchSnapshot(); + }); + + it('should see exactly 3 informational artifacts and six buttons to add artifact by template', () => { + fixture.componentInstance.ngOnInit(); + fixture.componentInstance.informationArtifacts$.subscribe((artifacts)=> { + expect(artifacts.length).toEqual(3); + }) + fixture.componentInstance.informationArtifactsAsButtons$.subscribe((artifacts)=> { + expect(artifacts.length).toEqual(6); + }) + + store.selectOnce(state => state.artifacts.artifacts).subscribe(artifacts => { + expect(artifacts.length).toEqual(9); + }); + }) + + +});
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilities-properties/capabilities-properties.html b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilities-properties/capabilities-properties.html new file mode 100644 index 0000000000..f496e64c17 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilities-properties/capabilities-properties.html @@ -0,0 +1,22 @@ +<div class="capabilities-properties-table"> + <ngx-datatable #componentsMetadataTable + columnMode="flex" + [headerHeight]="40" + [rowHeight]="35" + [rows]="capabilitiesProperties" + [sorts]="[{prop: 'name', dir: 'desc'}]"> + <ngx-datatable-column *ngFor="let column of capabilityPropertiesColumns" [ngSwitch]="column.prop" [resizeable]="false" + [draggable]="false" name={{column.name}} [flexGrow]="column.flexGrow"> + <ng-template ngx-datatable-cell-template let-row="row" *ngSwitchCase="'name'"> + <a data-tests-id="row[column.prop]" sdc-tooltip [tooltip-text]="row[column.prop]" (click)="updateProperty(row)">{{row[column.prop]}}</a> + </ng-template> + <ng-template ngx-datatable-cell-template let-row="row" *ngSwitchCase="'schema'"> + <span *ngIf="row[column.prop] && row[column.prop].property" data-tests-id="row[column.prop].property.type" + sdc-tooltip [tooltip-text]="row[column.prop].property.type">{{row[column.prop].property.type}}</span> + </ng-template> + <ng-template ngx-datatable-cell-template let-row="row" *ngSwitchDefault> + <span data-tests-id="row[column.prop]" sdc-tooltip [tooltip-text]="row[column.prop]" [tooltip-placement]="3">{{row[column.prop]}}</span> + </ng-template> + </ngx-datatable-column> + </ngx-datatable> +</div>
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilities-properties/capabilities-properties.less b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilities-properties/capabilities-properties.less new file mode 100644 index 0000000000..007f509538 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilities-properties/capabilities-properties.less @@ -0,0 +1,9 @@ + +:host ::ng-deep { + .ngx-datatable { + > div { + min-height: auto !important; + } + + } +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilities-properties/capabilities-properties.ts b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilities-properties/capabilities-properties.ts new file mode 100644 index 0000000000..2a1a16e265 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilities-properties/capabilities-properties.ts @@ -0,0 +1,33 @@ + +import { ViewChild, Input, OnInit, Component } from "@angular/core"; +import {SdcUiServices} from "onap-ui-angular"; +import { ModalsHandler } from "../../../../../../utils/modals-handler"; +import { WorkspaceService } from "../../../workspace.service"; +import { PropertyModel } from "../../../../../../models/properties"; + + +@Component({ + selector: 'capabilities-properties', + templateUrl: './capabilities-properties.html', + styleUrls: ['./capabilities-properties.less', '../../../../../../../assets/styles/table-style.less'] +}) +export class CapabilitiesPropertiesComponent { + @Input() public capabilitiesProperties: Array<PropertyModel> = []; + + private capabilityPropertiesColumns = [ + {name: 'Name', prop: 'name', flexGrow: 1}, + {name: 'Type', prop: 'type', flexGrow: 1}, + {name: 'Schema', prop: 'schema', flexGrow: 1}, + {name: 'Description', prop: 'description', flexGrow: 1}, + ]; + constructor(private modalsHandler: ModalsHandler, + private workspaceService: WorkspaceService) {} + + private updateProperty(property: PropertyModel): void { + _.forEach(this.capabilitiesProperties, (prop: PropertyModel) => { + prop.readonly = true; + }); + this.modalsHandler.openEditPropertyModal(property, this.workspaceService.metadata, this.capabilitiesProperties, false, 'component', + this.workspaceService.metadata.uniqueId); + } +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilities.component.html b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilities.component.html new file mode 100644 index 0000000000..819eb84849 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilities.component.html @@ -0,0 +1,59 @@ +<div class="capabilities-table"> + <div class="expand-collapse-all-rows"> + <svg-icon class="selected-all-capabilities" + [mode]="'primary'" [clickable]="true" [name]="'expand-o'" + [size]="'medium'" (click)="capabilitiesTable.rowDetail.expandAllRows()"> + </svg-icon> + <svg-icon class="unselected-all-capabilities" + [mode]="'primary'" [clickable]="true" [name]="'minimize-o'" + [size]="'medium'" (click)="capabilitiesTable.rowDetail.collapseAllRows()"> + </svg-icon> + </div> + <ngx-datatable #capabilitiesTable + columnMode="flex" + [headerHeight]="40" + [rowHeight]="35" + [rows]="capabilities" + (select)="onSelectCapabilities($event)" + [selectionType]="'single'"> + <ngx-datatable-row-detail [rowHeight]="undefiend"> + <ng-template let-row="row" ngx-datatable-row-detail-template> + <div class="properties-title">Properties</div> + <capabilities-properties [capabilitiesProperties]="row.properties"></capabilities-properties> + </ng-template> + </ngx-datatable-row-detail> + <ngx-datatable-column name="Name" [flexGrow]="1" [resizeable]="false"> + <ng-template ngx-datatable-cell-template let-row="row" let-expanded="expanded"> + <div class="expand-collapse-cell"> + <svg-icon [clickable]="true" class="expand-collapse-icon" + [name]="expanded ? 'caret1-up-o': 'caret1-down-o'" [mode]="'primary'" + [size]="'medium'" (click)="expendRow(row)"></svg-icon> + <span data-tests-id="row.name" sdc-tooltip [tooltip-text]="row.name" [tooltip-placement]="3" (click)="editCapability(row)">{{row.name}}</span> + </div> + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column name="Type" [flexGrow]="1" [resizeable]="false"> + <ng-template ngx-datatable-cell-template let-row="row"> + <span data-tests-id="row.type" sdc-tooltip [tooltip-text]="row.type" [tooltip-placement]="3">{{row.type ? row.type.replace("tosca.capabilities.",""): ''}}</span> + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column name="Description" [flexGrow]="1" [resizeable]="false"> + <ng-template ngx-datatable-cell-template let-row="row"> + <span data-tests-id="row.description" sdc-tooltip [tooltip-text]="row.description" [tooltip-placement]="3">{{row.description}}</span> + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column name="Valid Source" [flexGrow]="1" [resizeable]="false"> + <ng-template ngx-datatable-cell-template let-row="row"> + <span data-tests-id="row.validSourceTypes.join(',')" sdc-tooltip [tooltip-text]="row.validSourceTypes ? row.validSourceTypes.join(',') : null" [tooltip-placement]="3"> + {{row.validSourceTypes ? row.validSourceTypes.join(','): ''}}</span> + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column name="Occurrences" [flexGrow]="1" [prop]="'minOccurrences'" [resizeable]="false"> + <ng-template ngx-datatable-cell-template let-row="row"> + <span data-tests-id="row.minOccurrences+','+row.maxOccurrences" sdc-tooltip + [tooltip-text]="row.minOccurrences+','+row.maxOccurrences" [tooltip-placement]="3"> + {{row.minOccurrences}},{{row.maxOccurrences}}</span> + </ng-template> + </ngx-datatable-column> + </ngx-datatable> +</div>
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilities.component.less b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilities.component.less new file mode 100644 index 0000000000..0c520a8135 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilities.component.less @@ -0,0 +1,16 @@ +:host ::ng-deep { + .datatable-row-detail { + width: 1260px; + } + .datatable-body-row { + cursor: pointer; + } +} +.expand-collapse-all-rows { + position: absolute; + top: 172px; + left: 890px; +} +.properties-title { + padding-bottom: 10px; +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilities.component.ts b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilities.component.ts new file mode 100644 index 0000000000..02db5d3aee --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilities.component.ts @@ -0,0 +1,79 @@ +import {Capability, CapabilityUI} from "../../../../../models/capability"; +import { ViewChild, Input, OnInit, Component } from "@angular/core"; +import {SdcUiServices} from "onap-ui-angular"; +import {CapabilitiesEditorComponent} from "./capabilityEditor/capabilities-editor.component"; +import {WorkspaceService} from "../../workspace.service"; +import {TopologyTemplateService} from "../../../../services/component-services/topology-template.service"; +import {ReqAndCapabilitiesService} from "../req-and-capabilities.service"; +import {ModalComponent} from "onap-ui-angular/dist/modals/modal.component"; +import {EventListenerService} from "../../../../../services/event-listener-service"; + + +@Component({ + selector: 'capabilities', + templateUrl: './capabilities.component.html', + styleUrls: ['./capabilities.component.less','../../../../../../assets/styles/table-style.less'] +}) +export class CapabilitiesComponent { + @Input() public capabilities: Array<Capability>; + @ViewChild('capabilitiesTable') capabilitiesTable: any; + private customModalInstance: ModalComponent; + + constructor( + private workspaceService: WorkspaceService, + private loaderService: SdcUiServices.LoaderService, + private topologyTemplateService: TopologyTemplateService, + private reqAndCapabilitiesService : ReqAndCapabilitiesService, + private modalService: SdcUiServices.ModalService, + private eventListenerService: EventListenerService) { + } + + private onSelectCapabilities({ selected }) { + } + + editCapability(cap: CapabilityUI) { + let modalConfig = { + size: 'md', + title: 'Update Capability', + type: 'custom', + buttons: [ + { + id: 'saveButton', + text: ('Update'), + size: "'x-small'", + callback: () => this.updateCapability(), + closeModal: true + }, + {text: "Cancel", size: "'x-small'", closeModal: true}] + }; + let modalInputs = { + capability: cap, + capabilityTypesList: this.reqAndCapabilitiesService.getCapabilityTypesList(), + }; + + this.customModalInstance = this.modalService.openCustomModal(modalConfig, CapabilitiesEditorComponent, {input: modalInputs}); + this.customModalInstance.innerModalContent.instance. + onValidationChange.subscribe((isValid) => this.customModalInstance.getButtonById('saveButton').disabled = !isValid); + } + + expendRow(row) { + this.capabilitiesTable.rowDetail.toggleExpandRow(row); + } + + private updateCapability() { + const capability = this.customModalInstance.innerModalContent.instance.capabilityData; + this.loaderService.activate(); + if (capability.uniqueId) { + this.topologyTemplateService.updateCapability(this.workspaceService.metadata.getTypeUrl(), this.workspaceService.metadata.uniqueId, capability).subscribe((result) => { + let index = this.capabilities.findIndex((cap) => result[0].uniqueId === cap.uniqueId); + this.capabilities[index] = new CapabilityUI(result[0], this.workspaceService.metadata.uniqueId); + this.loaderService.deactivate(); + this.eventListenerService.notifyObservers('CAPABILITIES_UPDATED'); + }, () => { + this.loaderService.deactivate(); + }); + } + } + + +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilityEditor/capabilities-editor.component.html b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilityEditor/capabilities-editor.component.html new file mode 100644 index 0000000000..bc15d4d228 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilityEditor/capabilities-editor.component.html @@ -0,0 +1,93 @@ +<div class="capability-editor"> + <form class="w-sdc-form"> + <div class="i-sdc-form-content-capability-content"> + <div class="content-row"> + <div class="i-sdc-form-item"> + <sdc-input + label="{{ 'CAP_NAME' | translate }}" + required="true" + class="i-sdc-form-input" + testId="capName" + [disabled]="isReadonly" + [(value)]="capabilityData.name" + (valueChange)="validityChanged()"> + </sdc-input> + </div> + </div> + + <div class="group-with-border"> + <div class="content-row i-sdc-form-item"> + <sdc-dropdown + label="{{ 'CAP_TYPE' | translate }}" + required="true" + class="i-sdc-form-select" + testId="capType" + [disabled]="isReadonly" + [options]="capabilityTypesMappedList" + selectedOption="{{ capabilityData.type }}" + [placeHolder] = "capabilityData.type" + (changed)="onSelectCapabilityType($event)"> + </sdc-dropdown> + </div> + <div class="content-row i-sdc-form-item"> + <label class="i-sdc-form-label"> {{ 'CAP_DESCRIPTION' | translate }} </label> + <textarea + rows="3" + class="i-sdc-form-input description" + data-tests-id="capDesc" + disabled + value="{{capabilityData.description}}"> + </textarea> + </div> + <div class="content-row i-sdc-form-item"> + <label class="i-sdc-form-label valid-source-label"> {{ 'CAP_VALID_SOURCE' | translate }} </label> + <textarea + rows="2" + class="i-sdc-form-input" + data-tests-id="capValidSrc" + disabled + value="{{capabilityData.validSourceTypes}}"> + </textarea> + </div> + </div> + + <label class="i-sdc-form-label occurrences-label"> {{ 'REQ_CAP_OCCURRENCES' | translate }} </label> + <div class="content-row occurrences-section"> + <div class="min-occurrences-value"> + <sdc-input + label="{{ 'REQ_CAP_OCCURRENCES_MIN' | translate }}" + class="i-sdc-form-input" + testId="capOccurrencesMin" + [disabled]="isReadonly" + [(value)]="capabilityData.minOccurrences" + (valueChange)="validityChanged()" + type="number"> + </sdc-input> + </div> + <div class="sdc-input"> + <label class="sdc-input__label"> {{ 'REQ_CAP_OCCURRENCES_MAX' | translate }} </label> + <div class="max-occurrences-value"> + <sdc-checkbox + class="checkbox-label unbounded-value" + testId="capOccurrencesMaxUnbounded" + label="{{translatedUnboundTxt.toLowerCase()}}" + (checkedChange)="onUnboundedChanged()" + [checked]="isUnboundedChecked" + [disabled]="isReadonly"> + </sdc-checkbox> + + <sdc-input + *ngIf="!isUnboundedChecked" + class="i-sdc-form-input" + testId="capOccurrencesMax" + [disabled]="isReadonly" + [(value)]="capabilityData.maxOccurrences" + (valueChange)="validityChanged()" + type="number"> + </sdc-input> + </div> + </div> + </div> + </div> + </form> +</div>
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilityEditor/capabilities-editor.component.less b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilityEditor/capabilities-editor.component.less new file mode 100644 index 0000000000..324dc6c4d2 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilityEditor/capabilities-editor.component.less @@ -0,0 +1,38 @@ +@import '../../../../../../../assets/styles/variables.less'; + +.capability-editor { + .i-sdc-form-content-capability-content { + padding: 10px 25px; + .group-with-border { + margin: 25px 0; + padding: 15px 0; + border-top: 1px solid @tlv_color_u; + border-bottom: 1px solid @tlv_color_u; + .content-row:not(:last-of-type) { + padding-bottom: 13px; + } + } + + .occurrences-label { + font-family: @font-opensans-bold; + margin-bottom: 19px; + } + .occurrences-section, /deep/ .max-occurrences-value { + display: flex; + .min-occurrences-value { + padding-right: 30px; + } + .unbounded-value { + padding-top: 7px; + padding-right: 20px; + .sdc-checkbox__label { + text-transform: capitalize; + } + } + } + textarea { + min-height: unset; + height: unset; + } + } +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilityEditor/capabilities-editor.component.ts b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilityEditor/capabilities-editor.component.ts new file mode 100644 index 0000000000..3bafa42e0f --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilityEditor/capabilities-editor.component.ts @@ -0,0 +1,81 @@ +import {Component} from '@angular/core'; +import {ServiceServiceNg2} from "app/ng2/services/component-services/service.service"; +import {Capability, CapabilityTypeModel} from 'app/models'; +import {DropdownValue} from "app/ng2/components/ui/form-components/dropdown/ui-element-dropdown.component"; +import {TranslateService} from 'app/ng2/shared/translator/translate.service'; +import {Subject} from "rxjs"; + +@Component({ + selector: 'capabilities-editor', + templateUrl: './capabilities-editor.component.html', + styleUrls: ['./capabilities-editor.component.less'], + providers: [ServiceServiceNg2] +}) + +export class CapabilitiesEditorComponent { + input: { + test: string, + capability: Capability, + capabilityTypesList: Array<CapabilityTypeModel>, + isReadonly: boolean; + }; + capabilityData: Capability; + capabilityTypesMappedList: Array<DropdownValue>; + isUnboundedChecked: boolean; + isReadonly: boolean; + translatedUnboundTxt: string; + + public onValidationChange: Subject<boolean> = new Subject(); + + constructor(private translateService: TranslateService) { + } + + ngOnInit() { + this.capabilityData = new Capability(this.input.capability); + this.translatedUnboundTxt = ''; + this.capabilityData.minOccurrences = this.capabilityData.minOccurrences || 0; + this.translateService.languageChangedObservable.subscribe(lang => { + this.translatedUnboundTxt = this.translateService.translate('REQ_CAP_OCCURRENCES_UNBOUNDED'); + this.capabilityData.maxOccurrences = this.capabilityData.maxOccurrences || this.translatedUnboundTxt; + this.isUnboundedChecked = this.capabilityData.maxOccurrences === this.translatedUnboundTxt; + }); + this.capabilityTypesMappedList = _.map(this.input.capabilityTypesList, capType => new DropdownValue(capType.toscaPresentation.type, capType.toscaPresentation.type)); + this.isReadonly = this.input.isReadonly; + this.validityChanged(); + } + + onUnboundedChanged() { + this.isUnboundedChecked = !this.isUnboundedChecked; + this.capabilityData.maxOccurrences = this.isUnboundedChecked ? this.translatedUnboundTxt : null; + this.validityChanged(); + } + + checkFormValidForSubmit() { + return this.capabilityData.name && this.capabilityData.name.length && + this.capabilityData.type && this.capabilityData.type.length && !_.isEqual(this.capabilityData.minOccurrences, "") && this.capabilityData.minOccurrences >= 0 && + ( + this.isUnboundedChecked || + (this.capabilityData.maxOccurrences && (this.capabilityData.minOccurrences <= parseInt(this.capabilityData.maxOccurrences))) + ); + } + + onSelectCapabilityType(selectedCapType: DropdownValue) { + this.capabilityData.type = selectedCapType && selectedCapType.value; + if (selectedCapType && selectedCapType.value) { + let selectedCapabilityTypeObj: CapabilityTypeModel = this.input.capabilityTypesList.find(capType => capType.toscaPresentation.type === selectedCapType.value); + this.capabilityData.description = selectedCapabilityTypeObj.toscaPresentation.description; + this.capabilityData.validSourceTypes = selectedCapabilityTypeObj.toscaPresentation.validTargetTypes; + this.capabilityData.properties = _.forEach( + _.toArray(selectedCapabilityTypeObj.properties), + prop => prop.uniqueId = null //a requirement for the BE + ); + } + this.validityChanged(); + } + + validityChanged = () => { + let validState = this.checkFormValidForSubmit(); + this.onValidationChange.next(validState); + } + +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilityEditor/capabilities-editor.module.ts b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilityEditor/capabilities-editor.module.ts new file mode 100644 index 0000000000..38b104a0f6 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/capabilities/capabilityEditor/capabilities-editor.module.ts @@ -0,0 +1,29 @@ +import {NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {CapabilitiesEditorComponent} from './capabilities-editor.component'; +import {FormsModule} from '@angular/forms'; +import {FormElementsModule} from 'app/ng2/components/ui/form-components/form-elements.module'; +import {UiElementsModule} from 'app/ng2/components/ui/ui-elements.module'; +import {TranslateModule} from 'app/ng2/shared/translator/translate.module'; +import {SdcUiComponentsModule} from 'onap-ui-angular'; +// import {SdcUiComponentsModule} from "sdc-ui/lib/angular/index"; + +@NgModule({ + declarations: [ + CapabilitiesEditorComponent + ], + imports: [CommonModule, + FormsModule, + FormElementsModule, + UiElementsModule, + TranslateModule, + SdcUiComponentsModule + ], + exports: [], + entryComponents: [ + CapabilitiesEditorComponent + ], + providers: [] +}) +export class CapabilitiesEditorModule { +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/req-and-capabilities.component.html b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/req-and-capabilities.component.html new file mode 100644 index 0000000000..73e0ae52ae --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/req-and-capabilities.component.html @@ -0,0 +1,21 @@ +<div class="workspace-req-and-cap"> + <div> + <span class="addTitle" *ngIf="selectTabName === 'REQUIREMENTS'" (click)="addRequiremnet()">Add Requirement</span> + <span class="addTitle" *ngIf="selectTabName !== 'REQUIREMENTS'" (click)="addCapability()">Add Capability</span> + <span class="req-and-cap-filter" *ngIf="notEmptyTable"> + <sdc-filter-bar + [placeHolder]="'Search'" + (keyup)="updateFilter($event)" + [testId]="'search-box'"> + </sdc-filter-bar> + </span> + </div> + <sdc-tabs (selectedTab)="selectTab($event)" [tabStyle]="'sdc-table-tab'"> + <sdc-tab [title]="'Requirements('+(requirements.length||'0')+')'" [active]="true" [testId]="'req-tab'"> + <div #requirmentsContainer></div> + </sdc-tab> + <sdc-tab [title]="'Capabilities('+(capabilities.length||'0')+')'" [active]="false" [testId]="'cap-tab'"> + <div #capabilitiesContainer></div> + </sdc-tab> + </sdc-tabs> +</div>
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/req-and-capabilities.component.less b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/req-and-capabilities.component.less new file mode 100644 index 0000000000..f3d39cacd6 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/req-and-capabilities.component.less @@ -0,0 +1,19 @@ +.req-and-cap-filter { + width: 336px; + float: right; + margin-right: 10px; +} + +.addTitle { + float: right; + text-transform: uppercase; + font-family: OpenSans-Semibold, sans-serif; + color: #009fdb; + cursor: pointer; +} + +:host ::ng-deep .sdc-tabs { + .sdc-tab-content { + margin-top: 0; + } +} diff --git a/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/req-and-capabilities.component.spec.ts b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/req-and-capabilities.component.spec.ts new file mode 100644 index 0000000000..b7fad045d3 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/req-and-capabilities.component.spec.ts @@ -0,0 +1,127 @@ +import {async, ComponentFixture, TestBed} from "@angular/core/testing"; +import { NO_ERRORS_SCHEMA} from "@angular/core"; +import {ConfigureFn, configureTests} from "../../../../../jest/test-config.helper"; + +import {Observable} from "rxjs/Observable"; +import {NgxDatatableModule} from "@swimlane/ngx-datatable"; +import {SdcUiServices, SdcUiCommon} from "onap-ui-angular"; +import 'rxjs/add/observable/of'; +import {ReqAndCapabilitiesComponent} from "./req-and-capabilities.component"; +import {ReqAndCapabilitiesService} from "./req-and-capabilities.service"; +import {WorkspaceService} from "../workspace.service"; +import { + capabilitiesMock, + filterRequirmentsMock, + requirementMock +} from "../../../../../jest/mocks/req-and-capabilities.mock"; +import {ComponentMetadata} from "../../../../models/component-metadata"; +import { TopologyTemplateService } from "../../../services/component-services/topology-template.service"; +import {EventListenerService} from "../../../../services/event-listener-service"; + +describe('req and capabilities component', () => { + + let fixture: ComponentFixture<ReqAndCapabilitiesComponent>; + let workspaceServiceMock: Partial<WorkspaceService>; + let loaderServiceMock: Partial<SdcUiServices.LoaderService>; + let topologyTemplateServiceMock: Partial<TopologyTemplateService>; + let createDynamicComponentServiceMock: Partial<SdcUiServices.CreateDynamicComponentService> + let reqAndCapabilitiesService: Partial<ReqAndCapabilitiesService>; + let modalService: Partial<SdcUiServices.ModalService>; + let eventListenerService: Partial<EventListenerService>; + + + + beforeEach( + async(() => { + + workspaceServiceMock = { + metadata: new ComponentMetadata() + }; + + topologyTemplateServiceMock = { + getRequirementsAndCapabilitiesWithProperties: jest.fn().mockImplementation(() => + Observable.of({requirements: {'tosca.requirements.Node': requirementMock}, + capabilities: {'tosca.capabilities.Node': capabilitiesMock}})) + }; + + loaderServiceMock = { + activate : jest.fn(), + deactivate: jest.fn() + } + createDynamicComponentServiceMock = { + insertComponentDynamically: jest.fn() + } + + const configure: ConfigureFn = testBed => { + testBed.configureTestingModule({ + declarations: [ReqAndCapabilitiesComponent], + imports: [NgxDatatableModule], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + { provide: WorkspaceService, useValue: workspaceServiceMock }, + { provide: SdcUiServices.LoaderService, useValue: loaderServiceMock }, + { provide: TopologyTemplateService, useValue: topologyTemplateServiceMock }, + { provide: SdcUiServices.CreateDynamicComponentService, useValue: createDynamicComponentServiceMock }, + { provide: ReqAndCapabilitiesService, useValue: reqAndCapabilitiesService }, + { provide: SdcUiServices.ModalService, useValue: modalService }, + { provide: EventListenerService, useValue: eventListenerService } + ], + }); + }; + configureTests(configure).then(testBed => { + fixture = testBed.createComponent(ReqAndCapabilitiesComponent); + }); + }) + ); + + it('should see exactly 2 requirement in requirements table when call initCapabilitiesAndRequirements and meta data requirements null', () => { + workspaceServiceMock.metadata.requirements = null; + fixture.componentInstance.initCapabilitiesAndRequirements(); + expect(workspaceServiceMock.metadata.requirements["tosca.requirements.Node"].length).toBe(3); + }); + it('should see exactly 2 capabilities in capabilities table when call initCapabilitiesAndRequirements and meta data capabilities null', () => { + workspaceServiceMock.metadata.capabilities = null; + fixture.componentInstance.initCapabilitiesAndRequirements(); + expect(workspaceServiceMock.metadata.capabilities["tosca.capabilities.Node"].length).toBe(2); + }); + + it('capabilities array papulated when call populateReqOrCap with capabilities', () => { + workspaceServiceMock.metadata.capabilities = {"tosca.capabilities.Node": capabilitiesMock, "tosca.capabilities.Scalable": capabilitiesMock}; + fixture.componentInstance.populateReqOrCap("capabilities"); + expect(fixture.componentInstance.capabilities.length).toBe(4); + }); + + it('create requirements component when call loadReqOrCap with true', () => { + createDynamicComponentServiceMock.insertComponentDynamically.mockImplementation(() => { return {instance: {requirements: requirementMock}}}); + fixture.componentInstance.requirements = requirementMock; + fixture.componentInstance.loadReqOrCap(true); + expect(fixture.componentInstance.instanceRef.instance.requirements.length).toEqual(3); + }); + + it('create capabilities component when call loadReqOrCap with false', () => { + fixture.componentInstance.instanceRef = {instance: {requirements: null}}; + createDynamicComponentServiceMock.insertComponentDynamically.mockImplementation(() => { return {instance: {capabilities: capabilitiesMock}}}); + fixture.componentInstance.capabilities = capabilitiesMock; + fixture.componentInstance.requirementsUI = filterRequirmentsMock; + let event = { + target : { + value : 'root' + } + } + fixture.componentInstance.updateFilter(event); + expect(fixture.componentInstance.instanceRef.instance.requirements.length).toBe(1); + }); + + it('should filter 1 capabilities when searching and call updateFilter function and instanceRef is capabilities component', () => { + fixture.componentInstance.instanceRef = {instance: {capabilities: null}}; + fixture.componentInstance.capabilities = capabilitiesMock; + fixture.componentInstance.selectTabName = 'CAPABILITIES'; + let event = { + target : { + value : '1source' + } + } + fixture.componentInstance.updateFilter(event); + expect(fixture.componentInstance.instanceRef.instance.capabilities[0].type).toBe("tosca.capabilities.Node"); + }); +}); diff --git a/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/req-and-capabilities.component.ts b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/req-and-capabilities.component.ts new file mode 100644 index 0000000000..69999bfb86 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/req-and-capabilities.component.ts @@ -0,0 +1,229 @@ +import { Component, ComponentRef, OnInit, ViewChild, ViewContainerRef } from '@angular/core'; +import * as _ from 'lodash'; +import { SdcUiServices } from 'onap-ui-angular'; +import { Capability, CapabilityUI } from '../../../../models/capability'; +import { Requirement, RequirementUI } from '../../../../models/requirement'; +import { TopologyTemplateService } from '../../../services/component-services/topology-template.service'; +import { ComponentGenericResponse } from '../../../services/responses/component-generic-response'; +import { WorkspaceService } from '../workspace.service'; +import { CapabilitiesComponent } from './capabilities/capabilities.component'; +import { RequirmentsComponent } from './requirements/requirments.components'; +import {ReqAndCapabilitiesService} from "./req-and-capabilities.service"; +import {CapabilitiesEditorComponent} from "./capabilities/capabilityEditor/capabilities-editor.component"; +import {ModalComponent} from "onap-ui-angular/dist/modals/modal.component"; +import {EventListenerService} from "../../../../services/event-listener-service"; +import {RequirementsEditorComponent} from "./requirements/requirementEditor/requirements-editor.component"; + +@Component({ + selector: 'req-and-capabilities', + templateUrl: './req-and-capabilities.component.html', + styleUrls: ['./req-and-capabilities.component.less'] +}) +export class ReqAndCapabilitiesComponent implements OnInit { + + @ViewChild('requirmentsContainer', { read: ViewContainerRef }) requirmentsContainer: ViewContainerRef; + @ViewChild('capabilitiesContainer', { read: ViewContainerRef }) capabilitiesContainer: ViewContainerRef; + private requirements: Requirement[] = []; + private requirementsUI: RequirementUI[] = []; + private capabilities: Capability[] = []; + private selectTabName: string = 'REQUIREMENTS'; + private notEmptyTable: boolean = true; + private instanceRef: ComponentRef<any>; + private customModalInstance: ModalComponent; + readonly INPUTS_FOR_CAPABILITIES: string = 'INPUTS_FOR_CAPABILITIES'; + readonly INPUTS_FOR_REQUIREMENTS: string = 'INPUTS_FOR_REQUIREMENTS'; + + constructor(private workspaceService: WorkspaceService, + private loaderService: SdcUiServices.LoaderService, + private topologyTemplateService: TopologyTemplateService, + private createDynamicComponentService: SdcUiServices.CreateDynamicComponentService, + private reqAndCapabilitiesService : ReqAndCapabilitiesService, + private modalService: SdcUiServices.ModalService, + private eventListenerService: EventListenerService) { + } + + ngOnInit(): void { + this.initCapabilitiesAndRequirements(); + + this.eventListenerService.registerObserverCallback('CAPABILITIES_UPDATED', () => { + this.loadReqOrCap(); + }); + + this.eventListenerService.registerObserverCallback('REQUIREMENTS_UPDATED', () => { + this.loadReqOrCap(); + }); + } + + + + private initCapabilitiesAndRequirements(): void { + if (!this.workspaceService.metadata.capabilities || !this.workspaceService.metadata.requirements) { + this.loaderService.activate(); + this.topologyTemplateService.getRequirementsAndCapabilitiesWithProperties + (this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId) + .subscribe((response: ComponentGenericResponse) => { + this.workspaceService.metadata.capabilities = response.capabilities; + this.workspaceService.metadata.requirements = response.requirements; + this.initReqOrCap(); + this.loaderService.deactivate(); + }, (error) => { + this.loaderService.deactivate(); + }); + } else { + this.initReqOrCap(); + } + } + + private initReqOrCap() { + this.populateReqOrCap('requirements'); + this.extendRequirementsToRequiremnetsUI(this.requirements); + this.populateReqOrCap('capabilities'); + this.loadReqOrCap(); + } + + private populateReqOrCap(instanceName: string) { + _.forEach(this.workspaceService.metadata[instanceName], (concatArray: any[], name) => { + this[instanceName] = this[instanceName].concat(concatArray); + }); + } + + private updateFilter(event) { + const val = event.target.value.toLowerCase(); + if (this.selectTabName === 'REQUIREMENTS') { + this.instanceRef.instance.requirements = this.requirementsUI.filter((req: Requirement) => { + return !val || this.filterRequirments(req, val); + }); + } else { + this.instanceRef.instance.capabilities = this.capabilities.filter((cap: Capability) => { + return !val || this.filterCapabilities(cap, val); + }); + } + + } + + private selectTab($event) { + this.selectTabName = $event.title.contains('Requirement') ? 'REQUIREMENTS' : 'CATPABILITIES'; + this.loadReqOrCap(); + } + + private async loadReqOrCap() { + if (this.instanceRef) { + this.instanceRef.destroy(); + } + + if (this.selectTabName === 'REQUIREMENTS') { + this.notEmptyTable = this.requirementsUI.length !== 0; + this.instanceRef = this.createDynamicComponentService. + insertComponentDynamically(RequirmentsComponent, {requirements: this.requirementsUI}, this.requirmentsContainer); + // TODO - Keep the initInputs, so it will be called only for the first time - no need to wait to thse API's every time that a user switches tab + await this.reqAndCapabilitiesService.initInputs(this.INPUTS_FOR_REQUIREMENTS); + } else { + this.notEmptyTable = this.capabilities.length !== 0; + this.instanceRef = this.createDynamicComponentService. + insertComponentDynamically(CapabilitiesComponent, {capabilities: this.capabilities}, this.capabilitiesContainer); + // TODO - Keep the initInputs, so it will be called only for the first time - no need to wait to thse API's every time that a user switches tab + await this.reqAndCapabilitiesService.initInputs(this.INPUTS_FOR_CAPABILITIES); + } + } + + private filterCapabilities(capability: Capability, val: string): boolean { + return _.includes([capability.name, capability.description, capability.validSourceTypes.join(), + capability.minOccurrences, capability.maxOccurrences].join('').toLowerCase(), val) || + (capability.type && capability.type.replace('tosca.capabilities.', '').toLowerCase().indexOf(val) !== -1); + } + + private filterRequirments(requirement: Requirement, val: string): boolean { + return _.includes([requirement.name, requirement.minOccurrences, requirement.maxOccurrences].join('').toLowerCase(), val) || + (requirement.capability && requirement.capability.substring('tosca.capabilities.'.length).toLowerCase().indexOf(val) !== -1) || + (requirement.node && requirement.node.substring('tosca.node.'.length).toLowerCase().indexOf(val) !== -1) || + (requirement.relationship && requirement.relationship.substring('tosca.relationship.'.length) + .toLowerCase().indexOf(val) !== -1); + } + + private addCapability() { + let modalConfig = { + size: 'md', + title: 'Add Capability', + type: 'custom', + buttons: [ + { + id: 'saveButton', + text: ('Create'), + size: "'x-small'", + callback: () => this.createCapability(), + closeModal: true + }, + {text: "Cancel", size: "'x-small'", closeModal: true}] + }; + let modalInputs = { + capabilityTypesList: this.reqAndCapabilitiesService.getCapabilityTypesList(), + }; + + this.customModalInstance = this.modalService.openCustomModal(modalConfig, CapabilitiesEditorComponent, {input: modalInputs}); + this.customModalInstance.innerModalContent.instance. + onValidationChange.subscribe((isValid) => this.customModalInstance.getButtonById('saveButton').disabled = !isValid); + } + + private createCapability() { + const capability = this.customModalInstance.innerModalContent.instance.capabilityData; + this.loaderService.activate(); + if (!capability.uniqueId) { + this.topologyTemplateService.createCapability(this.workspaceService.metadata.getTypeUrl(), this.workspaceService.metadata.uniqueId, capability).subscribe((result) => { + this.capabilities.unshift(new CapabilityUI(result[0], this.workspaceService.metadata.uniqueId)); + this.loadReqOrCap(); + this.loaderService.deactivate(); + }, () => { + this.loaderService.deactivate(); + }); + } + } + + private addRequiremnet () { + let modalConfig = { + size: 'md', + title: 'Add Requirement', + type: 'custom', + buttons: [ + { + id: 'saveButton', + text: ('Create'), + size: "'x-small'", + callback: () => this.createRequirement(), + closeModal: true + }, + {text: "Cancel", size: "'x-small'", closeModal: true}] + }; + let modalInputs = { + // requirement: req, + relationshipTypesList: this.reqAndCapabilitiesService.getRelationsShipeTypeList(), + nodeTypesList: this.reqAndCapabilitiesService.getNodeTypesList(), + capabilityTypesList: this.reqAndCapabilitiesService.getCapabilityTypesList(), + // isReadonly: this.$scope.isViewMode() || !this.$scope.isDesigner(), + }; + + this.customModalInstance = this.modalService.openCustomModal(modalConfig, RequirementsEditorComponent, {input: modalInputs}); + this.customModalInstance.innerModalContent.instance. + onValidationChange.subscribe((isValid) => this.customModalInstance.getButtonById('saveButton').disabled = !isValid); + } + + + private createRequirement() { + const requirement = this.customModalInstance.innerModalContent.instance.requirementData; + this.loaderService.activate(); + if (!requirement.uniqueId) { + this.topologyTemplateService.createRequirement(this.workspaceService.metadata.getTypeUrl(), this.workspaceService.metadata.uniqueId, requirement).subscribe(result => { + this.requirementsUI.unshift(new RequirementUI(result[0], this.workspaceService.metadata.uniqueId)); + this.loadReqOrCap(); + this.loaderService.deactivate(); + }, () => { + this.loaderService.deactivate(); + }); + } + } + + private extendRequirementsToRequiremnetsUI(requirements: Requirement[]) { + this.requirements.map((requirement) => { + this.requirementsUI.push(new RequirementUI(requirement, this.workspaceService.metadata.uniqueId)); + }); + } +} diff --git a/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/req-and-capabilities.module.ts b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/req-and-capabilities.module.ts new file mode 100644 index 0000000000..aacb3a5bd1 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/req-and-capabilities.module.ts @@ -0,0 +1,49 @@ +import {NgModule} from "@angular/core"; +import {SdcUiComponentsModule} from "onap-ui-angular"; + +import {NgxDatatableModule} from "@swimlane/ngx-datatable"; +import { ReqAndCapabilitiesComponent } from "./req-and-capabilities.component"; +import { CommonModule } from "@angular/common"; + +import {RequirmentsComponent } from "./requirements/requirments.components"; +import { CapabilitiesComponent } from "./capabilities/capabilities.component"; +import { CapabilitiesPropertiesComponent } from "./capabilities/capabilities-properties/capabilities-properties"; +import {ReqAndCapabilitiesService} from "./req-and-capabilities.service"; +import {RequirementsEditorComponent} from "./requirements/requirementEditor/requirements-editor.component"; +import {CapabilitiesEditorComponent} from "./capabilities/capabilityEditor/capabilities-editor.component"; +import {TranslateModule} from "../../../shared/translator/translate.module"; +import {ToscaTypesServiceNg2} from "../../../services/tosca-types.service"; + +@NgModule({ + declarations: [ + ReqAndCapabilitiesComponent, + CapabilitiesComponent, + RequirmentsComponent, + CapabilitiesPropertiesComponent, + RequirementsEditorComponent, + CapabilitiesEditorComponent + ], + imports: [ + CommonModule, + SdcUiComponentsModule, + NgxDatatableModule, + TranslateModule + ], + exports: [ + ReqAndCapabilitiesComponent, + CapabilitiesComponent, + RequirmentsComponent, + CapabilitiesPropertiesComponent + ], + entryComponents: [ + ReqAndCapabilitiesComponent, + CapabilitiesComponent, + RequirmentsComponent, + CapabilitiesPropertiesComponent, + RequirementsEditorComponent, + CapabilitiesEditorComponent + ], + providers: [ ReqAndCapabilitiesService] +}) +export class reqAndCapabilitiesModule { +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/req-and-capabilities.service.ts b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/req-and-capabilities.service.ts new file mode 100644 index 0000000000..470aac75a6 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/req-and-capabilities.service.ts @@ -0,0 +1,80 @@ +import { Injectable } from "@angular/core"; +import { TopologyTemplateService } from "../../../services/component-services/topology-template.service"; +import { Store } from "@ngxs/store"; +import { SdcUiServices } from "onap-ui-angular"; +import { CapabilityTypeModel } from "../../../../models/capability-types"; +import { RelationshipTypeModel } from "../../../../models/relationship-types"; +import { NodeTypeModel } from "../../../../models/node-types"; +import { WorkspaceService } from "../workspace.service"; +import { ToscaTypesServiceNg2 } from "../../../services/tosca-types.service"; + + + +@Injectable() +export class ReqAndCapabilitiesService { + + private capabilityTypesList: CapabilityTypeModel[]; + private relationshipTypesList: RelationshipTypeModel[]; + private nodeTypesList: NodeTypeModel[]; + private capabilitiesListUpdated: boolean = false; + private requirementsListUpdated: boolean = false; + private nodeTypeListUpdated: boolean = false; + + readonly INPUTS_FOR_REQUIREMENTS: string = 'INPUTS_FOR_REQUIREMENTS'; + readonly INPUTS_FOR_CAPABILITIES: string = 'INPUTS_FOR_CAPABILITIES'; + + constructor( + private workspaceService: WorkspaceService, + private modalService: SdcUiServices.ModalService, + private loaderService: SdcUiServices.LoaderService, + private topologyTemplateService: TopologyTemplateService, + private store: Store, + private toscaTypesServiceNg2: ToscaTypesServiceNg2){} + + public isViewOnly = (): boolean => { + return this.store.selectSnapshot((state) => state.workspace.isViewOnly); + } + + public isDesigner = (): boolean => { + return this.store.selectSnapshot((state) => state.workspace.isDesigner); + } + + public async initInputs(initInputsFor: string) { + + if (!this.capabilitiesListUpdated){ + // -- COMMON for both -- + this.capabilityTypesList = []; + let capabilityTypesResult = await this.toscaTypesServiceNg2.fetchCapabilityTypes(); + Object.keys(capabilityTypesResult).forEach(key => {this.capabilityTypesList.push(capabilityTypesResult[key])}) + this.capabilitiesListUpdated = true; + } + + if (initInputsFor === 'INPUTS_FOR_REQUIREMENTS') { + if (!this.requirementsListUpdated){ + this.relationshipTypesList = []; + let relationshipTypesResult = await this.toscaTypesServiceNg2.fetchRelationshipTypes(); + Object.keys(relationshipTypesResult).forEach(key => {this.relationshipTypesList.push(relationshipTypesResult[key])}); + this.requirementsListUpdated = true; + } + + if (!this.nodeTypeListUpdated){ + this.nodeTypesList = []; + let nodeTypesResult = await this.toscaTypesServiceNg2.fetchNodeTypes(); + Object.keys(nodeTypesResult).forEach(key => {this.nodeTypesList.push(nodeTypesResult[key])}) + this.nodeTypeListUpdated = true; + } + } + } + + getCapabilityTypesList() { + return this.capabilityTypesList; + } + + getRelationsShipeTypeList() { + return this.relationshipTypesList; + } + + getNodeTypesList() { + return this.nodeTypesList; + } +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/requirements/requirementEditor/requirements-editor.component.html b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/requirements/requirementEditor/requirements-editor.component.html new file mode 100644 index 0000000000..330680d3ed --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/requirements/requirementEditor/requirements-editor.component.html @@ -0,0 +1,91 @@ +<div class="requirement-editor"> + <form class="w-sdc-form"> + <div class="i-sdc-form-content-requirement-content"> + <div class="content-row"> + <div class="i-sdc-form-item"> + <sdc-input + label="{{ 'REQ_NAME' | translate}}" + required="true" + testId="reqName" + [disabled]="isReadonly" + [(value)]="requirementData.name" + (valueChange)="validityChanged()"> + </sdc-input> + </div> + </div> + + <div class="group-with-border"> + <div class="content-row i-sdc-form-item"> + <sdc-dropdown + label="{{ 'REQ_RELATED_CAPABILITY' | translate }}" + testId="reqRelatedCapability" + required="true" + [disabled]="isReadonly" + [options]="capabilityTypesMappedList" + selectedOption="{{requirementData.capability}}" + [placeHolder] = "requirementData.capability" + (changed)="onCapabilityChanged($event)"> + </sdc-dropdown> + </div> + <div class="content-row i-sdc-form-item"> + <sdc-dropdown + label="{{ 'REQ_NODE' | translate }}" + testId="reqNode" + [disabled]="isReadonly" + [options]="nodeTypesMappedList" + selectedOption="{{requirementData.node}}" + [placeHolder] = "requirementData.node" + (changed)="onNodeChanged($event)"> + </sdc-dropdown> + </div> + <div class="content-row i-sdc-form-item"> + <sdc-dropdown + label="{{ 'REQ_RELATIONSHIP' | translate }}" + testId="reqRelationship" + [disabled]="isReadonly" + [options]="relationshipTypesMappedList" + selectedOption="{{requirementData.relationship}}" + [placeHolder] = "requirementData.relationship" + (changed)="onRelationshipChanged($event)"> + </sdc-dropdown> + </div> + </div> + + <label class="i-sdc-form-label occurrences-label"> {{ 'REQ_CAP_OCCURRENCES' | translate}} </label> + <div class="content-row occurrences-section"> + <div class="min-occurrences-value"> + <sdc-input + label="{{ 'REQ_CAP_OCCURRENCES_MIN' | translate}}" + testId="reqOccurrencesMin" + [disabled]="isReadonly" + [(value)]="requirementData.minOccurrences" + (valueChange)="validityChanged()" + type="number"> + </sdc-input> + </div> + <div class="sdc-input"> + <label class="sdc-input__label"> {{ 'REQ_CAP_OCCURRENCES_MAX' | translate}} </label> + <div class="max-occurrences-value"> + <sdc-checkbox + class="checkbox-label unbounded-value" + testId="reqOccurrencesMaxUnbounded" + label="{{translatedUnboundTxt.toLowerCase()}}" + (checkedChange)="onUnboundedChanged()" + [checked]="isUnboundedChecked" + [disabled]="isReadonly"> + </sdc-checkbox> + <sdc-input + *ngIf="!isUnboundedChecked" + testId="reqOccurrencesMax" + [disabled]="isReadonly" + [(value)]="requirementData.maxOccurrences" + (valueChange)="validityChanged()" + type="number"> + </sdc-input> + </div> + + </div> + </div> + </div> + </form> +</div>
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/requirements/requirementEditor/requirements-editor.component.less b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/requirements/requirementEditor/requirements-editor.component.less new file mode 100644 index 0000000000..6e50eb79f5 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/requirements/requirementEditor/requirements-editor.component.less @@ -0,0 +1,35 @@ +@import '../../../../../../../assets/styles/variables.less'; + +.requirement-editor { + .i-sdc-form-content-requirement-content { + padding: 10px 25px; + + .group-with-border { + margin: 25px 0; + padding: 15px 0; + border-top: 1px solid @tlv_color_u; + border-bottom: 1px solid @tlv_color_u; + .content-row:not(:last-of-type) { + padding-bottom: 13px; + } + } + + .occurrences-label { + font-family: @font-opensans-bold; + margin-bottom: 19px; + } + .occurrences-section, /deep/ .max-occurrences-value { + display: flex; + .min-occurrences-value { + padding-right: 30px; + } + .unbounded-value { + padding-top: 7px; + padding-right: 20px; + .sdc-checkbox__label { + text-transform: capitalize; + } + } + } + } +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/requirements/requirementEditor/requirements-editor.component.ts b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/requirements/requirementEditor/requirements-editor.component.ts new file mode 100644 index 0000000000..2c5c96f3da --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/requirements/requirementEditor/requirements-editor.component.ts @@ -0,0 +1,90 @@ +import {Component} from '@angular/core'; +import {ServiceServiceNg2} from "app/ng2/services/component-services/service.service"; +import {Requirement, RelationshipTypeModel, NodeTypeModel, CapabilityTypeModel} from 'app/models'; +import {TranslateService} from 'app/ng2/shared/translator/translate.service'; +import {DropdownValue} from "app/ng2/components/ui/form-components/dropdown/ui-element-dropdown.component"; +import {Subject} from "rxjs"; + +@Component({ + selector: 'requirements-editor', + templateUrl: 'requirements-editor.component.html', + styleUrls: ['requirements-editor.component.less'], + providers: [ServiceServiceNg2, TranslateService] +}) + +export class RequirementsEditorComponent { + + input: { + requirement: Requirement, + relationshipTypesList: Array<RelationshipTypeModel>; + nodeTypesList: Array<NodeTypeModel>; + capabilityTypesList: Array<CapabilityTypeModel>; + isReadonly: boolean; + }; + requirementData: Requirement; + capabilityTypesMappedList: Array<DropdownValue>; + relationshipTypesMappedList: Array<DropdownValue>; + nodeTypesMappedList: Array<DropdownValue>; + isUnboundedChecked: boolean; + isReadonly: boolean; + translatedUnboundTxt: string; + + public onValidationChange: Subject<boolean> = new Subject(); + + constructor(private translateService: TranslateService) { + } + + ngOnInit() { + this.requirementData = new Requirement(this.input.requirement); + this.requirementData.minOccurrences = this.requirementData.minOccurrences || 0; + this.translatedUnboundTxt = ''; + this.capabilityTypesMappedList = _.map(this.input.capabilityTypesList, capType => new DropdownValue(capType.toscaPresentation.type, capType.toscaPresentation.type)); + this.relationshipTypesMappedList = _.map(this.input.relationshipTypesList, rType => new DropdownValue(rType.toscaPresentation.type, rType.toscaPresentation.type)); + this.nodeTypesMappedList = _.map(this.input.nodeTypesList, nodeType => { + return new DropdownValue( + nodeType.componentMetadataDefinition.componentMetadataDataDefinition.toscaResourceName, + nodeType.componentMetadataDefinition.componentMetadataDataDefinition.toscaResourceName) + }); + this.translateService.languageChangedObservable.subscribe(lang => { + this.translatedUnboundTxt = this.translateService.translate('REQ_CAP_OCCURRENCES_UNBOUNDED'); + this.requirementData.maxOccurrences = this.requirementData.maxOccurrences || this.translatedUnboundTxt; + this.isUnboundedChecked = this.requirementData.maxOccurrences === this.translatedUnboundTxt; + }); + this.isReadonly = this.input.isReadonly; + this.validityChanged(); + } + + onUnboundedChanged() { + this.isUnboundedChecked = !this.isUnboundedChecked; + this.requirementData.maxOccurrences = this.isUnboundedChecked ? this.translatedUnboundTxt : null; + this.validityChanged(); + } + + onCapabilityChanged(selectedCapability: DropdownValue) { + this.requirementData.capability = selectedCapability && selectedCapability.value; + this.validityChanged(); + } + + onNodeChanged(selectedNode: DropdownValue) { + this.requirementData.node = selectedNode && selectedNode.value; + } + + onRelationshipChanged(selectedRelationship: DropdownValue) { + this.requirementData.relationship = selectedRelationship && selectedRelationship.value; + } + + checkFormValidForSubmit() { + return this.requirementData.name && this.requirementData.name.length && + this.requirementData.capability && this.requirementData.capability.length && !_.isEqual(this.requirementData.minOccurrences, "") && this.requirementData.minOccurrences >= 0 && + ( + this.isUnboundedChecked || + (this.requirementData.maxOccurrences && (this.requirementData.minOccurrences <= parseInt(this.requirementData.maxOccurrences))) + ); + } + + validityChanged = () => { + let validState = this.checkFormValidForSubmit(); + this.onValidationChange.next(validState); + } + +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/requirements/requirementEditor/requirements-editor.module.ts b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/requirements/requirementEditor/requirements-editor.module.ts new file mode 100644 index 0000000000..b1d8db54aa --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/requirements/requirementEditor/requirements-editor.module.ts @@ -0,0 +1,28 @@ +import {NgModule} from "@angular/core"; +import {CommonModule} from "@angular/common"; +import {RequirementsEditorComponent} from "./requirements-editor.component"; +import {FormsModule} from "@angular/forms"; +// import {FormElementsModule} from "../../../components/ui/form-components/form-elements.module"; +import {TranslateModule} from 'app/ng2/shared/translator/translate.module'; +import {SdcUiComponentsModule} from "onap-ui-angular/"; +import {FormElementsModule} from 'app/ng2/components/ui/form-components/form-elements.module'; +// import {SdcUiComponentsModule} from "sdc-ui/lib/angular/index"; + +@NgModule({ + declarations: [ + RequirementsEditorComponent + ], + imports: [CommonModule, + FormsModule, + FormElementsModule, + TranslateModule, + SdcUiComponentsModule + ], + exports: [], + entryComponents: [ + RequirementsEditorComponent + ], + providers: [] +}) +export class RequirementsEditorModule { +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/requirements/requirements.component.less b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/requirements/requirements.component.less new file mode 100644 index 0000000000..19f1c9b55a --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/requirements/requirements.component.less @@ -0,0 +1,4 @@ +/deep/ .importedFromFile { + background-color: #f8f8f8; + color: #959595; + }
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/requirements/requirments.components.html b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/requirements/requirments.components.html new file mode 100644 index 0000000000..7606ed189a --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/requirements/requirments.components.html @@ -0,0 +1,38 @@ +<div class="requirements-table"> + <ngx-datatable #capabilitiesTable + columnMode="flex" + [headerHeight]="40" + [rowHeight]="35" + [rowClass]="getRowClass" + [rows]="requirements"> + <ngx-datatable-column name="Name" [flexGrow]="1" [resizeable]="false" > + <ng-template ngx-datatable-cell-template let-row="row"> + <span [ngStyle]="{'cursor':row.isCreatedManually ? 'pointer' : 'null' }" data-tests-id="row.name" sdc-tooltip [tooltip-text]="row.name" [tooltip-placement]="3" (click)="editRequirement(row)">{{row.name}}</span> + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column name="Capability" [flexGrow]="1" [resizeable]="false"> + <ng-template ngx-datatable-cell-template let-row="row"> + <span data-tests-id="row.capability" sdc-tooltip [tooltip-text]="row.capability" [tooltip-placement]="3">{{row.capability ? row.capability.substring("tosca.capabilities.".length) : ''}}</span> + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column name="Node" [flexGrow]="1" [resizeable]="false"> + <ng-template ngx-datatable-cell-template let-row="row"> + <span data-tests-id="row.node" sdc-tooltip [tooltip-text]="row.node" [tooltip-placement]="3">{{row.node ? row.node.substring("tosca.nodes.".length) : ''}}</span> + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column name="Relationship" [flexGrow]="1" [resizeable]="false"> + <ng-template ngx-datatable-cell-template let-row="row"> + <span data-tests-id="row.relationship" sdc-tooltip [tooltip-text]="row.relationship" [tooltip-placement]="3">{{row.relationship ? row.relationship.substring("tosca.relationships.".length): ''}}</span> + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column name="Connected To" [flexGrow]="1" [resizeable]="false"> + </ngx-datatable-column> + <ngx-datatable-column name="Occurrences" [flexGrow]="1" [prop]="'minOccurrences'" [resizeable]="false"> + <ng-template ngx-datatable-cell-template let-row="row"> + <span data-tests-id="row.minOccurrences+','+row.maxOccurrences" sdc-tooltip + [tooltip-text]="row.minOccurrences+','+row.maxOccurrences" [tooltip-placement]="3"> + {{row.minOccurrences}},{{row.maxOccurrences}}</span> + </ng-template> + </ngx-datatable-column> + </ngx-datatable> +</div>
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/requirements/requirments.components.ts b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/requirements/requirments.components.ts new file mode 100644 index 0000000000..b65489ce4e --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/req-and-capabilities/requirements/requirments.components.ts @@ -0,0 +1,103 @@ +import {Input, Component, OnInit} from "@angular/core"; +import {Requirement, RequirementUI} from "../../../../../models/requirement"; +import {RequirementsEditorComponent} from "./requirementEditor/requirements-editor.component"; +import {WorkspaceService} from "../../workspace.service"; +import {TopologyTemplateService} from "../../../../services/component-services/topology-template.service"; +import {ReqAndCapabilitiesService} from "../req-and-capabilities.service"; +import {EventListenerService} from "../../../../../services/event-listener-service"; +import {ModalComponent} from "onap-ui-angular/dist/modals/modal.component"; +import {SdcUiServices} from "onap-ui-angular"; +import sortedIndexBy = require("lodash/sortedIndexBy"); + +@Component({ + selector: 'requirments', + templateUrl: './requirments.components.html', + styleUrls: ['../../../../../../assets/styles/table-style.less', './requirements.component.less'] +}) + + + +export class RequirmentsComponent implements OnInit { + @Input() public requirements: Array<RequirementUI>; + private customModalInstance: ModalComponent; + + constructor( + private workspaceService: WorkspaceService, + private loaderService: SdcUiServices.LoaderService, + private topologyTemplateService: TopologyTemplateService, + private reqAndCapabilitiesService : ReqAndCapabilitiesService, + private modalService: SdcUiServices.ModalService, + private eventListenerService: EventListenerService) { + } + + + ngOnInit(): void { + let isCreatedManually: RequirementUI[] = []; + let isImportedFromFile: RequirementUI[] = []; + + isCreatedManually = this.requirements.filter((requirement) => requirement.isCreatedManually); + isImportedFromFile = this.requirements.filter((requirement) => !requirement.isCreatedManually); + + this.requirements = []; + + isCreatedManually.map((requirement) => this.requirements.push(requirement)); + isImportedFromFile.map((requirement) => this.requirements.push(requirement)); + + } + + + + editRequirement(req) { + + let modalConfig = { + size: 'md', + title: 'Update Requirement', + type: 'custom', + buttons: [ + { + id: 'saveButton', + text: ('Update'), + size: "'x-small'", + callback: () => this.updateRequirement(), + closeModal: true + }, + {text: "Cancel", size: "'x-small'", closeModal: true}] + }; + let modalInputs = { + requirement: req, + relationshipTypesList: this.reqAndCapabilitiesService.getRelationsShipeTypeList(), + nodeTypesList: this.reqAndCapabilitiesService.getNodeTypesList(), + capabilityTypesList: this.reqAndCapabilitiesService.getCapabilityTypesList(), + // isReadonly: this.$scope.isViewMode() || !this.$scope.isDesigner(), + }; + + this.customModalInstance = this.modalService.openCustomModal(modalConfig, RequirementsEditorComponent, {input: modalInputs}); + this.customModalInstance.innerModalContent.instance. + onValidationChange.subscribe((isValid) => this.customModalInstance.getButtonById('saveButton').disabled = !isValid); + + } + + private updateRequirement() { + const requirement = this.customModalInstance.innerModalContent.instance.requirementData; + this.loaderService.activate(); + if (requirement.uniqueId) { + this.topologyTemplateService.updateRequirement(this.workspaceService.metadata.getTypeUrl(), this.workspaceService.metadata.uniqueId, requirement).subscribe(result => { + let index = this.requirements.findIndex(req => result[0].uniqueId === req.uniqueId); + this.requirements[index] = new RequirementUI(result[0], this.workspaceService.metadata.uniqueId); + this.eventListenerService.notifyObservers('REQUIREMENTS_UPDATED'); + this.loaderService.deactivate(); + }, () => { + this.loaderService.deactivate(); + }); + } + } + + getRowClass(row) { + if (!row.isCreatedManually) { + return { + 'importedFromFile': true + }; + } + } + +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/workspace/tosca-artifacts/__snapshots__/tosca-artifact-page.spec.ts.snap b/catalog-ui/src/app/ng2/pages/workspace/tosca-artifacts/__snapshots__/tosca-artifact-page.spec.ts.snap new file mode 100644 index 0000000000..14146d51d2 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/tosca-artifacts/__snapshots__/tosca-artifact-page.spec.ts.snap @@ -0,0 +1,35 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`tosca artifacts page should match current snapshot of tosca artifact pages component 1`] = ` +<tosca-artifact-page + serviceLoader={[Function Object]} + store={[Function Store]} + table={[Function DatatableComponent]} + workspaceService={[Function Object]} +> + <div + class="tosca-artifact-page" + > + <ngx-datatable + class="ngx-datatable" + columnmode="flex" + > + <div + visibilityobserver="" + > + + <datatable-body + class="datatable-body" + > + <datatable-selection> + + + + </datatable-selection> + </datatable-body> + + </div> + </ngx-datatable> + </div> +</tosca-artifact-page> +`; diff --git a/catalog-ui/src/app/ng2/pages/workspace/tosca-artifacts/tosca-artifact-page.component.html b/catalog-ui/src/app/ng2/pages/workspace/tosca-artifacts/tosca-artifact-page.component.html new file mode 100644 index 0000000000..fece92ee37 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/tosca-artifacts/tosca-artifact-page.component.html @@ -0,0 +1,50 @@ +<div class="tosca-artifact-page"> + <ngx-datatable + columnMode="flex" + [headerHeight]="40" + [rowHeight]="35" + [reorderable]="false" + [swapColumns]="false" + [rows]="toscaArtifacts$ | async" + [sorts]="[{prop: 'artifactDisplayName', dir: 'desc'}]" + #toscaArtifactsTable + (activate)="onActivate($event)"> + <ngx-datatable-row-detail [rowHeight]="80"> + <ng-template let-row="row" let-expanded="expanded" ngx-datatable-row-detail-template> + <div>Label: {{row.artifactLabel}}</div> + <div>UUID: {{row.artifactUUID}}</div> + <div>Description: {{row.description}}</div> + </ng-template> + </ngx-datatable-row-detail> + <ngx-datatable-column [resizeable]="false" name="Name" [flexGrow]="3" + [prop]="'artifactDisplayName'"> + <ng-template ngx-datatable-cell-template let-row="row" let-expanded="expanded"> + <div class="expand-collapse-cell"> + <svg-icon [clickable]="true" class="expand-collapse-icon" + [name]="expanded ? 'caret1-up-o': 'caret1-down-o'" [mode]="'primary'" + [size]="'medium'"></svg-icon> + <span>{{row.artifactDisplayName }}</span> + </div> + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column [resizeable]="false"name="Type" [flexGrow]="3"> + <ng-template ngx-datatable-cell-template let-row="row"> + {{row.artifactType}} + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column [resizeable]="false" name="Version" [flexGrow]="1"> + <ng-template ngx-datatable-cell-template let-row="row"> + {{ row.artifactVersion }} + </ng-template> + </ngx-datatable-column> + <ngx-datatable-column [resizeable]="false"[flexGrow]="1"> + <ng-template ngx-datatable-cell-template let-row="row"> + <div class="download-artifact-button"> + <download-artifact [artifact]="row" [componentId]="componentId" + [componentType]="componentType" + testId="download_{{row.artifactDisplayName}}"></download-artifact> + </div> + </ng-template> + </ngx-datatable-column> + </ngx-datatable> +</div>
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/workspace/tosca-artifacts/tosca-artifact-page.component.less b/catalog-ui/src/app/ng2/pages/workspace/tosca-artifacts/tosca-artifact-page.component.less new file mode 100644 index 0000000000..9c5dd47585 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/tosca-artifacts/tosca-artifact-page.component.less @@ -0,0 +1,7 @@ +.tosca-artifact-page { + .download-artifact-button { + text-align: center; + padding-top: 4px; + + } +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/workspace/tosca-artifacts/tosca-artifact-page.component.ts b/catalog-ui/src/app/ng2/pages/workspace/tosca-artifacts/tosca-artifact-page.component.ts new file mode 100644 index 0000000000..e74e5db668 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/tosca-artifacts/tosca-artifact-page.component.ts @@ -0,0 +1,46 @@ +import {Component, OnInit, ViewChild} from "@angular/core"; +import {WorkspaceService} from "../workspace.service"; +import {SdcUiServices} from "onap-ui-angular"; +import {ArtifactModel} from "../../../../models"; +import {Select, Store} from "@ngxs/store"; +import {WorkspaceState} from "../../../store/states/workspace.state"; +import * as _ from "lodash"; +import {ArtifactGroupType, COMPONENT_FIELDS} from "../../../../utils"; +import {GetArtifactsByTypeAction} from "../../../store/actions/artifacts.action"; +import {Observable} from "rxjs/index"; +import {ArtifactsState} from "../../../store/states/artifacts.state"; +import {ArtifactType} from "../../../../utils/constants"; +import {map} from "rxjs/operators"; + +@Component({ + selector: 'tosca-artifact-page', + + templateUrl: './tosca-artifact-page.component.html', + styleUrls: ['./tosca-artifact-page.component.less', '../../../../../assets/styles/table-style.less'] +}) +export class ToscaArtifactPageComponent implements OnInit { + + @Select(WorkspaceState.isViewOnly) isViewOnly$: boolean; + @ViewChild('toscaArtifactsTable') table: any; + public toscaArtifacts$: Observable<ArtifactModel[]>; + public componentId: string; + public componentType:string; + + constructor(private serviceLoader: SdcUiServices.LoaderService, private workspaceService: WorkspaceService, private store: Store) { + } + + + ngOnInit(): void { + this.componentId = this.workspaceService.metadata.uniqueId; + this.componentType = this.workspaceService.metadata.componentType; + + this.store.dispatch(new GetArtifactsByTypeAction({componentType:this.componentType, componentId:this.componentId, artifactType:ArtifactGroupType.TOSCA})); + this.toscaArtifacts$ = this.store.select(ArtifactsState.getArtifactsByType).pipe(map(filterFn => filterFn(ArtifactGroupType.TOSCA))); + } + + onActivate(event) { + if(event.type === 'click'){ + this.table.rowDetail.toggleExpandRow(event.row); + } + } +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/workspace/tosca-artifacts/tosca-artifact-page.module.ts b/catalog-ui/src/app/ng2/pages/workspace/tosca-artifacts/tosca-artifact-page.module.ts new file mode 100644 index 0000000000..00c7b0b371 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/tosca-artifacts/tosca-artifact-page.module.ts @@ -0,0 +1,28 @@ +import {CommonModule} from "@angular/common"; +import {NgModule} from "@angular/core"; +import {SdcUiComponentsModule} from "onap-ui-angular"; +import {GlobalPipesModule} from "../../../pipes/global-pipes.module"; +import {NgxDatatableModule} from "@swimlane/ngx-datatable"; +import {ToscaArtifactPageComponent} from "./tosca-artifact-page.component"; +import {UiElementsModule} from "../../../components/ui/ui-elements.module"; + +@NgModule({ + declarations: [ + ToscaArtifactPageComponent + ], + imports: [ + CommonModule, + SdcUiComponentsModule, + NgxDatatableModule, + UiElementsModule + ], + exports: [ + ToscaArtifactPageComponent + ], + entryComponents: [ + ToscaArtifactPageComponent + ], + +}) +export class ToscaArtifactPageModule { +} diff --git a/catalog-ui/src/app/ng2/pages/workspace/tosca-artifacts/tosca-artifact-page.spec.ts b/catalog-ui/src/app/ng2/pages/workspace/tosca-artifacts/tosca-artifact-page.spec.ts new file mode 100644 index 0000000000..af3558e15b --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/tosca-artifacts/tosca-artifact-page.spec.ts @@ -0,0 +1,71 @@ +import {async, ComponentFixture, TestBed} from "@angular/core/testing"; +import {NO_ERRORS_SCHEMA} from "@angular/core"; +import {ToscaArtifactPageComponent} from "./tosca-artifact-page.component"; +import {ConfigureFn, configureTests} from "../../../../../jest/test-config.helper"; +import {NgxDatatableModule} from "@swimlane/ngx-datatable"; +import {WorkspaceService} from "../workspace.service"; +import {SdcUiServices} from "onap-ui-angular"; +import {TopologyTemplateService} from "../../../services/component-services/topology-template.service"; +import {Observable} from "rxjs/Observable"; +import {ComponentMetadata} from "../../../../models/component-metadata"; +import 'rxjs/add/observable/of'; +import {NgxsModule, Store} from "@ngxs/store"; +import {ArtifactsState} from "../../../store/states/artifacts.state"; +import {toscaArtifactMock} from "../../../../../jest/mocks/artifacts-mock"; + +describe('tosca artifacts page', () => { + + let fixture: ComponentFixture<ToscaArtifactPageComponent>; + let topologyTemplateServiceMock: Partial<TopologyTemplateService>; + let workspaceServiceMock: Partial<WorkspaceService>; + let loaderServiceMock: Partial<SdcUiServices.LoaderService>; + let store: Store; + + + beforeEach( + async(() => { + + topologyTemplateServiceMock = { + getArtifactsByType: jest.fn().mockImplementation((componentType, id, artifactType) => Observable.of(toscaArtifactMock)) + }; + workspaceServiceMock = {metadata: <ComponentMetadata>{uniqueId: 'service_unique_id', componentType: 'SERVICE'}} + + loaderServiceMock = { + activate : jest.fn(), + deactivate: jest.fn() + } + const configure: ConfigureFn = testBed => { + testBed.configureTestingModule({ + declarations: [ToscaArtifactPageComponent], + imports: [NgxDatatableModule, NgxsModule.forRoot([ArtifactsState])], + schemas: [NO_ERRORS_SCHEMA], + providers: [ + {provide: WorkspaceService, useValue: workspaceServiceMock}, + {provide: TopologyTemplateService, useValue: topologyTemplateServiceMock}, + {provide: SdcUiServices.LoaderService, useValue: loaderServiceMock } + ], + }); + }; + + configureTests(configure).then(testBed => { + fixture = testBed.createComponent(ToscaArtifactPageComponent); + store = testBed.get(Store); + }); + }) + ); + + it('should match current snapshot of tosca artifact pages component', () => { + expect(fixture).toMatchSnapshot(); + }); + + it('should see exactly 2 tosca artifacts', () => { + fixture.componentInstance.ngOnInit(); + fixture.componentInstance.toscaArtifacts$.subscribe((artifacts)=> { + expect(artifacts.length).toEqual(2); + }) + store.selectOnce(state => state.artifacts.toscaArtifacts).subscribe(artifacts => { + expect(artifacts.length).toEqual(9); + }); + }) + +});
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-information-tab.component.ts b/catalog-ui/src/app/ng2/pages/workspace/workspace-ng1-bridge-service.ts index 3639639c88..3d93b459a2 100644 --- a/catalog-ui/src/app/ng2/pages/composition/panel/panel-tabs/policies/policy-information-tab.component.ts +++ b/catalog-ui/src/app/ng2/pages/workspace/workspace-ng1-bridge-service.ts @@ -1,3 +1,6 @@ +/** + * Created by ob0695 on 6/24/2018. + */ /*- * ============LICENSE_START======================================================= * SDC @@ -17,23 +20,18 @@ * limitations under the License. * ============LICENSE_END========================================================= */ +import {Store} from "@ngxs/store"; +import {Injectable} from "@angular/core"; +import {UpdateIsViewOnly} from "../../store/actions/workspace.action"; -import * as _ from "lodash"; -import { Component, Inject, Input, Output, EventEmitter } from "@angular/core"; -import { TranslateService } from './../../../../../shared/translator/translate.service'; -import { PolicyInstance } from 'app/models/graph/zones/policy-instance'; +@Injectable() +export class WorkspaceNg1BridgeService { -@Component({ - selector: 'policy-information-tab', - templateUrl: './policy-information-tab.component.html', - styleUrls: ['./../base/base-tab.component.less'] -}) -export class PolicyInformationTabComponent { - - @Input() policy:PolicyInstance; - @Input() isViewOnly: boolean; + constructor(private store: Store) { + }; - constructor(private translateService:TranslateService) { + public updateIsViewOnly = (isViewOnly: boolean):void => { + this.store.dispatch(new UpdateIsViewOnly(isViewOnly)); } } diff --git a/catalog-ui/src/app/ng2/pages/workspace/workspace.component.ts b/catalog-ui/src/app/ng2/pages/workspace/workspace.component.ts new file mode 100644 index 0000000000..a209406a53 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/workspace.component.ts @@ -0,0 +1,3 @@ +/** + * Created by ob0695 on 6/11/2018. + */ diff --git a/catalog-ui/src/app/ng2/pages/workspace/workspace.module.ts b/catalog-ui/src/app/ng2/pages/workspace/workspace.module.ts new file mode 100644 index 0000000000..cb646379d2 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/workspace.module.ts @@ -0,0 +1,50 @@ +/** + * Created by ob0695 on 6/4/2018. + */ +/** + * Created by ob0695 on 6/4/2018. + */ +import {NgModule} from "@angular/core"; +import {CompositionPageModule} from "../composition/composition-page.module"; + +import {NgxsModule} from "@ngxs/store"; +import {TopologyTemplateService} from "../../services/component-services/topology-template.service"; +import {WorkspaceState} from "../../store/states/workspace.state"; +import {WorkspaceService} from "./workspace.service"; +import {DeploymentPageModule} from "./deployment/deployment-page.module"; +import {ToscaArtifactPageModule} from "./tosca-artifacts/tosca-artifact-page.module"; +import {InformationArtifactPageModule} from "./information-artifact/information-artifact-page.module"; +import { reqAndCapabilitiesModule } from "./req-and-capabilities/req-and-capabilities.module"; +import {AttributesModule} from "./attributes/attributes.module"; +import {ArtifactsState} from "../../store/states/artifacts.state"; +import {InstanceArtifactsState} from "../../store/states/instance-artifacts.state"; +import {DeploymentArtifactsPageModule} from "./deployment-artifacts/deployment-artifacts-page.module"; +import { DistributionModule } from './disribution/distribution.module'; +import { ActivityLogModule } from './activity-log/activity-log.module'; + +@NgModule({ + declarations: [], + imports: [ + DeploymentPageModule, + CompositionPageModule, + AttributesModule, + reqAndCapabilitiesModule, + ToscaArtifactPageModule, + DeploymentArtifactsPageModule, + InformationArtifactPageModule, + DistributionModule, + ActivityLogModule, + NgxsModule.forFeature([WorkspaceState, ArtifactsState, InstanceArtifactsState]) + ], + + exports: [], + entryComponents: [], + providers: [TopologyTemplateService, WorkspaceService] +}) + +export class WorkspaceModule { + + constructor() { + + } +} diff --git a/catalog-ui/src/app/ng2/pages/workspace/workspace.service.ts b/catalog-ui/src/app/ng2/pages/workspace/workspace.service.ts new file mode 100644 index 0000000000..9f985016ec --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/workspace/workspace.service.ts @@ -0,0 +1,70 @@ +/** + * Created by ob0695 on 6/5/2018. + */ +/*- + * ============LICENSE_START======================================================= + * SDC + * ================================================================================ + * Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ============LICENSE_END========================================================= + */ + +/** + * Created by rc2122 on 5/23/2017. + */ +import { Injectable } from '@angular/core'; +import {WorkspaceMode, ComponentState, Role} from "../../../utils/constants"; +import {Component as TopologyTemplate, ComponentMetadata} from "app/models"; +import {CacheService} from "../../services/cache.service"; +import {IComponentMetadata} from "../../../models/component-metadata"; +import {ComponentType} from "../../../utils"; + +@Injectable() +export class WorkspaceService { + + public metadata:ComponentMetadata; + + constructor(private cacheService:CacheService) { + + } + + public setComponentMetadata = (metadata: ComponentMetadata) => { + this.metadata = metadata; + } + + public getMetadataType(): string { + switch (this.metadata.componentType) { + case ComponentType.SERVICE: + return ComponentType.SERVICE; + default: + return this.metadata.resourceType; + } + } + + public getComponentMode = (component:TopologyTemplate):WorkspaceMode => {//return if is edit or view for resource or service + let mode = WorkspaceMode.VIEW; + + let user = this.cacheService.get("user"); + if (component.lifecycleState === ComponentState.NOT_CERTIFIED_CHECKOUT && + component.lastUpdaterUserId === user.userId) { + if ((component.isService() || component.isResource()) && user.role == Role.DESIGNER) { + mode = WorkspaceMode.EDIT; + } + } + return mode; + } +} + +
\ No newline at end of file |