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/canvas-zone/zone-instance | |
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/canvas-zone/zone-instance')
4 files changed, 422 insertions, 0 deletions
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 |