Increase character limit of VFC property name to 100 and allow @ character
[sdc.git] / catalog-ui / src / app / view-models / forms / property-forms / component-property-form / property-form-view-model.ts
1 /*-
2  * ============LICENSE_START=======================================================
3  * SDC
4  * ================================================================================
5  * Copyright (C) 2017 AT&T Intellectual Property. 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 'use strict';
22 import * as _ from "lodash";
23 import {FormState, PROPERTY_DATA, PROPERTY_TYPES, PROPERTY_VALUE_CONSTRAINTS, ValidationUtils} from "app/utils";
24 import {DataTypesService} from "app/services";
25 import {DataTypesMap, PropertyModel} from "app/models";
26 import {ComponentInstance} from "../../../../models/componentsInstances/componentInstance";
27 import {ComponentInstanceServiceNg2} from "app/ng2/services/component-instance-services/component-instance.service";
28 import {SdcUiCommon, SdcUiComponents, SdcUiServices} from "onap-ui-angular";
29 import {CompositionService} from "app/ng2/pages/composition/composition.service";
30 import {WorkspaceService} from "app/ng2/pages/workspace/workspace.service";
31 import {Observable} from "rxjs";
32 import {TopologyTemplateService} from "app/ng2/services/component-services/topology-template.service";
33 import {InstanceFeDetails} from "../../../../models/instance-fe-details";
34 import {ToscaGetFunction} from "../../../../models/tosca-get-function";
35 import {ToscaFunctionValidationEvent} from "../../../../ng2/pages/properties-assignment/tosca-function/tosca-function.component";
36
37 export interface IEditPropertyModel {
38     property:PropertyModel;
39     types:Array<string>;
40     simpleTypes:Array<string>;
41     hasGetFunctionValue: boolean;
42     isGetFunctionValid: boolean;
43 }
44
45 interface IPropertyFormViewModelScope extends ng.IScope {
46     forms:any;
47     editForm:ng.IFormController;
48     footerButtons:Array<any>;
49     isNew:boolean;
50     nameMaxLength:number;
51     isLoading:boolean;
52     componentMetadata: { isService: boolean, isVfc: boolean }
53     validationPattern:RegExp;
54     propertyNameValidationPattern:RegExp;
55     commentValidationPattern:RegExp;
56     editPropertyModel: IEditPropertyModel;
57     componentInstanceMap: Map<string, InstanceFeDetails>;
58     modalInstanceProperty:ng.ui.bootstrap.IModalServiceInstance;
59     currentPropertyIndex:number;
60     isLastProperty:boolean;
61     myValue:any;
62     nonPrimitiveTypes:Array<string>;
63     dataTypes:DataTypesMap;
64     isTypeDataType:boolean;
65     maxLength:number;
66     isViewOnly:boolean;
67     isPropertyValueOwner:boolean;
68     isVnfConfiguration:boolean;
69     constraints:string[];
70     modelNameFilter:string;
71     isGetFunctionValueType: boolean;
72
73     validateJson(json:string):boolean;
74     save(doNotCloseModal?:boolean):void;
75     getValidationPattern(type:string):RegExp;
76     validateIntRange(value:string):boolean;
77     close():void;
78     onValueChange():void;
79     onSchemaTypeChange():void;
80     onTypeChange(resetSchema:boolean):void;
81     showSchema():boolean;
82     delete(property:PropertyModel):void;
83     getPrev():void;
84     getNext():void;
85     isSimpleType(typeName:string):boolean;
86     getDefaultValue():any;
87     onValueTypeChange(): void;
88 }
89
90 export class PropertyFormViewModel {
91
92     static '$inject' = [
93         '$scope',
94         'Sdc.Services.DataTypesService',
95         '$uibModalInstance',
96         'property',
97         'ValidationPattern',
98         'PropertyNameValidationPattern',
99         'CommentValidationPattern',
100         'ValidationUtils',
101         // 'component',
102         '$filter',
103         'ModalServiceSdcUI',
104         'filteredProperties',
105         '$timeout',
106         'isViewOnly',
107         'isPropertyValueOwner',
108         'propertyOwnerType',
109         'propertyOwnerId',
110         'ComponentInstanceServiceNg2',
111         'TopologyTemplateService',
112         'CompositionService',
113         'workspaceService'
114     ];
115
116     private formState:FormState;
117
118     constructor(private $scope:IPropertyFormViewModelScope,
119                 private DataTypesService:DataTypesService,
120                 private $uibModalInstance:ng.ui.bootstrap.IModalServiceInstance,
121                 private property:PropertyModel,
122                 private ValidationPattern:RegExp,
123                 private PropertyNameValidationPattern:RegExp,
124                 private CommentValidationPattern:RegExp,
125                 private ValidationUtils:ValidationUtils,
126                 // private component:Component,
127                 private $filter:ng.IFilterService,
128                 private modalService:SdcUiServices.ModalService,
129                 private filteredProperties:Array<PropertyModel>,
130                 private $timeout:ng.ITimeoutService,
131                 private isViewOnly:boolean,
132                 private isPropertyValueOwner:boolean,
133                 private propertyOwnerType:string,
134                 private propertyOwnerId:string,
135                 private ComponentInstanceServiceNg2: ComponentInstanceServiceNg2,
136                 private topologyTemplateService: TopologyTemplateService,
137                 private compositionService: CompositionService,
138                 private workspaceService: WorkspaceService) {
139
140         this.formState = angular.isDefined(property.name) ? FormState.UPDATE : FormState.CREATE;
141         this.initScope();
142     }
143
144     private initResource = ():void => {
145         this.$scope.editPropertyModel.property = new PropertyModel(this.property);
146         this.$scope.editPropertyModel.property.type = this.property.type ? this.property.type : null;
147         this.$scope.constraints = this.property.constraints && this.property.constraints[0] ? this.property.constraints[0]["validValues"] : null;
148         this.initToscaGetFunction();
149         this.setMaxLength();
150     };
151
152     private initToscaGetFunction() {
153         this.$scope.editPropertyModel.hasGetFunctionValue = this.$scope.editPropertyModel.property.isToscaFunction();
154         this.$scope.editPropertyModel.isGetFunctionValid = true;
155     }
156
157     private initForNotSimpleType = ():void => {
158         const property = this.$scope.editPropertyModel.property;
159         this.$scope.isTypeDataType = this.DataTypesService.isDataTypeForPropertyType(this.$scope.editPropertyModel.property);
160         if (property.isToscaFunction()) {
161             this.initValueForGetFunction();
162             return;
163         }
164
165         if (this.isComplexType(property.type)) {
166             if (property.value || property.defaultValue) {
167                 this.$scope.myValue = JSON.parse(property.value || property.defaultValue);
168             } else {
169                 this.initEmptyComplexValue(property.type);
170             }
171         }
172     };
173
174     private initValueForGetFunction(): void {
175         const property = this.$scope.editPropertyModel.property;
176         if (property.defaultValue) {
177             this.$scope.myValue = JSON.parse(property.defaultValue);
178             return;
179         }
180         if (this.isComplexType(property.type)) {
181             this.initEmptyComplexValue(property.type);
182             return;
183         }
184
185         this.$scope.myValue = undefined;
186     }
187
188     private initComponentInstanceMap() {
189         this.$scope.componentInstanceMap = new Map<string, InstanceFeDetails>();
190         if (this.compositionService.componentInstances) {
191             this.compositionService.componentInstances.forEach(value => {
192                 this.$scope.componentInstanceMap.set(value.uniqueId, <InstanceFeDetails>{
193                     name: value.name
194                 });
195             });
196         }
197     }
198
199     private initEmptyComplexValue(type: string): any {
200         switch (type) {
201             case PROPERTY_TYPES.MAP:
202                 this.$scope.myValue = {'': null};
203                 break;
204             case PROPERTY_TYPES.LIST:
205                 this.$scope.myValue = [];
206                 break;
207             default:
208                 this.$scope.myValue = {};
209         }
210     }
211
212     private isComplexType(type: string): boolean {
213         if (!type) {
214             return false;
215         }
216         return PROPERTY_DATA.SIMPLE_TYPES.indexOf(type) == -1;
217     }
218
219     private setMaxLength = ():void => {
220         switch (this.$scope.editPropertyModel.property.type) {
221             case PROPERTY_TYPES.MAP:
222             case PROPERTY_TYPES.LIST:
223                 this.$scope.maxLength = this.$scope.editPropertyModel.property.schema.property.type == PROPERTY_TYPES.JSON ?
224                     PROPERTY_VALUE_CONSTRAINTS.JSON_MAX_LENGTH :
225                     PROPERTY_VALUE_CONSTRAINTS.MAX_LENGTH;
226                 break;
227             case PROPERTY_TYPES.JSON:
228                 this.$scope.maxLength = PROPERTY_VALUE_CONSTRAINTS.JSON_MAX_LENGTH;
229                 break;
230             default:
231                 this.$scope.maxLength =PROPERTY_VALUE_CONSTRAINTS.MAX_LENGTH;
232         }
233     };
234
235
236     private initScope = ():void => {
237
238         //scope properties
239         this.$scope.isViewOnly = this.isViewOnly;
240         this.$scope.isLoading = true;
241         this.$scope.forms = {};
242         this.$scope.validationPattern = this.ValidationPattern;
243         this.$scope.propertyNameValidationPattern = this.PropertyNameValidationPattern;
244         this.$scope.commentValidationPattern = this.CommentValidationPattern;
245         this.$scope.nameMaxLength = PROPERTY_VALUE_CONSTRAINTS.NAME_MAX_LENGTH;
246         this.$scope.isNew = (this.formState === FormState.CREATE);
247         this.$scope.componentMetadata = {
248             isService: this.workspaceService.metadata.isService(),
249             isVfc: this.workspaceService.metadata.isVfc()
250         }
251         this.$scope.modalInstanceProperty = this.$uibModalInstance;
252         this.$scope.currentPropertyIndex = _.findIndex(this.filteredProperties, i=> i.name == this.property.name);
253         this.$scope.isLastProperty = this.$scope.currentPropertyIndex == (this.filteredProperties.length - 1);
254         const property = new PropertyModel(this.property);
255         this.$scope.editPropertyModel = {
256             'property': property,
257             types: PROPERTY_DATA.TYPES,
258             simpleTypes: PROPERTY_DATA.SIMPLE_TYPES,
259             hasGetFunctionValue: property.isToscaFunction(),
260             isGetFunctionValid: true,
261         };
262         this.$scope.isPropertyValueOwner = this.isPropertyValueOwner;
263         this.$scope.propertyOwnerType = this.propertyOwnerType;
264         this.$scope.modelNameFilter = this.workspaceService.metadata.model;
265         //check if property of VnfConfiguration
266         this.$scope.isVnfConfiguration = false;
267         if(this.propertyOwnerType == "component" && angular.isArray(this.compositionService.componentInstances)) {
268             const componentPropertyOwner:ComponentInstance = this.compositionService.componentInstances.find((ci:ComponentInstance) => {
269                 return ci.uniqueId === this.property.resourceInstanceUniqueId;
270             });
271             if (componentPropertyOwner && componentPropertyOwner.componentName === 'vnfConfiguration') {
272                 this.$scope.isVnfConfiguration = true;
273             }
274         }
275         this.initResource();
276         this.initForNotSimpleType();
277         this.initComponentInstanceMap();
278
279         this.$scope.validateJson = (json:string):boolean => {
280             if (!json) {
281                 return true;
282             }
283             return this.ValidationUtils.validateJson(json);
284         };
285
286         this.DataTypesService.fetchDataTypesByModel(this.workspaceService.metadata.model).then(response => {
287             this.$scope.dataTypes = response.data as DataTypesMap;
288             this.$scope.nonPrimitiveTypes = _.filter(Object.keys(this.$scope.dataTypes), (type:string)=> {
289                 return this.$scope.editPropertyModel.types.indexOf(type) == -1;
290             });
291
292             this.$scope.isLoading = false;
293         });
294
295         //scope methods
296         this.$scope.save = (doNotCloseModal?:boolean):void => {
297             let property:PropertyModel = this.$scope.editPropertyModel.property;
298             this.$scope.editPropertyModel.property.description = this.ValidationUtils.stripAndSanitize(this.$scope.editPropertyModel.property.description);
299             //if read only - or no changes made - just closes the modal
300             //need to check for property.value changes manually to detect if map properties deleted
301             if ((this.$scope.editPropertyModel.property.readonly && !this.$scope.isPropertyValueOwner)
302                 || (!this.$scope.forms.editForm.$dirty && angular.equals(JSON.stringify(this.$scope.myValue), this.$scope.editPropertyModel.property.value))) {
303                 this.$uibModalInstance.close();
304                 return;
305             }
306
307             this.$scope.isLoading = true;
308
309             let onPropertyFailure = (response):void => {
310                 console.error('Failed to update property', response);
311                 this.$scope.isLoading = false;
312             };
313
314             let onPropertySuccess = (propertyFromBE:PropertyModel):void => {
315                 this.$scope.isLoading = false;
316                 this.filteredProperties[this.$scope.currentPropertyIndex] = propertyFromBE;
317                 if (!doNotCloseModal) {
318                     this.$uibModalInstance.close(propertyFromBE);
319                 } else {
320                     this.$scope.forms.editForm.$setPristine();
321                     this.$scope.editPropertyModel.property = new PropertyModel();
322                 }
323             };
324
325             //Not clean, but doing this as a temporary fix until we update the property right panel modals
326             if (this.propertyOwnerType === "group"){
327                 this.ComponentInstanceServiceNg2.updateComponentGroupInstanceProperties(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId, this.propertyOwnerId, [property])
328                     .subscribe((propertiesFromBE) => { onPropertySuccess(<PropertyModel>propertiesFromBE[0])}, error => onPropertyFailure(error));
329             } else if (this.propertyOwnerType === "policy"){
330                 if (!this.$scope.editPropertyModel.property.simpleType &&
331                     !this.$scope.isSimpleType(this.$scope.editPropertyModel.property.type) &&
332                     !_.isNil(this.$scope.myValue)) {
333                     property.value = JSON.stringify(this.$scope.myValue);
334                 }
335                 this.ComponentInstanceServiceNg2.updateComponentPolicyInstanceProperties(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId, this.propertyOwnerId, [property])
336                     .subscribe((propertiesFromBE) => { onPropertySuccess(<PropertyModel>propertiesFromBE[0])}, error => onPropertyFailure(error));
337             } else {
338                 //in case we have uniqueId we call update method
339                 if (this.$scope.isPropertyValueOwner) {
340                     if (!this.$scope.editPropertyModel.property.simpleType && !this.$scope.isSimpleType(property.type)) {
341                         property.value = JSON.stringify(this.$scope.myValue);
342                     }
343                     this.updateInstanceProperties(property.resourceInstanceUniqueId, [property]).subscribe((propertiesFromBE) => onPropertySuccess(propertiesFromBE[0]),
344                         error => onPropertyFailure(error));
345                 } else {
346                     if (!this.$scope.editPropertyModel.property.simpleType && !this.$scope.isSimpleType(property.type)) {
347                         property.defaultValue = JSON.stringify(this.$scope.myValue);
348                     } else {
349                         this.$scope.editPropertyModel.property.defaultValue = this.$scope.editPropertyModel.property.value;
350                     }
351                     this.addOrUpdateProperty(property).subscribe(onPropertySuccess, error => onPropertyFailure(error));
352                 }
353             }
354         };
355
356         this.$scope.getPrev = ():void=> {
357             this.property = this.filteredProperties[--this.$scope.currentPropertyIndex];
358             this.initResource();
359             this.initForNotSimpleType();
360             this.$scope.isLastProperty = false;
361         };
362
363         this.$scope.getNext = ():void=> {
364             this.property = this.filteredProperties[++this.$scope.currentPropertyIndex];
365             this.initResource();
366             this.initForNotSimpleType();
367             this.$scope.isLastProperty = this.$scope.currentPropertyIndex == (this.filteredProperties.length - 1);
368         };
369
370         this.$scope.isSimpleType = (typeName:string):boolean=> {
371             return typeName && this.$scope.editPropertyModel.simpleTypes.indexOf(typeName) != -1;
372         };
373
374         this.$scope.showSchema = ():boolean => {
375             return [PROPERTY_TYPES.LIST, PROPERTY_TYPES.MAP].indexOf(this.$scope.editPropertyModel.property.type) > -1;
376         };
377
378         this.$scope.getValidationPattern = (type:string):RegExp => {
379             return this.ValidationUtils.getValidationPattern(type);
380         };
381
382         this.$scope.validateIntRange = (value:string):boolean => {
383             return !value || this.ValidationUtils.validateIntRange(value);
384         };
385
386         this.$scope.close = ():void => {
387             this.$uibModalInstance.close();
388         };
389
390         // put default value when instance value is empty
391         this.$scope.onValueChange = ():void => {
392             if (!this.$scope.editPropertyModel.property.value) {
393                 if (this.$scope.isPropertyValueOwner) {
394                     this.$scope.editPropertyModel.property.value = this.$scope.editPropertyModel.property.defaultValue;
395                 }
396             }
397         };
398
399         // Add the done button at the footer.
400         this.$scope.footerButtons = [
401             {'name': 'Save', 'css': 'blue', 'callback': this.$scope.save},
402             {'name': 'Cancel', 'css': 'grey', 'callback': this.$scope.close}
403         ];
404
405         this.$scope.$watch("forms.editForm.$invalid", (newVal) => {
406             if (this.$scope.editPropertyModel.hasGetFunctionValue) {
407                 this.$scope.footerButtons[0].disabled = newVal || !this.$scope.editPropertyModel.property.toscaFunction || this.isViewOnly;
408             } else {
409                 this.$scope.footerButtons[0].disabled = newVal || this.isViewOnly;
410             }
411         });
412
413         this.$scope.$watch("forms.editForm.$valid", (newVal) => {
414             if (this.$scope.editPropertyModel.hasGetFunctionValue) {
415                 this.$scope.footerButtons[0].disabled = !newVal || !this.$scope.editPropertyModel.property.toscaFunction || this.isViewOnly;
416             } else {
417                 this.$scope.footerButtons[0].disabled = !newVal || this.isViewOnly;
418             }
419         });
420
421         this.$scope.getDefaultValue = ():any => {
422             return this.$scope.isPropertyValueOwner ? this.$scope.editPropertyModel.property.defaultValue : null;
423         };
424
425         this.$scope.onTypeChange = ():void => {
426             this.$scope.editPropertyModel.property.value = '';
427             this.$scope.editPropertyModel.property.defaultValue = '';
428             this.setMaxLength();
429             this.initForNotSimpleType();
430         };
431
432         this.$scope.onSchemaTypeChange = ():void => {
433             if (this.$scope.editPropertyModel.property.type == PROPERTY_TYPES.MAP) {
434                 this.$scope.myValue = {};
435             } else if (this.$scope.editPropertyModel.property.type == PROPERTY_TYPES.LIST) {
436                 this.$scope.myValue = [];
437             }
438             this.setMaxLength();
439         };
440
441         this.$scope.delete = (property:PropertyModel):void => {
442             let onOk: Function = ():void => {
443                 this.deleteProperty(property.uniqueId).subscribe(
444                     this.$scope.close
445                 );
446             };
447             let title:string = this.$filter('translate')("PROPERTY_VIEW_DELETE_MODAL_TITLE");
448             let message:string = this.$filter('translate')("PROPERTY_VIEW_DELETE_MODAL_TEXT", "{'name': '" + property.name + "'}");
449             const okButton = {testId: "OK", text: "OK", type: SdcUiCommon.ButtonType.info, callback: onOk, closeModal: true} as SdcUiComponents.ModalButtonComponent;
450             this.modalService.openInfoModal(title, message, 'delete-modal', [okButton]);
451         };
452
453         this.$scope.onValueTypeChange = (): void => {
454             this.setEmptyValue();
455             if (this.$scope.editPropertyModel.hasGetFunctionValue) {
456                 this.$scope.editPropertyModel.isGetFunctionValid = undefined;
457             } else {
458                 this.$scope.editPropertyModel.property.toscaFunction = undefined;
459                 this.$scope.editPropertyModel.isGetFunctionValid = true;
460             }
461         }
462
463         this.$scope.onGetFunctionValidFunction = (toscaGetFunction: ToscaGetFunction): void => {
464             this.$scope.editPropertyModel.property.toscaFunction = toscaGetFunction;
465         }
466
467         this.$scope.onToscaFunctionValidityChange = (validationEvent: ToscaFunctionValidationEvent): void => {
468             if (validationEvent.isValid) {
469                 this.$scope.editPropertyModel.isGetFunctionValid = true;
470                 return;
471             }
472             this.$scope.editPropertyModel.isGetFunctionValid = undefined;
473         }
474
475     };
476
477     private setEmptyValue() {
478         const property1 = this.$scope.editPropertyModel.property;
479         property1.value = undefined;
480         if (this.isComplexType(property1.type)) {
481             this.initEmptyComplexValue(property1.type);
482             return;
483         }
484         this.$scope.myValue = '';
485     }
486
487     private updateInstanceProperties = (componentInstanceId:string, properties:PropertyModel[]):Observable<PropertyModel[]> => {
488
489         return this.ComponentInstanceServiceNg2.updateInstanceProperties(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId, componentInstanceId, properties)
490             .map(newProperties => {
491                 newProperties.forEach((newProperty) => {
492                     if (!_.isNil(newProperty.path)) {
493                         if (newProperty.path[0] === newProperty.resourceInstanceUniqueId) newProperty.path.shift();
494                         // find exist instance property in parent component for update the new value ( find bu uniqueId & path)
495                         let existProperty: PropertyModel = <PropertyModel>_.find(this.compositionService.componentInstancesProperties[newProperty.resourceInstanceUniqueId], {
496                             uniqueId: newProperty.uniqueId,
497                             path: newProperty.path
498                         });
499                         let index = this.compositionService.componentInstancesProperties[newProperty.resourceInstanceUniqueId].indexOf(existProperty);
500                         this.compositionService.componentInstancesProperties[newProperty.resourceInstanceUniqueId][index] = newProperty;
501                     }
502                 });
503                 return newProperties;
504             });
505     };
506
507     private addOrUpdateProperty = (property: PropertyModel): Observable<PropertyModel> => {
508         if (!property.uniqueId) {
509             let onSuccess = (newProperty: PropertyModel): PropertyModel => {
510                 this.filteredProperties.push(newProperty);
511                 return newProperty;
512             };
513             return this.topologyTemplateService.addProperty(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId, property)
514                 .map(onSuccess);
515         } else {
516             let onSuccess = (newProperty: PropertyModel): PropertyModel => {
517                 // find exist instance property in parent component for update the new value ( find bu uniqueId )
518                 let existProperty: PropertyModel = <PropertyModel>_.find(this.filteredProperties, {uniqueId: newProperty.uniqueId});
519                 let propertyIndex = this.filteredProperties.indexOf(existProperty);
520                 this.filteredProperties[propertyIndex] = newProperty;
521                 return newProperty;
522             };
523             return this.topologyTemplateService.updateProperty(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId, property).map(onSuccess);
524         }
525     };
526
527     public deleteProperty = (propertyId:string):Observable<void> => {
528         let onSuccess = ():void => {
529             console.debug("Property deleted");
530             delete _.remove(this.filteredProperties, {uniqueId: propertyId})[0];
531         };
532         let onFailed = ():void => {
533             console.debug("Failed to delete property");
534         };
535         return this.topologyTemplateService.deleteProperty(this.workspaceService.metadata.componentType, this.workspaceService.metadata.uniqueId, propertyId).map(onSuccess, onFailed);
536     };
537
538 }