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/composition/graph | |
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/composition/graph')
72 files changed, 7299 insertions, 0 deletions
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/composition/graph/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 new file mode 100644 index 0000000000..b24e469554 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-properties-view/connection-properties-view.component.html @@ -0,0 +1,20 @@ +<!-- + ~ 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> + <connection-wizard-header currentStepIndex="2"></connection-wizard-header> + <properties-step></properties-step> +</div> diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/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 new file mode 100644 index 0000000000..07f9aa2135 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-properties-view/connection-properties-view.component.less @@ -0,0 +1,4 @@ +connection-wizard-header { + display: block; + margin-bottom: 15px; +} diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/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 new file mode 100644 index 0000000000..5abb879013 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-properties-view/connection-properties-view.component.ts @@ -0,0 +1,10 @@ +import {Component} from "@angular/core"; + + +@Component({ + selector: 'connection-properties-view', + templateUrl: './connection-properties-view.component.html', + styleUrls:['./connection-properties-view.component.less'] +}) +export class ConnectionPropertiesViewComponent { +} diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/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 new file mode 100644 index 0000000000..7e7e82d85f --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard-header/connection-wizard-header.component.html @@ -0,0 +1,52 @@ +<!-- + ~ 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="header-main-container"> + <div class="inner-container"> + <div class="node from-node" [ngClass]="{'selected':currentStepIndex == 0}"> + <div class="text"> + <div class="node-name"> + {{connectWizardService.connectRelationModel.fromNode.componentInstance.name}} + </div> + <div class="selected-req-or-cap" [ngClass]="{'selected': currentStepIndex == 2 && !connectWizardService.selectedMatch.isFromTo}"> + {{getSelectedReqOrCapName(true)}} + </div> + </div> + <div class="icon"> + <div class="small medium {{connectWizardService.connectRelationModel.fromNode.componentInstance.iconSprite}} {{connectWizardService.connectRelationModel.fromNode.componentInstance.icon}}"> + </div> + </div> + </div> + <div class="connection"> + + </div> + <div class="node to-node" [ngClass]="{'selected':currentStepIndex == 1}"> + <div class="icon"> + <div class="small medium {{connectWizardService.connectRelationModel.toNode.componentInstance.iconSprite}} {{connectWizardService.connectRelationModel.toNode.componentInstance.icon}}"> + </div> + </div> + + <div class="text"> + <div class="node-name"> + {{connectWizardService.connectRelationModel.toNode.componentInstance.name}} + </div> + <div class="selected-req-or-cap" [ngClass]="{'selected': currentStepIndex == 2 && connectWizardService.selectedMatch.isFromTo}"> + {{getSelectedReqOrCapName(false)}} + </div> + </div> + </div> + </div> +</div>
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/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 new file mode 100644 index 0000000000..d8bab288d3 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard-header/connection-wizard-header.component.less @@ -0,0 +1,53 @@ +@import '../../../../../../../assets/styles/sprite-proxy-services-icons'; +@import '../../../../../../../assets/styles/variables'; +.header-main-container{ + background-color: #f8f8f8; + width: 100%; + height: 100px; + display: flex; + .inner-container{ + margin: 0 auto; + display: flex; + } +} +.selected { + color: @main_color_a; +} +.node{ + display: flex; + &.from-node{ + text-align: right; + } + &.to-node{ + text-align: left; + } + &.selected{ + .icon{ + border: solid 3px @main_color_a; + padding: 4px; + border-radius: 50%; + background-color: @main_color_p; + } + } + .icon{ + margin: auto 0; + display: flex; + } + .text{ + font-family: @font-opensans-medium; + margin: auto 10px; + min-width: 450px; + .node-name{ + font-size: 11px; + } + .selected-req-or-cap{ + font-size: 14px; + } + } +} +.connection{ + width: 67px; + height: 0px; + border-bottom: dashed 2px #979797; + margin: auto 0; +} diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/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 new file mode 100644 index 0000000000..f5bc3b7ca4 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard-header/connection-wizard-header.component.ts @@ -0,0 +1,37 @@ +/** + * Created by rc2122 on 9/27/2017. + */ +import {Component, Inject, forwardRef} from "@angular/core"; +import {ConnectionWizardService} from "../connection-wizard.service"; +import {WizardHeaderBaseComponent} from "app/ng2/components/ui/multi-steps-wizard/multi-steps-wizard-header-base.component"; + +@Component({ + selector: 'connection-wizard-header', + templateUrl: './connection-wizard-header.component.html', + styleUrls:['./connection-wizard-header.component.less'] +}) + +export class ConnectionWizardHeaderComponent extends WizardHeaderBaseComponent{ + + constructor(@Inject(forwardRef(() => ConnectionWizardService)) public connectWizardService: ConnectionWizardService) { + super(); + } + + private _getReqOrCapName(isFromNode:boolean) { + const attributeReqOrCap:string = isFromNode ? 'requirement' : 'capability'; + if (this.connectWizardService.selectedMatch[attributeReqOrCap]) { + return this.connectWizardService.selectedMatch[attributeReqOrCap].getTitle(); + } else if (this.connectWizardService.selectedMatch.relationship) { + return this.connectWizardService.selectedMatch.relationship.relation[attributeReqOrCap]; + } + return ''; + } + + private getSelectedReqOrCapName = (isFromNode:boolean):string => { + if(!this.connectWizardService.selectedMatch){ + return ''; + } + return this._getReqOrCapName(this.connectWizardService.selectedMatch.isFromTo ? isFromNode : !isFromNode); + } +} + diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard.module.ts b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard.module.ts new file mode 100644 index 0000000000..80464dc970 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard.module.ts @@ -0,0 +1,43 @@ +import {ToNodeStepComponent} from "./to-node-step/to-node-step.component"; +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 {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"; + +@NgModule({ + declarations: [ + FromNodeStepComponent, + ToNodeStepComponent, + PropertiesStepComponent, + ConnectionWizardHeaderComponent, + ConnectionPropertiesViewComponent + ], + imports: [ + FormElementsModule, + PropertyTableModule, + SelectRequirementOrCapabilityModule, + BrowserModule + ], + exports: [ + FromNodeStepComponent, + ToNodeStepComponent, + PropertiesStepComponent, + ConnectionWizardHeaderComponent, + ConnectionPropertiesViewComponent + ], + entryComponents: [FromNodeStepComponent, + ToNodeStepComponent, + PropertiesStepComponent, + ConnectionWizardHeaderComponent, + ConnectionPropertiesViewComponent + ], + providers: [ConnectionWizardService] +}) +export class ConnectionWizardModule { +}
\ No newline at end of file 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/composition/graph/connection-wizard/connection-wizard.service.ts b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard.service.ts new file mode 100644 index 0000000000..2eb5428f61 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/connection-wizard.service.ts @@ -0,0 +1,58 @@ +import * as _ from "lodash"; +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; + selectedMatch:Match; + changedCapabilityProperties:PropertyFEModel[]; + + + constructor(private workspaceService: WorkspaceService) { + this.changedCapabilityProperties = []; + + } + + public setRelationMenuDirectiveObj = (connectRelationModel:ConnectRelationModel) => { + this.connectRelationModel = connectRelationModel; + // this.selectedCapability = rel + } + + getOptionalRequirementsByInstanceUniqueId = (isFromTo: boolean, matchWith?:Capability): Dictionary<Requirement[]> => { + let requirements: Array<Requirement> = []; + _.forEach(this.connectRelationModel.possibleRelations, (match: Match) => { + if(!matchWith || match.capability.uniqueId == matchWith.uniqueId){ + if(match.isFromTo == isFromTo){ + requirements.push(match.requirement); + } + } + }); + requirements = _.uniqBy(requirements, (req:Requirement)=>{ + return req.ownerId + req.uniqueId + req.name; + }); + return _.groupBy(requirements, 'capability'); + } + + getOptionalCapabilitiesByInstanceUniqueId = (isFromTo: boolean, matchWith?:Requirement): Dictionary<Capability[]> => { + let capabilities: Array<Capability> = []; + _.forEach(this.connectRelationModel.possibleRelations, (match: Match) => { + if(!matchWith || match.requirement.uniqueId == matchWith.uniqueId){ + if(match.isFromTo == isFromTo){ + capabilities.push(match.capability); + } + } + }); + capabilities = _.uniqBy(capabilities, (cap:Capability)=>{ + return cap.ownerId + cap.uniqueId + cap.name; + }); + return _.groupBy(capabilities, 'type'); + } +} + 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/composition/graph/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 new file mode 100644 index 0000000000..0a70069748 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/from-node-step/from-node-step.component.html @@ -0,0 +1,22 @@ +<!-- + ~ 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. + --> + +<select-requirement-or-capability [optionalRequirementsMap]="optionalRequirementsMap" + [optionalCapabilitiesMap]="optionalCapabilitiesMap" + [selectedReqOrCapModel]="connectWizardService.selectedMatch && (connectWizardService.selectedMatch.isFromTo ? connectWizardService.selectedMatch.requirement : connectWizardService.selectedMatch.capability)" + [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/composition/graph/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 new file mode 100644 index 0000000000..cffd58c9ea --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/from-node-step/from-node-step.component.ts @@ -0,0 +1,44 @@ +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', + templateUrl: './from-node-step.component.html' +}) + +export class FromNodeStepComponent implements IStepComponent, OnInit{ + + optionalRequirementsMap: Dictionary<Requirement[]>; + optionalCapabilitiesMap: Dictionary<Capability[]>; + + constructor(@Inject(forwardRef(() => ConnectionWizardService)) public connectWizardService: ConnectionWizardService) {} + + ngOnInit() { + this.optionalRequirementsMap = this.connectWizardService.getOptionalRequirementsByInstanceUniqueId(true); + this.optionalCapabilitiesMap = this.connectWizardService.getOptionalCapabilitiesByInstanceUniqueId(false); + } + + preventNext = (): boolean => { + return !this.connectWizardService.selectedMatch || (!this.connectWizardService.selectedMatch.capability && !this.connectWizardService.selectedMatch.requirement); + } + + preventBack = (): boolean => { + return true; + } + + private updateSelectedReqOrCap = (selected: Requirement|Capability): void => { + if (!selected) { + this.connectWizardService.selectedMatch = null; + } 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); + } + } + +} diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/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 new file mode 100644 index 0000000000..a8177595a5 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/properties-step/properties-step.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="title"> + <span class="capability-name"> + {{(connectWizardService.selectedMatch.capability && connectWizardService.selectedMatch.capability.getTitle()) || connectWizardService.selectedMatch.relationship.relation.capability}} + </span> +</div> +<div class="properties-table-container"> + <properties-table class="properties-table" + (propertyChanged)="propertyValueChanged($event)" + [fePropertiesMap]="capabilityPropertiesMap" + [selectedPropertyId]="''" + [hidePropertyType]="true"> + </properties-table> +</div>
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/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 new file mode 100644 index 0000000000..c8ad4d38d2 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/properties-step/properties-step.component.less @@ -0,0 +1,15 @@ +@import '../../../../../../../assets/styles/variables'; +.title{ + margin-bottom: 20px; + .capability-name-label{ + font-size: 13px; + } + .capability-name{ + font-family: @font-opensans-medium; + color: @main_color_a; + } +} +.properties-table-container{ + height: 362px; + overflow-y: auto; +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/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 new file mode 100644 index 0000000000..2c12e0daed --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/properties-step/properties-step.component.ts @@ -0,0 +1,68 @@ +/** + * Created by ob0695 on 9/4/2017. + */ +/** + * Created by rc2122 on 9/4/2017. + */ +import {Component, Inject, forwardRef} from '@angular/core'; +import {IStepComponent} from "app/models" +import {ConnectionWizardService} from "../connection-wizard.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', + templateUrl: './properties-step.component.html', + styleUrls: ['./properties-step.component.less'] +}) + +export class PropertiesStepComponent implements IStepComponent{ + + capabilityPropertiesMap: InstanceFePropertiesMap; + savingProperty:boolean = false; + + constructor(@Inject(forwardRef(() => ConnectionWizardService)) public connectWizardService: ConnectionWizardService, private componentInstanceServiceNg2:ComponentInstanceServiceNg2, private propertiesUtils:PropertiesUtils) { + + this.capabilityPropertiesMap = this.propertiesUtils.convertPropertiesMapToFEAndCreateChildren({'capability' : connectWizardService.selectedMatch.capability.properties}, false); + } + + ngOnInit() { + this.connectWizardService.changedCapabilityProperties = []; + } + + onPropertySelectedUpdate = ($event) => { + console.log("==>" + 'PROPERTY VALUE SELECTED'); + // this.selectedFlatProperty = $event; + // let parentProperty:PropertyFEModel = this.propertiesService.getParentPropertyFEModelFromPath(this.instanceFePropertiesMap[this.selectedFlatProperty.instanceName], this.selectedFlatProperty.path); + // parentProperty.expandedChildPropertyId = this.selectedFlatProperty.path; + }; + + propertyValueChanged = (property: PropertyFEModel) => { + if (!property.isDeclared) { + const propChangedIdx = this.connectWizardService.changedCapabilityProperties.indexOf(property); + if (property.hasValueObjChanged()) { + // if (this.componentInstanceServiceNg2.hasPropertyChanged(property)) { + console.log("==>" + this.constructor.name + ": propertyValueChanged " + property); + if (propChangedIdx === -1) { + this.connectWizardService.changedCapabilityProperties.push(property); + } + } + else { + if (propChangedIdx !== -1) { + console.log("==>" + this.constructor.name + ": propertyValueChanged (reset to original) " + property); + this.connectWizardService.changedCapabilityProperties.splice(propChangedIdx, 1); + } + } + } + }; + + preventNext = ():boolean => { + return false; + } + + preventBack = ():boolean => { + return this.savingProperty; + } +} 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/composition/graph/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 new file mode 100644 index 0000000000..4892b7fadc --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/to-node-step/to-node-step.component.html @@ -0,0 +1,22 @@ +<!-- + ~ 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. + --> +<select-requirement-or-capability [optionalRequirementsMap]="optionalRequirementsMap" + [optionalCapabilitiesMap]="optionalCapabilitiesMap" + [selectedReqOrCapModel]="connectWizardService.selectedMatch.isFromTo ? connectWizardService.selectedMatch.capability : connectWizardService.selectedMatch.requirement" + [selectedReqOrCapOption]="displayRequirementsOrCapabilities" + [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/composition/graph/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 new file mode 100644 index 0000000000..67dc381284 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/connection-wizard/to-node-step/to-node-step.component.ts @@ -0,0 +1,65 @@ +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 "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', + templateUrl: './to-node-step.component.html' +}) + +export class ToNodeStepComponent implements IStepComponent{ + + displayRequirementsOrCapabilities:string; //get 'Requirement' or 'Capability' + optionalRequirementsMap: Dictionary<Requirement[]> = {}; + optionalCapabilitiesMap: Dictionary<Capability[]> ={}; + + constructor(@Inject(forwardRef(() => ConnectionWizardService)) public connectWizardService: ConnectionWizardService) { + } + + ngOnInit(){ + if(this.connectWizardService.selectedMatch.isFromTo){ + this.displayRequirementsOrCapabilities = 'Capability'; + this.optionalRequirementsMap = {}; + this.optionalCapabilitiesMap = this.connectWizardService.getOptionalCapabilitiesByInstanceUniqueId(true, this.connectWizardService.selectedMatch.requirement); + }else{ + this.displayRequirementsOrCapabilities = 'Requirement'; + this.optionalRequirementsMap = this.connectWizardService.getOptionalRequirementsByInstanceUniqueId(false, this.connectWizardService.selectedMatch.capability); + this.optionalCapabilitiesMap = {} + } + + + } + + preventNext = ():boolean => { + return !this.connectWizardService.selectedMatch.capability || !this.connectWizardService.selectedMatch.requirement; + } + + preventBack = ():boolean => { + return false; + } + + private updateSelectedReqOrCap = (selected:Requirement|Capability):void => { + if (!selected) { + if (this.connectWizardService.selectedMatch.isFromTo) { + this.connectWizardService.selectedMatch.capability = undefined; + this.connectWizardService.selectedMatch.toNode = undefined; + } else { + this.connectWizardService.selectedMatch.requirement = undefined; + this.connectWizardService.selectedMatch.fromNode = undefined; + } + } else if (selected instanceof Requirement) { + this.connectWizardService.selectedMatch.requirement = <Requirement>selected; + this.connectWizardService.selectedMatch.fromNode = this.connectWizardService.connectRelationModel.toNode.componentInstance.uniqueId; + } else { + this.connectWizardService.selectedMatch.capability = <Capability>selected; + this.connectWizardService.selectedMatch.toNode = this.connectWizardService.connectRelationModel.toNode.componentInstance.uniqueId; + } + this.connectWizardService.selectedMatch.relationship = undefined; + } + +} 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/composition/graph/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 new file mode 100644 index 0000000000..0abdda1cc6 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/link-row.component.html @@ -0,0 +1,61 @@ +<!-- + ~ 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. + --> + +<ui-element-dropdown + data-tests-id="linkSrc" + [readonly]="!link.isFirst || (link.isFirst && !link.canEdit)" + class="cell link-selector" + [values]="source" + [(value)]="link.fromNode" + (valueChange)="onSourceSelected($event)"> +</ui-element-dropdown> + +<ui-element-dropdown + data-tests-id="linkSrcCP" + [readonly]="!link.isFirst || (link.isFirst && !link.canEdit)" + class="cell link-selector" + [values]="srcCP" + [(value)]="link.fromCP" + (valueChange)="onSrcCPSelected($event)"> +</ui-element-dropdown> + +<ui-element-dropdown + data-tests-id="linkTarget" + [readonly]="!link.canEdit" + class="cell link-selector" + [values]="target" + [(value)]="link.toNode" + (valueChange)="onTargetSelected($event)"> +</ui-element-dropdown> + +<ui-element-dropdown + data-tests-id="linkTargetCP" + [readonly]="!link.canEdit" + class="cell link-selector" + [values]="targetCP" + [(value)]="link.toCP" + (valueChange)="onTargetCPSelected($event)"> +</ui-element-dropdown> + +<div + class="cell remove" + data-tests-id="removeLnk"> + <span + *ngIf="link.canRemove" + class="sprite-new delete-item-icon" + (click)="removeRow()"> + </span> +</div> diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/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 new file mode 100644 index 0000000000..2a1d0d98c8 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/link-row.component.less @@ -0,0 +1,21 @@ +@import './../../../../../../../assets/styles/variables.less'; +.remove { + display: flex; + align-items: center; + justify-content: center; +} + +.cell { + padding: 0; +} + +/deep/ .link-selector { + select { + height: 30px; + border: none; + stroke: none; + } + +} + + 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/composition/graph/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 new file mode 100644 index 0000000000..83c30b1a60 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/link-row.component.ts @@ -0,0 +1,104 @@ +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', + templateUrl: './link-row.component.html', + styleUrls: ['./link-row.component.less'] +}) + + +export class LinkRowComponent { + @Input() data:Array<ServicePathMapItem>; + @Input() link:Link; + @Input() removeRow:Function; + source: Array<DropdownValue> = []; + target: Array<DropdownValue> = []; + srcCP: Array<DropdownValue> = []; + targetCP: Array<DropdownValue> = []; + + ngOnChanges() { + if (this.data) { + this.parseInitialData(this.data); + } + } + + parseInitialData(data: Array<ServicePathMapItem>) { + this.source = this.convertValuesToDropDownOptions(data); + if (this.link.fromNode) { + let srcCPOptions = this.findOptions(data, this.link.fromNode); + if (!srcCPOptions) { return; } + this.srcCP = this.convertValuesToDropDownOptions(srcCPOptions); + if (this.link.fromCP) { + this.target = this.convertValuesToDropDownOptions(data); + if (this.link.toNode) { + let targetCPOptions = this.findOptions(data, this.link.toNode); + if (!targetCPOptions) { return; } + this.targetCP = this.convertValuesToDropDownOptions(targetCPOptions); + } + } + } + } + + private findOptions(items: Array<ServicePathMapItem>, nodeOrCPId: string) { + let item = _.find(items, (dataItem) => nodeOrCPId === dataItem.id); + if (item && item.data && item.data.options) { + return item.data.options; + } + console.warn('no option was found to match selection of Node/CP with id:' + nodeOrCPId); + return null; + } + + private convertValuesToDropDownOptions(values: Array<ServicePathMapItem>): Array<DropdownValue> { + let result:Array<DropdownValue> = []; + for (let i = 0; i < values.length ; i++) { + result[result.length] = new DropdownValue(values[i].id, values[i].data.name); + } + return result.sort((a, b) => a.label.localeCompare(b.label)); + } + + onSourceSelected(id) { + if (id) { + let srcCPOptions = this.findOptions(this.data, id); + this.srcCP = this.convertValuesToDropDownOptions(srcCPOptions); + this.link.fromCP = ''; + this.link.toNode = ''; + this.link.toCP = ''; + this.target = []; + this.targetCP = []; + } + } + + onSrcCPSelected (id) { + if (id) { + let srcCPOptions = this.findOptions(this.data, this.link.fromNode); + let srcCPData = srcCPOptions.find(option => id === option.id).data; + this.target = this.convertValuesToDropDownOptions(this.data); + this.link.fromCPOriginId = srcCPData.ownerId; + this.link.toNode = ''; + this.link.toCP = ''; + this.targetCP = []; + } + + } + + onTargetSelected(id) { + if (id) { + let targetCPOptions = this.findOptions(this.data, id); + this.targetCP = this.convertValuesToDropDownOptions(targetCPOptions); + this.link.toCP = ''; + } + + } + + onTargetCPSelected(id) { + if (id) { + let targetCPOptions = this.findOptions(this.data, this.link.toNode); + let targetCPDataObj = targetCPOptions.find(option => id === option.id).data; + this.link.toCPOriginId = targetCPDataObj.ownerId; + } + } +} diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/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 new file mode 100644 index 0000000000..80128eb42e --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/link-row/link.model.ts @@ -0,0 +1,36 @@ +/*- + * ============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 {ForwardingPathLink} from "app/models/forwarding-path-link"; + +export class Link extends ForwardingPathLink { + public canEdit:boolean = false; + public canRemove:boolean = false; + public isFirst:boolean = false; + + constructor(link: ForwardingPathLink, canEdit: boolean, canRemove: boolean, isFirst: boolean) { + super(link.fromNode,link.fromCP, link.toNode, link.toCP, link.fromCPOriginId, link.toCPOriginId); + this.canEdit = canEdit; + this.canRemove = canRemove; + this.isFirst = isFirst; + } +} + + diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/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 new file mode 100644 index 0000000000..db0d912934 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/service-path-creator.component.html @@ -0,0 +1,55 @@ +<!-- + ~ 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-creator"> + <form class="w-sdc-form"> + <div class="i-sdc-form-item" > + <label class="i-sdc-form-label required">Flow Name</label> + <input type="text" data-tests-id="pathName" name="pathName" [(ngModel)]="forwardingPath.name" [attr.maxLength]="200" /> + </div> + + <div class="side-by-side"> + <div class="i-sdc-form-item" > + <label class="i-sdc-form-label">Protocol</label> + <input type="text" data-tests-id="pathProtocol" name="protocol" [(ngModel)]="forwardingPath.protocol" [attr.maxLength]="200" /> + </div> + <div class="i-sdc-form-item" > + <label class="i-sdc-form-label">Destination Port Numbers</label> + <input type="text" data-tests-id="pathPortNumbers" name="portNumbers" [(ngModel)]="forwardingPath.destinationPortNumber" pattern="[0-9,]*" /> + </div> + </div> + + <div class="separator-buttons"> + <span class="based-on-title">Based On</span> + <a (click)="addRow()" [ngClass]="{'disabled':!isExtendAllowed()}" data-tests-id="extendPathlnk">Extend Flow</a> + </div> + + <div class="generic-table"> + <div class="header-row"> + <div class="cell header-cell" *ngFor="let header of headers"> + {{header}} + </div> + </div> + <div *ngIf="links && links.length === 0" class="no-row-text" > + There is no data to display + </div> + <div> + <link-row *ngFor="let link of links" [data]="linksMap" [link]="link" [removeRow]="removeRow" class="data-row" ></link-row> + </div> + </div> + + + </form> +</div>
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/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 new file mode 100644 index 0000000000..2a3efbdd3c --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/service-path-creator.component.less @@ -0,0 +1,45 @@ +@import './../../../../../../assets/styles/variables.less'; +.service-path-creator { + font-family: @font-opensans-regular; + .separator-buttons { + margin: 10px 0; + display: flex; + justify-content: space-between; + } + .i-sdc-form-label { + font-size: 12px; + } + .w-sdc-form .i-sdc-form-item { + margin-bottom: 15px; + } + + .side-by-side { + display: flex; + .i-sdc-form-item { + flex-basis: 100%; + &:first-child { + margin-right: 10px; + } + } + } + + .generic-table { + max-height: 233px; + .header-row .header-cell { + &:last-child { + padding: 0; + } + } + /deep/ .cell { + &:last-child { + min-width: 30px; + } + } + } + + .based-on-title { + text-transform: uppercase; + font-size: 18px; + font-family: @font-opensans-regular; + } +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/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 new file mode 100644 index 0000000000..17c2081a75 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/service-path-creator.component.ts @@ -0,0 +1,149 @@ +/*- + * ============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, ElementRef, forwardRef, Inject } from '@angular/core'; +import {Link} from './link-row/link.model'; +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', + templateUrl: './service-path-creator.component.html', + styleUrls:['./service-path-creator.component.less'], + providers: [ServiceServiceNg2] +}) + +export class ServicePathCreatorComponent { + + linksMap:Array<ServicePathMapItem>; + links:Array<Link> = []; + input:any; + headers: Array<string> = []; + removeRow: Function; + forwardingPath:ForwardingPath; + //isExtendAllowed:boolean = false; + + 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', ' ']; + this.removeRow = () => { + if (this.links.length === 1) { + return; + } + this.links.splice(this.links.length-1, 1); + this.enableCurrentRow(); + }; + } + + ngOnInit() { + this.serviceService.getNodesAndLinksMap(this.input.serviceId).subscribe((res:any) => { + this.linksMap = res; + }); + this.processExistingPath(); + + } + + private processExistingPath() { + if (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; + this.forwardingPath.uniqueId = forwardingPath.uniqueId; + this.links = []; + _.forEach(forwardingPath.pathElements.listToscaDataDefinition, (link:ForwardingPathLink) => { + this.links[this.links.length] = new Link(link, false, false, false); + }); + this.links[this.links.length - 1].canEdit = true; + this.links[this.links.length - 1].canRemove = true; + this.links[0].isFirst = true; + } + } + + isExtendAllowed():boolean { + if (this.links[this.links.length-1].toCP) { + return true; + } + return false; + } + + enableCurrentRow() { + this.links[this.links.length-1].canEdit = true; + if (this.links.length !== 1) { + this.links[this.links.length-1].canRemove = true; + } + } + + addRow() { + this.disableRows(); + this.links[this.links.length] = new Link( + new ForwardingPathLink(this.links[this.links.length-1].toNode, + this.links[this.links.length-1].toCP, + '', + '', + this.links[this.links.length-1].toCPOriginId, + '' + ), + true, + true, + false + ); + } + + disableRows() { + for (let i = 0 ; i < this.links.length ; i++) { + this.links[i].canEdit = false; + this.links[i].canRemove = false; + } + } + + createPathLinksObject() { + for (let i = 0 ; i < this.links.length ; i++) { + let link = this.links[i]; + this.forwardingPath.addPathLink(link.fromNode, link.fromCP, link.toNode, link.toCP, link.fromCPOriginId, link.toCPOriginId); + } + } + + createServicePathData() { + this.createPathLinksObject(); + return this.forwardingPath; + } + + checkFormValidForSubmit():boolean { + if (this.forwardingPath.name && this.isPathValid() ) { + return true; + } + return false; + } + + isPathValid():boolean { + let lastLink = this.links[this.links.length -1] ; + if (lastLink.toNode && lastLink.toCP && lastLink.fromNode && lastLink.fromCP) { + return true; + } + return false; + } +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/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 new file mode 100644 index 0000000000..78005317a2 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-path-creator/service-path-creator.module.ts @@ -0,0 +1,25 @@ +import { NgModule } from "@angular/core"; +import {CommonModule} from "@angular/common"; +import {ServicePathCreatorComponent} from "./service-path-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 {LinkRowComponent} from './link-row/link-row.component' +@NgModule({ + declarations: [ + ServicePathCreatorComponent, + LinkRowComponent + ], + imports: [CommonModule, + FormsModule, + FormElementsModule, + UiElementsModule + ], + exports: [], + entryComponents: [ + ServicePathCreatorComponent + ], + providers: [] +}) +export class ServicePathCreatorModule { +}
\ No newline at end of file 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/composition/graph/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 new file mode 100644 index 0000000000..39c41916a2 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-paths-list/service-paths-list.component.html @@ -0,0 +1,21 @@ +<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" > + <div class="header-row"> + <div class="cell header-cell" *ngFor="let header of headers"> + {{header}} + </div> + </div> + <div *ngFor="let path of paths" class="data-row" > + <div class="cell" data-tests-id="path-name" >{{path.name}}</div> + <div class="cell path-action-buttons"> + <span class="sprite-new update-component-icon" (click)="onEditServicePath(path.uniqueId)" data-tests-id="update-service-path-btn" ></span> + <span class="sprite-new delete-item-icon" *ngIf="!isViewOnly" (click)="deletePath(path.uniqueId)" data-tests-id="delete-service-path-btn"></span> + </div> + </div> + <div *ngIf="paths && paths.length === 0" class="no-row-text" > + No flows have been added yet. + </div> + </div> + +</div>
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/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 new file mode 100644 index 0000000000..17f70926ff --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-paths-list/service-paths-list.component.less @@ -0,0 +1,24 @@ +@import './../../../../../../assets/styles/variables.less'; + +.add-path-link { + display: flex; + align-items: flex-end; + flex-direction: column; + padding-bottom: 10px; +} + +.generic-table { + max-height: 233px; +} + +.path-action-buttons { + display: flex; + align-items: center; + justify-content: space-between; + .sprite-new { + cursor: pointer; + } + & > span:only-child { + margin: auto; +} +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/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 new file mode 100644 index 0000000000..81abe42cb3 --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-paths-list/service-paths-list.component.ts @@ -0,0 +1,70 @@ +/*- + * ============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, ComponentRef} from '@angular/core'; +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', + templateUrl: './service-paths-list.component.html', + styleUrls:['service-paths-list.component.less'], + providers: [ServiceServiceNg2, ModalService] +}) +export class ServicePathsListComponent { + modalInstance: ComponentRef<ModalComponent>; + headers: Array<string> = []; + paths: Array<ForwardingPath> = []; + input:any; + onAddServicePath: Function; + onEditServicePath: Function; + isViewOnly: boolean; + + constructor(private serviceService:ServiceServiceNg2, + private compositionService: CompositionService) { + this.headers = ['Flow Name','Actions']; + } + + ngOnInit() { + _.forEach(this.compositionService.forwardingPaths, (path: ForwardingPath)=> { + this.paths[this.paths.length] = path; + }); + this.paths.sort((a:ForwardingPath, b:ForwardingPath)=> { + return a.name.localeCompare(b.name); + }); + this.onAddServicePath = this.input.onCreateServicePath; + this.onEditServicePath = this.input.onEditServicePath; + this.isViewOnly = this.input.isViewOnly; + } + + deletePath = (id:string):void => { + 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; + }); + }); + }; + +}
\ No newline at end of file diff --git a/catalog-ui/src/app/ng2/pages/composition/graph/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 new file mode 100644 index 0000000000..5121627a9d --- /dev/null +++ b/catalog-ui/src/app/ng2/pages/composition/graph/service-paths-list/service-paths-list.module.ts @@ -0,0 +1,17 @@ +import { NgModule } from "@angular/core"; +import {CommonModule} from "@angular/common"; +import { ServicePathsListComponent } from "./service-paths-list.component"; + +@NgModule({ + declarations: [ + ServicePathsListComponent + ], + imports: [CommonModule], + exports: [], + entryComponents: [ + ServicePathsListComponent + ], + providers: [] +}) +export class ServicePathsListModule { +}
\ No newline at end of file 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 + } + + } + }] + } + }); + } + +} |