Fix - Declare Output button disabled after saving a default value
[sdc.git] / catalog-ui / src / app / ng2 / pages / attributes-outputs / attributes-outputs.page.component.ts
1 /*-
2  * ============LICENSE_START=======================================================
3  * SDC
4  * ================================================================================
5  * Copyright (C) 2021 Nordix Foundation. All rights reserved.
6  * ================================================================================
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  *
11  *      http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  * ============LICENSE_END=========================================================
19  */
20
21 import {Component, Inject, ViewChild} from '@angular/core';
22 import {
23   ButtonModel,
24   Component as ComponentData,
25   ComponentInstance,
26   ModalModel,
27   ToscaPresentationData
28 } from 'app/models';
29 import {SdcUiCommon, SdcUiServices} from 'onap-ui-angular';
30 import {TopologyTemplateService} from "../../services/component-services/topology-template.service";
31 import {Tab, Tabs} from "../../components/ui/tabs/tabs.component";
32 import * as _ from 'lodash';
33 import {OutputFEModel} from "../../../models/attributes-outputs/output-fe-model";
34 import {OutputBEModel} from "../../../models/attributes-outputs/output-be-model";
35 import {EVENTS, ResourceType, WorkspaceMode} from "../../../utils/constants";
36 import {ComponentModeService} from "../../services/component-services/component-mode.service";
37 import {EventListenerService} from "app/services";
38 import {HierarchyNavService} from "./services/hierarchy-nav.service";
39 import {ComponentServiceNg2} from "../../services/component-services/component.service";
40 import {ComponentInstanceServiceNg2} from "../../services/component-instance-services/component-instance.service";
41 import {KeysPipe} from "../../pipes/keys.pipe";
42 import {
43   InstanceAttributesAPIMap,
44   InstanceBeAttributesMap,
45   InstanceFeAttributesMap
46 } from "app/models/attributes-outputs/attribute-fe-map";
47 import {
48   InstanceBePropertiesMap
49 } from "app/models/properties-inputs/property-fe-map";
50 import {ModalService} from "../../services/modal.service";
51 import {InstanceFeDetails} from "../../../models/instance-fe-details";
52 import {HierarchyDisplayOptions} from "../../components/logic/hierarchy-navigtion/hierarchy-display-options";
53 import {UnsavedChangesComponent} from "../../components/ui/forms/unsaved-changes/unsaved-changes.component";
54 import {SimpleFlatAttribute} from "app/models/attributes-outputs/simple-flat-attribute";
55 import {AttributeFEModel} from "../../../models/attributes-outputs/attribute-fe-model";
56 import {AttributesUtils} from "./services/attributes.utils";
57 import {OutputsUtils} from "app/ng2/pages/attributes-outputs/services/outputs.utils";
58 import {AttributesService} from "app/ng2/services/attributes.service";
59 import {DerivedFEAttribute} from "../../../models/attributes-outputs/derived-fe-attribute";
60 import {AttributeBEModel} from "../../../models/attributes-outputs/attribute-be-model";
61 import {AttributeCreatorComponent} from "app/ng2/pages/attributes-outputs/attribute-creator/attribute-creator.component";
62 import {AttributeRowSelectedEvent} from "app/ng2/components/logic/attributes-table/attributes-table.component";
63 import { DeclareInputComponent } from '../properties-assignment/declare-input/declare-input.component';
64
65 const SERVICE_SELF_TITLE = "SELF";
66
67 @Component({
68   selector: 'attributes-outputs',
69   templateUrl: './attributes-outputs.page.component.html',
70   styleUrls: ['./attributes-outputs.page.component.less', '../../../../assets/styles/table-style.less']
71 })
72 export class AttributesOutputsComponent {
73   title = "Attributes & Outputs";
74
75   @ViewChild('componentAttributesTable')
76   private table: any;
77
78   component: ComponentData;
79   componentInstanceNamesMap: Map<string, InstanceFeDetails> = new Map<string, InstanceFeDetails>();//instanceUniqueId, {name, iconClass}
80
81   attributesNavigationData = [];
82   instancesNavigationData = [];
83
84   instanceFeAttributesMap: InstanceFeAttributesMap;
85   outputs: Array<OutputFEModel> = [];
86   instances: Array<ComponentInstance> = [];
87   searchQuery: string;
88   attributeStructureHeader: string;
89
90   selectedFlatAttribute: SimpleFlatAttribute = new SimpleFlatAttribute();
91   selectedInstanceData: ComponentInstance = null;
92   checkedAttributesCount: number = 0;
93
94   hierarchyAttributesDisplayOptions: HierarchyDisplayOptions = new HierarchyDisplayOptions('path', 'name', 'childrens');
95   hierarchyInstancesDisplayOptions: HierarchyDisplayOptions = new HierarchyDisplayOptions('uniqueId', 'name', 'archived', null, 'iconClass');
96   searchAttributeName: string;
97   currentMainTab: Tab;
98   isOutputsTabSelected: boolean;
99   isAttributesTabSelected: boolean;
100   isReadonly: boolean;
101   resourceIsReadonly: boolean;
102   loadingInstances: boolean = false;
103   loadingOutputs: boolean = false;
104   loadingAttributes: boolean = false;
105   changedData: Array<AttributeFEModel | OutputFEModel>;
106   hasChangedData: boolean;
107   isValidChangedData: boolean;
108   savingChangedData: boolean;
109   stateChangeStartUnregister: Function;
110   serviceBeAttributesMap: InstanceBeAttributesMap;
111
112   @ViewChild('hierarchyNavTabs') hierarchyNavTabs: Tabs;
113   @ViewChild('attributeOutputTabs') attributeOutputTabs: Tabs;
114
115
116   constructor(
117     private attributesService: AttributesService,
118     private hierarchyNavService: HierarchyNavService,
119     private attributesUtils: AttributesUtils,
120     private outputsUtils: OutputsUtils,
121     private componentServiceNg2: ComponentServiceNg2,
122     private componentInstanceServiceNg2: ComponentInstanceServiceNg2,
123     @Inject("$stateParams") _stateParams,
124     @Inject("$scope") private $scope: ng.IScope,
125     @Inject("$state") private $state: ng.ui.IStateService,
126     @Inject("Notification") private Notification: any,
127     private componentModeService: ComponentModeService,
128     private EventListenerService: EventListenerService,
129     private ModalServiceSdcUI: SdcUiServices.ModalService,
130     private ModalService: ModalService,
131     private keysPipe: KeysPipe,
132     private topologyTemplateService: TopologyTemplateService
133   ) {
134
135     this.instanceFeAttributesMap = new InstanceFeAttributesMap();
136     /* This is the way you can access the component data, please do not use any data except metadata, all other data should be received from the new api calls on the first time
137     than if the data is already exist, no need to call the api again - Ask orit if you have any questions*/
138     this.component = _stateParams.component;
139     this.EventListenerService.registerObserverCallback(EVENTS.ON_LIFECYCLE_CHANGE, this.onCheckout);
140     this.updateViewMode();
141
142     this.changedData = [];
143     this.updateHasChangedData();
144     this.isValidChangedData = true;
145   }
146
147   ngOnInit() {
148     this.loadingOutputs = true;
149     this.loadingInstances = true;
150     this.loadingAttributes = true;
151     this.topologyTemplateService
152     .getComponentOutputsWithAttributes(this.component.componentType, this.component.uniqueId)
153     .subscribe(response => {
154       if (response.outputs) {
155         response.outputs.forEach(output => {
156           const newOutput: OutputFEModel = new OutputFEModel(output);
157           this.outputsUtils.resetOutputDefaultValue(newOutput, output.defaultValue);
158           this.outputs.push(newOutput);
159         });
160       }
161     }, error => {
162       this.Notification.error({
163         message: 'Failed to Initialize:' + error,
164         title: 'Failure'
165       });
166     }, () => {
167       this.loadingOutputs = false;
168     });
169     this.componentServiceNg2
170     .getComponentResourceAttributesData(this.component)
171     .subscribe(response => {
172       this.instances = [];
173       this.instances.push(...response.componentInstances);
174
175       // add the service self instance to the top of the list.
176       const serviceInstance = new ComponentInstance();
177       serviceInstance.name = SERVICE_SELF_TITLE;
178       serviceInstance.uniqueId = this.component.uniqueId;
179       this.instances.unshift(serviceInstance);
180       if (this.instances) {
181         this.instances.forEach(instance => {
182           this.instancesNavigationData.push(instance);
183           this.componentInstanceNamesMap[instance.uniqueId] = <InstanceFeDetails>{
184             name: instance.name,
185             iconClass: instance.iconClass,
186             originArchived: instance.originArchived
187           };
188         });
189       }
190       this.selectFirstInstanceByDefault();
191     }, (error) => {
192       this.Notification.error({
193         message: 'Failed to Initialize:' + error,
194         title: 'Failure'
195       });
196     }, () => {
197       this.loadingInstances = false;
198       this.loadingAttributes = false;
199     });
200
201     this.stateChangeStartUnregister = this.$scope.$on('$stateChangeStart', (event, toState, toParams) => {
202       // stop if has changed attributes
203       if (this.hasChangedData) {
204         event.preventDefault();
205         this.showUnsavedChangesAlert().then(() => {
206           this.$state.go(toState, toParams);
207         });
208       }
209     });
210   }
211
212   ngOnDestroy() {
213     this.EventListenerService.unRegisterObserver(EVENTS.ON_LIFECYCLE_CHANGE);
214     this.stateChangeStartUnregister();
215   }
216
217   selectFirstInstanceByDefault = () => {
218     if (this.instancesNavigationData.length > 0) {
219       this.onInstanceSelectedUpdate(this.instancesNavigationData[0]);
220     }
221   };
222
223   updateViewMode = () => {
224     this.isReadonly = this.componentModeService.getComponentMode(this.component) === WorkspaceMode.VIEW;
225   }
226
227   onCheckout = (component: ComponentData) => {
228     this.component = component;
229     this.updateViewMode();
230   }
231
232   isSelf = (): boolean => {
233     return this.selectedInstanceData && this.selectedInstanceData.uniqueId == this.component.uniqueId;
234   }
235
236   getServiceAttributes() {
237     this.loadingAttributes = true;
238     this.topologyTemplateService
239     .getServiceAttributes(this.component.uniqueId)
240     .subscribe((response) => {
241       this.serviceBeAttributesMap = new InstanceBeAttributesMap();
242       this.serviceBeAttributesMap[this.component.uniqueId] = response;
243       this.processInstanceAttributesResponse(this.serviceBeAttributesMap, false, null);
244     }, (error) => {
245       this.Notification.error({
246         message: 'Failed to get Service Attribute:' + error,
247         title: 'Failure'
248       });
249     }, () => {
250       this.loadingAttributes = false;
251     });
252   }
253
254   onInstanceSelectedUpdate = (instance: ComponentInstance) => {
255     // stop if has changed attributes
256     if (this.hasChangedData) {
257       this.showUnsavedChangesAlert().then(() => {
258         this.changeSelectedInstance(instance)
259       });
260       return;
261     }
262     this.changeSelectedInstance(instance);
263   };
264
265   changeSelectedInstance = (instance: ComponentInstance) => {
266     this.selectedInstanceData = instance;
267     this.loadingAttributes = true;
268     this.instanceFeAttributesMap = undefined;
269     if (instance instanceof ComponentInstance) {
270       let instanceBeAttributesMap: InstanceBeAttributesMap | InstanceBePropertiesMap = new InstanceBeAttributesMap();
271       if (this.isOutput(instance.originType)) {
272         this.componentInstanceServiceNg2
273         .getComponentInstanceOutputs(this.component, instance)
274         .subscribe(response => {
275           instanceBeAttributesMap[instance.uniqueId] = response;
276           this.processInstanceAttributesResponse(instanceBeAttributesMap, true, instance.uniqueId);
277         }, error => {
278           this.Notification.error({
279             message: 'Failed to change Selected Instance:' + error,
280             title: 'Failure'
281           });
282         }, () => {
283           this.loadingAttributes = false;
284         });
285       } else if (this.isSelf()) {
286         this.getServiceAttributes();
287       } else {
288         this.componentInstanceServiceNg2
289         .getComponentInstanceAttributes(this.component, instance.uniqueId)
290         .subscribe(response => {
291           instanceBeAttributesMap = new InstanceBeAttributesMap();
292           instanceBeAttributesMap[instance.uniqueId] = response;
293           this.processInstanceAttributesResponse(instanceBeAttributesMap, false, instance.uniqueId);
294         }, error => {
295           this.Notification.error({
296             message: 'Failed to change Selected Instance:' + error,
297             title: 'Failure'
298           });
299         }, () => {
300           this.loadingAttributes = false;
301         });
302         this.componentInstanceServiceNg2
303         .getComponentInstanceProperties(this.component, instance.uniqueId)
304         .subscribe(response => {
305           instanceBeAttributesMap = new InstanceBePropertiesMap();
306           instanceBeAttributesMap[instance.uniqueId] = response;
307           this.processInstanceAttributesResponse(instanceBeAttributesMap, false, instance.uniqueId);
308         }, error => {
309           this.Notification.error({
310             message: 'Failed to change Selected Instance:' + error,
311             title: 'Failure'
312           });
313         }, () => {
314           this.loadingAttributes = false;
315         });
316       }
317       this.resourceIsReadonly = (instance.componentName === "vnfConfiguration");
318     } else {
319       this.loadingAttributes = false;
320     }
321
322     //clear selected attribute from the navigation
323     this.selectedFlatAttribute = new SimpleFlatAttribute();
324     this.attributesNavigationData = [];
325   };
326
327   /**
328    * Entry point handling response from server
329    */
330   processInstanceAttributesResponse = (instanceBeAttributesMap: InstanceBePropertiesMap | InstanceBeAttributesMap, originTypeIsVF: boolean, instanceId: string) => {
331     let attributesMap = this.attributesUtils.convertAttributesMapToFEAndCreateChildren(instanceBeAttributesMap, originTypeIsVF, this.outputs); //create flattened children, disable declared attribs, and init values
332     if (!this.instanceFeAttributesMap) {
333       this.instanceFeAttributesMap = attributesMap;
334     } else if (instanceId) {
335       let toAdd: AttributeFEModel[];
336       if (instanceBeAttributesMap instanceof InstanceBeAttributesMap) {
337         toAdd = this.instanceFeAttributesMap[instanceId].filter(currentAtr => !attributesMap[instanceId].some(newAtr => newAtr.name === currentAtr.name))
338         this.instanceFeAttributesMap[instanceId] = attributesMap[instanceId];
339       } else {
340         toAdd = attributesMap[instanceId].filter(currentAtr => !this.instanceFeAttributesMap[instanceId].some(newAtr => newAtr.name === currentAtr.name))
341       }
342       this.instanceFeAttributesMap[instanceId] = this.instanceFeAttributesMap[instanceId].concat(toAdd);
343     }
344     this.checkedAttributesCount = 0;
345   };
346
347
348   /*** VALUE CHANGE EVENTS ***/
349   dataChanged = (item: AttributeFEModel | OutputFEModel) => {
350     let itemHasChanged;
351     if (this.isAttributesTabSelected && item instanceof AttributeFEModel) {
352       itemHasChanged = item.hasValueObjChanged();
353     } else if (this.isOutputsTabSelected && item instanceof OutputFEModel) {
354       itemHasChanged = item.hasChanged();
355     }
356
357     const dataChangedIdx = this.changedData.findIndex((changedItem) => changedItem === item);
358     if (itemHasChanged) {
359       if (dataChangedIdx === -1) {
360         this.changedData.push(item);
361       }
362     } else {
363       if (dataChangedIdx !== -1) {
364         this.changedData.splice(dataChangedIdx, 1);
365       }
366     }
367
368     if (this.isAttributesTabSelected) {
369       this.isValidChangedData = this.changedData.every((changedItem) => (<AttributeFEModel>changedItem).valueObjIsValid);
370     } else if (this.isOutputsTabSelected) {
371       this.isValidChangedData = this.changedData.every((changedItem) => (<OutputFEModel>changedItem).defaultValueObjIsValid);
372     }
373     this.updateHasChangedData();
374   };
375
376
377   /*** HEIRARCHY/NAV RELATED FUNCTIONS ***/
378
379   /**
380    * Handle select node in navigation area, and select the row in table
381    */
382   onAttributeSelectedUpdate = ($event) => {
383     this.selectedFlatAttribute = $event;
384     let parentAttribute: AttributeFEModel = this.attributesService.getParentAttributeFEModelFromPath(this.instanceFeAttributesMap[this.selectedFlatAttribute.instanceName], this.selectedFlatAttribute.path);
385     parentAttribute.expandedChildAttributeId = this.selectedFlatAttribute.path;
386   };
387
388   /**
389    * When user select row in table, this will prepare the hierarchy object for the tree.
390    */
391   selectAttributeRow = (attributeRowSelectedEvent: AttributeRowSelectedEvent) => {
392     let attribute = attributeRowSelectedEvent.attributeModel;
393     let instanceName = attributeRowSelectedEvent.instanceName;
394     this.attributeStructureHeader = null;
395
396     // Build hierarchy tree for the navigation and update attributesNavigationData with it.
397     if (!(this.selectedInstanceData instanceof ComponentInstance) || this.selectedInstanceData.originType !== ResourceType.VF) {
398       let simpleFlatAttributes: Array<SimpleFlatAttribute>;
399       if (attribute instanceof AttributeFEModel) {
400         simpleFlatAttributes = this.hierarchyNavService.getSimpleAttributesTree(attribute, instanceName);
401       } else if (attribute instanceof DerivedFEAttribute) {
402         // Need to find parent AttributeFEModel
403         let parentAttributeFEModel: AttributeFEModel = _.find(this.instanceFeAttributesMap[instanceName], (tmpFeAttribute): boolean => {
404           return attribute.attributesName.indexOf(tmpFeAttribute.name) === 0;
405         });
406         simpleFlatAttributes = this.hierarchyNavService.getSimpleAttributesTree(parentAttributeFEModel, instanceName);
407       }
408       this.attributesNavigationData = simpleFlatAttributes;
409     }
410
411     // Update the header in the navigation tree with attribute name.
412     this.attributeStructureHeader = (attribute.attributesName.split('#'))[0];
413
414     // Set selected attribute in table
415     this.selectedFlatAttribute = this.hierarchyNavService.createSimpleFlatAttribute(attribute, instanceName);
416     this.hierarchyNavTabs.triggerTabChange('Attribute Structure');
417   };
418
419   tabChanged = (event) => {
420     // stop if has changed attributes
421     if (this.hasChangedData) {
422       this.attributeOutputTabs.triggerTabChange(this.currentMainTab.title);
423       this.showUnsavedChangesAlert().then(() => {
424         this.attributeOutputTabs.selectTab(this.attributeOutputTabs.tabs.find((tab) => tab.title === event.title));
425       });
426       return;
427     }
428
429     this.currentMainTab = this.attributeOutputTabs.tabs.find((tab) => tab.title === event.title);
430     this.isAttributesTabSelected = this.currentMainTab.title === "Attributes";
431     this.isOutputsTabSelected = this.currentMainTab.title === "Outputs";
432     this.attributeStructureHeader = null;
433     this.searchQuery = '';
434   };
435
436   declareOutput = (): void => {
437     if (this.checkedAttributesCount == 1) {
438         this.openAddOutputNameModal();
439     } else if (this.checkedAttributesCount > 1) {
440         this.declareAttributes();
441     }
442   }
443
444   private openAddOutputNameModal = (): void => {
445     const modalTitle = 'Enter name of the ouput to be created';
446     const modalButtons = [];
447     const modal = this.ModalService.createCustomModal(new ModalModel(
448         'sm',
449         modalTitle,
450         null,
451         modalButtons,
452         null /* type */
453     ));
454     modalButtons.push(new ButtonModel('Save', 'blue',
455         () => {
456             const outputName: string = modal.instance.dynamicContent.instance.inputNameForm.value;
457             if (outputName) {
458                 this.declareAttributes(outputName);
459             } else {
460                 this.Notification.warning({
461                     message: 'Failed to set input name',
462                     title: 'Warning'
463                 });
464             }
465             this.ModalService.closeCurrentModal();
466         }
467     ));
468     modalButtons.push(new ButtonModel('Cancel', 'outline grey', () => {
469         this.ModalService.closeCurrentModal();
470     }));
471     this.ModalService.addDynamicContentToModal(modal, DeclareInputComponent, {defaultInputName: this.generateDefaultOutputName()});
472     modal.instance.open();
473   }
474
475   generateDefaultOutputName = (): string => {
476     let defaultInputName: string;
477     let instancesIds = this.keysPipe.transform(this.instanceFeAttributesMap, []);
478     angular.forEach(instancesIds, (instanceId: string) => {
479       const selectedOutput : AttributeBEModel = this.attributesService.getCheckedAttributes(this.instanceFeAttributesMap[instanceId])[0];
480         let selectedInstanceData: any = this.instances.find(instance => instance.uniqueId == instanceId);
481         defaultInputName = selectedOutput.name;
482         if (selectedInstanceData.invariantName) {
483           defaultInputName = selectedInstanceData.invariantName+'_'+selectedOutput.name;
484         }
485     });
486     return defaultInputName;
487   }
488
489   /*** DECLARE ATTRIBUTES/OUTPUTS ***/
490   declareAttributes = (outputName?: string): void => {
491     this.loadingAttributes = true;
492     let selectedComponentInstancesAttributes: InstanceBeAttributesMap = new InstanceBeAttributesMap();
493     let selectedComponentInstancesOutputs: InstanceBeAttributesMap = new InstanceBeAttributesMap();
494     let instancesIds = this.keysPipe.transform(this.instanceFeAttributesMap, []);
495
496     angular.forEach(instancesIds, (instanceId: string): void => {
497       let selectedInstanceData: any = this.instances.find(instance => instance.uniqueId == instanceId);
498       if (selectedInstanceData instanceof ComponentInstance) {
499         if (!this.isOutput(selectedInstanceData.originType)) {
500           // convert Attribute FE model -> Attribute BE model, extract only checked
501           selectedComponentInstancesAttributes[instanceId] = this.attributesService.getCheckedAttributes(this.instanceFeAttributesMap[instanceId]);
502           if (outputName) {
503             selectedComponentInstancesAttributes[instanceId][0].outputName = outputName;
504           }
505         } else {
506           selectedComponentInstancesOutputs[instanceId] = this.attributesService.getCheckedAttributes(this.instanceFeAttributesMap[instanceId]);
507         }
508       }
509     });
510
511     let outputsToCreate: InstanceAttributesAPIMap = new InstanceAttributesAPIMap(selectedComponentInstancesOutputs, selectedComponentInstancesAttributes);
512     this.topologyTemplateService
513     .createOutput(this.component, outputsToCreate, this.isSelf())
514     .subscribe((response) => {
515       this.setOutputTabIndication(response.length);
516       this.checkedAttributesCount = 0;
517       response.forEach((output: OutputBEModel) => {
518         const newOutput: OutputFEModel = new OutputFEModel(output);
519         this.outputsUtils.resetOutputDefaultValue(newOutput, output.defaultValue);
520         this.outputs.push(newOutput);
521         this.updateAttributeValueAfterDeclare(newOutput);
522       });
523       this.loadingAttributes = false;
524     }, error => {this.loadingAttributes = false;}); //ignore error
525   };
526
527   saveChangedData = (): Promise<(AttributeBEModel | OutputBEModel)[]> => {
528     return new Promise((resolve, reject) => {
529       if (!this.isValidChangedData) {
530         reject('Changed data is invalid - cannot save!');
531         return;
532       }
533       if (!this.changedData.length) {
534         resolve([]);
535         return;
536       }
537
538       // make request and its handlers
539       let request;
540       let handleSuccess, handleError;
541       if (this.isAttributesTabSelected) {
542         const changedAttribs = this.changedData.map((changedAttrib) => {
543           changedAttrib = <AttributeFEModel>changedAttrib;
544           const attribBE = new AttributeBEModel(changedAttrib);
545           attribBE.toscaPresentation = new ToscaPresentationData();
546           attribBE.toscaPresentation.ownerId = changedAttrib.parentUniqueId;
547           attribBE.value = changedAttrib.getJSONValue();
548           attribBE.name = changedAttrib.origName || changedAttrib.name;
549           delete attribBE.origName;
550           return attribBE;
551         });
552
553         if (this.selectedInstanceData instanceof ComponentInstance) {
554           if (this.isSelf()) {
555             console.log("changedAttribs", changedAttribs);
556             request = this.topologyTemplateService.updateServiceAttributes(this.component.uniqueId, _.map(changedAttribs, cp => {
557               delete cp.constraints;
558               return cp;
559             }));
560           } else {
561             request = this.componentInstanceServiceNg2
562             .updateInstanceAttributes(this.component.componentType, this.component.uniqueId, this.selectedInstanceData.uniqueId, changedAttribs);
563           }
564           handleSuccess = (response) => {
565             // reset each changed attribute with new value and remove it from changed attributes list
566             response.forEach((resAttrib) => {
567               const changedAttrib = <AttributeFEModel>this.changedData.shift();
568               this.attributesUtils.resetAttributeValue(changedAttrib, resAttrib.value);
569             });
570             resolve(response);
571             console.log("updated instance attributes: ", response);
572           };
573         }
574       } else if (this.isOutputsTabSelected) {
575         const changedOutputs: OutputBEModel[] = this.changedData.map((changedOutput) => {
576           changedOutput = <OutputFEModel>changedOutput;
577           const outputBE = new OutputBEModel(changedOutput);
578           outputBE.defaultValue = changedOutput.getJSONDefaultValue();
579           return outputBE;
580         });
581         request = this.componentServiceNg2.updateComponentOutputs(this.component, changedOutputs);
582         handleSuccess = (response) => {
583           // reset each changed attribute with new value and remove it from changed attributes list
584           response.forEach((resOutput) => {
585             const changedOutput = <OutputFEModel>this.changedData.shift();
586             this.outputsUtils.resetOutputDefaultValue(changedOutput, resOutput.defaultValue);
587             changedOutput.required = resOutput.required;
588           });
589         }
590       }
591
592       this.savingChangedData = true;
593       request.subscribe(
594           (response) => {
595             this.savingChangedData = false;
596             handleSuccess && handleSuccess(response);
597             this.updateHasChangedData();
598             resolve(response);
599           },
600           (error) => {
601             this.savingChangedData = false;
602             handleError && handleError(error);
603             this.updateHasChangedData();
604             reject(error);
605           }
606       );
607
608     });
609   };
610
611
612   reverseChangedData = (): void => {
613     // make reverse item handler
614     let handleReverseItem;
615     if (this.isAttributesTabSelected) {
616       handleReverseItem = (changedItem) => {
617         changedItem = <AttributeFEModel>changedItem;
618         this.attributesUtils.resetAttributeValue(changedItem, changedItem.value);
619         this.checkedAttributesCount = 0;
620       };
621     } else if (this.isOutputsTabSelected) {
622       handleReverseItem = (changedItem) => {
623         changedItem = <OutputFEModel>changedItem;
624         this.outputsUtils.resetOutputDefaultValue(changedItem, changedItem.defaultValue);
625         changedItem.required = changedItem.requiredOrig;
626       };
627     }
628
629     this.changedData.forEach(handleReverseItem);
630     this.changedData = [];
631     this.updateHasChangedData();
632   };
633
634   updateHasChangedData = (): boolean => {
635     const curHasChangedData: boolean = (this.changedData.length > 0);
636     if (curHasChangedData !== this.hasChangedData) {
637       this.hasChangedData = curHasChangedData;
638       if (this.hasChangedData) {
639         this.EventListenerService.notifyObservers(EVENTS.ON_WORKSPACE_UNSAVED_CHANGES, this.hasChangedData, this.showUnsavedChangesAlert);
640       } else {
641         this.EventListenerService.notifyObservers(EVENTS.ON_WORKSPACE_UNSAVED_CHANGES, false);
642       }
643     }
644     return this.hasChangedData;
645   };
646
647   doSaveChangedData = (onSuccessFunction?: Function, onError?: Function): void => {
648     this.saveChangedData().then(
649         () => {
650           this.Notification.success({
651             message: 'Successfully saved changes',
652             title: 'Saved'
653           });
654           if (onSuccessFunction) onSuccessFunction();
655           if (this.isAttributesTabSelected) {
656             this.checkedAttributesCount = 0;
657           }
658           this.instanceFeAttributesMap[this.selectedInstanceData.uniqueId].forEach(prop => prop.isSelected = false);
659           this.hasChangedData = false;
660           this.isValidChangedData = false;
661         },
662         () => {
663           this.Notification.error({
664             message: 'Failed to save changes!',
665             title: 'Failure'
666           });
667           if (onError) onError();
668         }
669     );
670   };
671
672   showUnsavedChangesAlert = (): Promise<any> => {
673     let modalTitle: string;
674     if (this.isAttributesTabSelected) {
675       modalTitle = `Unsaved attributes for ${this.selectedInstanceData.name}`;
676     } else if (this.isOutputsTabSelected) {
677       modalTitle = `Unsaved outputs for ${this.component.name}`;
678     }
679
680     return new Promise<any>((resolve, reject) => {
681       this.ModalServiceSdcUI.openCustomModal(
682           {
683             title: modalTitle,
684             size: 'sm',
685             type: SdcUiCommon.ModalType.custom,
686             testId: "navigate-modal",
687
688             buttons: [
689               {
690                 id: 'cancelButton',
691                 text: 'Cancel',
692                 type: SdcUiCommon.ButtonType.secondary,
693                 size: 'xsm',
694                 closeModal: true,
695                 callback: () => reject()
696               },
697               {
698                 id: 'discardButton',
699                 text: 'Discard',
700                 type: SdcUiCommon.ButtonType.secondary,
701                 size: 'xsm',
702                 closeModal: true,
703                 callback: () => {
704                   this.reverseChangedData();
705                   resolve()
706                 }
707               },
708               {
709                 id: 'saveButton',
710                 text: 'Save',
711                 type: SdcUiCommon.ButtonType.primary,
712                 size: 'xsm',
713                 closeModal: true,
714                 disabled: !this.isValidChangedData,
715                 callback: () => this.doSaveChangedData(resolve, reject)
716               }
717             ] as SdcUiCommon.IModalButtonComponent[]
718           } as SdcUiCommon.IModalConfig, UnsavedChangesComponent, {isValidChangedData: this.isValidChangedData});
719     });
720
721   }
722
723   updateAttributeValueAfterDeclare = (output: OutputFEModel) => {
724     const attributeList = this.instanceFeAttributesMap[output.instanceUniqueId];
725     if (attributeList) {
726       const instanceName = output.instanceUniqueId.slice(output.instanceUniqueId.lastIndexOf('.') + 1);
727       const attributeForUpdatingVal = attributeList.find((feAttribute: AttributeFEModel) => {
728         return feAttribute.name == output.relatedAttributeName &&
729             (feAttribute.name == output.relatedAttributeName || output.name === instanceName.concat('_').concat(feAttribute.name.replace(/[.]/g, '_')));
730       });
731       const outputPath = (output.outputPath && output.outputPath != attributeForUpdatingVal.name) ? output.outputPath : undefined;
732       attributeForUpdatingVal.setAsDeclared(outputPath); //set attribute as declared before assigning value
733       // this.attributesService.disableRelatedAttributes(attributeForUpdatingVal, outputPath);
734       this.attributesUtils.resetAttributeValue(attributeForUpdatingVal, output.relatedAttributeValue, outputPath);
735     }
736   }
737
738   //used for declare button, to keep count of newly checked attributes (and ignore declared attributes)
739   updateCheckedAttributeCount = (increment: boolean): void => {
740     this.checkedAttributesCount += (increment) ? 1 : -1;
741   };
742
743   setOutputTabIndication = (numOutputs: number): void => {
744     this.attributeOutputTabs.setTabIndication('Outputs', numOutputs);
745   };
746
747   deleteOutput = (output: OutputFEModel) => {
748     let outputToDelete = new OutputBEModel(output);
749
750     this.componentServiceNg2
751     .deleteOutput(this.component, outputToDelete)
752     .subscribe(response => {
753       this.outputs = this.outputs.filter(output => output.uniqueId !== response.uniqueId);
754
755       //Reload the whole instance for now - TODO: CHANGE THIS after the BE starts returning attributes within the response, use commented code below instead!
756       this.changeSelectedInstance(this.selectedInstanceData);
757     }, error => {
758       this.Notification.error({
759         message: 'Failed to delete Output:' + error,
760         title: 'Failure'
761       });
762     });
763   };
764
765   deleteAttribute = (attribute: AttributeFEModel) => {
766     const attributeToDelete = new AttributeFEModel(attribute);
767     this.loadingAttributes = true;
768     const feMap = this.instanceFeAttributesMap;
769     this.topologyTemplateService
770     .deleteServiceAttribute(this.component.uniqueId, attributeToDelete)
771     .subscribe((response) => {
772       const attribs = feMap[this.component.uniqueId];
773       attribs.splice(attribs.findIndex(p => p.uniqueId === response), 1);
774     }, (error) => {
775       this.Notification.error({
776         message: 'Failed to delete Attribute:' + error,
777         title: 'Failure'
778       });
779     }, () => {
780       this.loadingAttributes = false;
781     });
782   }
783
784   addAttribute = () => {
785     let modalTitle = 'Add Attribute';
786     let modal = this.ModalService.createCustomModal(new ModalModel(
787         'sm',
788         modalTitle,
789         null,
790         [
791           new ButtonModel('Save', 'blue', () => {
792             modal.instance.dynamicContent.instance.isLoading = true;
793             const newAttribute: AttributeBEModel = modal.instance.dynamicContent.instance.attributeModel;
794             this.topologyTemplateService.createServiceAttribute(this.component.uniqueId, newAttribute)
795             .subscribe((response) => {
796               modal.instance.dynamicContent.instance.isLoading = false;
797               const newAttrib: AttributeFEModel = this.attributesUtils.convertAddAttributeBEToAttributeFE(response);
798               this.instanceFeAttributesMap[this.component.uniqueId].push(newAttrib);
799               modal.instance.close();
800             }, (error) => {
801               modal.instance.dynamicContent.instance.isLoading = false;
802               this.Notification.error({
803                 message: 'Failed to add Attribute:' + error,
804                 title: 'Failure'
805               });
806             });
807           }, () => !modal.instance.dynamicContent.instance.checkFormValidForSubmit()),
808           new ButtonModel('Cancel', 'outline grey', () => {
809             modal.instance.close();
810           }),
811         ],
812         null
813     ));
814     this.ModalService.addDynamicContentToModal(modal, AttributeCreatorComponent, {});
815     modal.instance.open();
816   }
817
818   private isOutput = (instanceType: string): boolean => {
819     return instanceType === ResourceType.VF || instanceType === ResourceType.PNF || instanceType === ResourceType.CVFC || instanceType === ResourceType.CR;
820   }
821
822 }