Support TOSCA functions in sub properties
[sdc.git] / catalog-ui / src / app / ng2 / pages / properties-assignment / tosca-function / tosca-get-function / tosca-get-function.component.ts
1 /*
2  * ============LICENSE_START=======================================================
3  *  Copyright (C) 2021 Nordix Foundation
4  *  ================================================================================
5  *  Licensed under the Apache License, Version 2.0 (the "License");
6  *  you may not use this file except in compliance with the License.
7  *  You may obtain a copy of the License at
8  *
9  *        http://www.apache.org/licenses/LICENSE-2.0
10  *  Unless required by applicable law or agreed to in writing, software
11  *  distributed under the License is distributed on an "AS IS" BASIS,
12  *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  *  See the License for the specific language governing permissions and
14  *  limitations under the License.
15  *
16  *  SPDX-License-Identifier: Apache-2.0
17  *  ============LICENSE_END=========================================================
18  */
19
20 import {Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges} from '@angular/core';
21 import {AttributeBEModel, ComponentMetadata, DataTypeModel, PropertyBEModel, PropertyModel, PropertyDeclareAPIModel} from 'app/models';
22 import {TopologyTemplateService} from "../../../../services/component-services/topology-template.service";
23 import {WorkspaceService} from "../../../workspace/workspace.service";
24 import {PropertiesService} from "../../../../services/properties.service";
25 import {PROPERTY_DATA, PROPERTY_TYPES} from "../../../../../utils/constants";
26 import {DataTypeService} from "../../../../services/data-type.service";
27 import {ToscaGetFunctionType} from "../../../../../models/tosca-get-function-type";
28 import {TranslateService} from "../../../../shared/translator/translate.service";
29 import {ComponentGenericResponse} from '../../../../services/responses/component-generic-response';
30 import {Observable} from 'rxjs/Observable';
31 import {PropertySource} from "../../../../../models/property-source";
32 import {InstanceFeDetails} from "../../../../../models/instance-fe-details";
33 import {ToscaGetFunction} from "../../../../../models/tosca-get-function";
34 import {FormControl, FormGroup, Validators} from "@angular/forms";
35 import {ToscaGetFunctionTypeConverter} from "../../../../../models/tosca-get-function-type-converter";
36
37 @Component({
38     selector: 'app-tosca-get-function',
39     templateUrl: './tosca-get-function.component.html',
40     styleUrls: ['./tosca-get-function.component.less']
41 })
42 export class ToscaGetFunctionComponent implements OnInit, OnChanges {
43
44     @Input() property: PropertyBEModel;
45     @Input() toscaGetFunction: ToscaGetFunction;
46     @Input() componentInstanceMap: Map<string, InstanceFeDetails> = new Map<string, InstanceFeDetails>();
47     @Input() functionType: ToscaGetFunctionType;
48     @Output() onValidFunction: EventEmitter<ToscaGetFunction> = new EventEmitter<ToscaGetFunction>();
49     @Output() onValidityChange: EventEmitter<ToscaGetFunctionValidationEvent> = new EventEmitter<ToscaGetFunctionValidationEvent>();
50
51     formGroup: FormGroup = new FormGroup({
52         'selectedProperty': new FormControl(undefined, Validators.required),
53         'propertySource': new FormControl(undefined, Validators.required)
54     });
55
56     isLoading: boolean = false;
57     propertyDropdownList: Array<PropertyDropdownValue> = [];
58     propertySourceList: Array<string> = [];
59     instanceNameAndIdMap: Map<string, string> = new Map<string, string>();
60     dropdownValuesLabel: string;
61     dropDownErrorMsg: string;
62
63     private isInitialized: boolean = false;
64     private componentMetadata: ComponentMetadata;
65
66     constructor(private topologyTemplateService: TopologyTemplateService,
67                 private workspaceService: WorkspaceService,
68                 private propertiesService: PropertiesService,
69                 private dataTypeService: DataTypeService,
70                 private translateService: TranslateService) {
71     }
72
73     ngOnInit(): void {
74         this.componentMetadata = this.workspaceService.metadata;
75         this.formGroup.valueChanges.subscribe(() => {
76             if (!this.isInitialized) {
77                 return;
78             }
79             this.onValidityChange.emit({
80                 isValid: this.formGroup.valid,
81                 toscaGetFunction: this.formGroup.valid ? this.buildGetFunctionFromForm() : undefined
82             });
83             if (this.formGroup.valid) {
84                 this.onValidFunction.emit(this.buildGetFunctionFromForm());
85             }
86         });
87         this.loadPropertySourceDropdown();
88         this.loadPropertyDropdownLabel();
89         this.initToscaGetFunction().subscribe(() => {
90             this.isInitialized = true;
91         });
92
93     }
94
95     ngOnChanges(_changes: SimpleChanges): void {
96         if (!this.isInitialized) {
97             return;
98         }
99         this.isInitialized = false;
100         this.resetForm();
101         this.loadPropertySourceDropdown();
102         this.loadPropertyDropdownLabel();
103         this.initToscaGetFunction().subscribe(() => {
104             this.isInitialized = true;
105         });
106     }
107
108     private initToscaGetFunction(): Observable<void> {
109         return new Observable(subscriber => {
110             if (!this.toscaGetFunction) {
111                 if (this.isGetInput()) {
112                     this.setSelfPropertySource();
113                     this.loadPropertyDropdown();
114                 }
115                 subscriber.next();
116                 return;
117             }
118             if (this.toscaGetFunction.propertySource == PropertySource.SELF) {
119                 this.propertySource.setValue(PropertySource.SELF);
120             } else if (this.toscaGetFunction.propertySource == PropertySource.INSTANCE) {
121                 this.propertySource
122                 .setValue(this.propertySourceList.find(source => this.toscaGetFunction.sourceName === source));
123             }
124             if (this.propertySource.valid) {
125                 this.loadPropertyDropdown(() => {
126                     this.selectedProperty
127                     .setValue(this.propertyDropdownList.find(property => property.propertyName === this.toscaGetFunction.propertyName));
128                     subscriber.next();
129                 });
130             } else {
131                 subscriber.next();
132             }
133         });
134     }
135
136     private buildGetFunctionFromForm() {
137         const toscaGetFunction = new ToscaGetFunction();
138         toscaGetFunction.type = ToscaGetFunctionTypeConverter.convertToToscaFunctionType(this.functionType);
139         toscaGetFunction.functionType = this.functionType;
140         const propertySource = this.propertySource.value;
141         if (this.isPropertySourceSelf()) {
142             toscaGetFunction.propertySource = propertySource
143             toscaGetFunction.sourceName = this.componentMetadata.name;
144             toscaGetFunction.sourceUniqueId = this.componentMetadata.uniqueId;
145         } else {
146             toscaGetFunction.propertySource = PropertySource.INSTANCE;
147             toscaGetFunction.sourceName = propertySource;
148             toscaGetFunction.sourceUniqueId = this.instanceNameAndIdMap.get(propertySource);
149         }
150
151         const selectedProperty: PropertyDropdownValue = this.selectedProperty.value;
152         toscaGetFunction.propertyUniqueId = selectedProperty.propertyId;
153         toscaGetFunction.propertyName = selectedProperty.propertyName;
154         toscaGetFunction.propertyPathFromSource = selectedProperty.propertyPath;
155
156         return toscaGetFunction;
157     }
158
159     private loadPropertySourceDropdown(): void {
160         if (this.isGetInput()) {
161             return;
162         }
163         this.propertySourceList = [];
164         this.propertySourceList.push(PropertySource.SELF);
165         this.componentInstanceMap.forEach((value, key) => {
166             const instanceName = value.name;
167             this.instanceNameAndIdMap.set(instanceName, key);
168             if (instanceName !== PropertySource.SELF) {
169                 this.addToPropertySource(instanceName);
170             }
171         });
172     }
173
174     private addToPropertySource(source: string): void {
175         this.propertySourceList.push(source);
176         this.propertySourceList.sort((a, b) => {
177             if (a === PropertySource.SELF) {
178                 return -1;
179             } else if (b === PropertySource.SELF) {
180                 return 1;
181             }
182
183             return a.localeCompare(b);
184         });
185     }
186
187     private loadPropertyDropdown(onComplete?: () => any): void  {
188         this.loadPropertyDropdownLabel();
189         this.loadPropertyDropdownValues(onComplete);
190     }
191
192     private resetForm(): void {
193         this.formGroup.reset();
194     }
195
196     private loadPropertyDropdownLabel(): void {
197         if (!this.functionType) {
198             return;
199         }
200         if (this.isGetInput()) {
201             this.dropdownValuesLabel = this.translateService.translate('INPUT_DROPDOWN_LABEL');
202         } else if (this.isGetProperty()) {
203             this.dropdownValuesLabel = this.translateService.translate('TOSCA_FUNCTION_PROPERTY_DROPDOWN_LABEL');
204         } else if (this.isGetAttribute()) {
205             this.dropdownValuesLabel = this.translateService.translate('TOSCA_FUNCTION_ATTRIBUTE_DROPDOWN_LABEL');
206         }
207     }
208
209     private loadPropertyDropdownValues(onComplete?: () => any): void {
210         if (!this.functionType) {
211             return;
212         }
213         this.resetPropertyDropdown();
214         this.fillPropertyDropdownValues(onComplete);
215     }
216
217     private resetPropertyDropdown(): void {
218         this.dropDownErrorMsg = undefined;
219         this.selectedProperty.reset();
220         this.propertyDropdownList = [];
221     }
222
223     private fillPropertyDropdownValues(onComplete?: () => any): void {
224         this.startLoading();
225         const propertiesObservable: Observable<ComponentGenericResponse> = this.getPropertyObservable();
226         propertiesObservable.subscribe( (response: ComponentGenericResponse) => {
227             const properties: Array<PropertyBEModel | AttributeBEModel> = this.extractProperties(response);
228             if (!properties || properties.length === 0) {
229                 const msgCode = this.getNotFoundMsgCode();
230                 this.dropDownErrorMsg = this.translateService.translate(msgCode, {type: this.propertyTypeToString()});
231                 return;
232             }
233             this.addPropertiesToDropdown(properties);
234             if (this.propertyDropdownList.length == 0) {
235                 const msgCode = this.getNotFoundMsgCode();
236                 this.dropDownErrorMsg = this.translateService.translate(msgCode, {type: this.propertyTypeToString()});
237             }
238         }, (error) => {
239             console.error('An error occurred while loading properties.', error);
240             this.stopLoading();
241         }, () => {
242             if (onComplete) {
243                 onComplete();
244             }
245             this.stopLoading();
246         });
247     }
248
249     private getNotFoundMsgCode(): string {
250         if (this.isGetInput()) {
251             return 'TOSCA_FUNCTION_NO_INPUT_FOUND';
252         }
253         if (this.isGetAttribute()) {
254             return 'TOSCA_FUNCTION_NO_ATTRIBUTE_FOUND';
255         }
256         if (this.isGetProperty()) {
257             return 'TOSCA_FUNCTION_NO_PROPERTY_FOUND';
258         }
259
260         return undefined;
261     }
262
263     private propertyTypeToString() {
264             if (this.isSubProperty()){
265                 return this.getType((<PropertyDeclareAPIModel>this.property).propertiesName.split("#").slice(1),  this.property.type);
266         }
267         if (this.property.schemaType) {
268             return `${this.property.type} of ${this.property.schemaType}`;
269         }
270         return this.property.type;
271     }
272
273     private isSubProperty(): boolean{
274             return this.property instanceof PropertyDeclareAPIModel && (<PropertyDeclareAPIModel>this.property).propertiesName && (<PropertyDeclareAPIModel>this.property).propertiesName.length > 1;
275     }
276
277     private extractProperties(componentGenericResponse: ComponentGenericResponse): Array<PropertyBEModel | AttributeBEModel> {
278         if (this.isGetInput()) {
279             return componentGenericResponse.inputs;
280         }
281         const instanceId = this.instanceNameAndIdMap.get(this.propertySource.value);
282         if (this.isGetProperty()) {
283             if (this.isPropertySourceSelf()) {
284                 return componentGenericResponse.properties;
285             }
286             return this.removeSelectedProperty(componentGenericResponse.componentInstancesProperties[instanceId]);
287         }
288         if (this.isPropertySourceSelf()) {
289             return [...(componentGenericResponse.attributes || []), ...(componentGenericResponse.properties || [])];
290         }
291         return [...(componentGenericResponse.componentInstancesAttributes[instanceId] || []),
292             ...(componentGenericResponse.componentInstancesProperties[instanceId] || [])];
293     }
294
295     private isPropertySourceSelf() {
296         return this.propertySource.value === PropertySource.SELF;
297     }
298
299     private getPropertyObservable(): Observable<ComponentGenericResponse> {
300         if (this.isGetInput()) {
301             return this.topologyTemplateService.getComponentInputsValues(this.componentMetadata.componentType, this.componentMetadata.uniqueId);
302         }
303         if (this.isGetProperty()) {
304             if (this.isPropertySourceSelf()) {
305                 return this.topologyTemplateService.findAllComponentProperties(this.componentMetadata.componentType, this.componentMetadata.uniqueId);
306             }
307             return this.topologyTemplateService.getComponentInstanceProperties(this.componentMetadata.componentType, this.componentMetadata.uniqueId);
308         }
309         if (this.isGetAttribute()) {
310             if (this.isPropertySourceSelf()) {
311                 return this.topologyTemplateService.findAllComponentAttributesAndProperties(this.componentMetadata.componentType, this.componentMetadata.uniqueId);
312             }
313             return this.topologyTemplateService.getComponentInstanceAttributesAndProperties(this.componentMetadata.uniqueId, this.componentMetadata.componentType);
314         }
315     }
316
317     private removeSelectedProperty(componentInstanceProperties: PropertyModel[]): PropertyModel[] {
318         if (!componentInstanceProperties) {
319             return [];
320         }
321         return componentInstanceProperties.filter(property =>
322             (property.uniqueId !== this.property.uniqueId) ||
323             (property.uniqueId === this.property.uniqueId && property.resourceInstanceUniqueId !== this.property.parentUniqueId)
324         );
325     }
326
327     private addPropertyToDropdown(propertyDropdownValue: PropertyDropdownValue): void {
328         this.propertyDropdownList.push(propertyDropdownValue);
329         this.propertyDropdownList.sort((a, b) => a.propertyLabel.localeCompare(b.propertyLabel));
330     }
331
332     private addPropertiesToDropdown(properties: Array<PropertyBEModel | AttributeBEModel>): void {
333         for (const property of properties) {
334             if (this.hasSameType(property)) {
335                 this.addPropertyToDropdown({
336                     propertyName: property.name,
337                     propertyId: property.uniqueId,
338                     propertyLabel: property.name,
339                     propertyPath: [property.name]
340                 });
341             } else if (this.isComplexType(property.type)) {
342                 this.fillPropertyDropdownWithMatchingChildProperties(property);
343             }
344         }
345     }
346
347     private fillPropertyDropdownWithMatchingChildProperties(inputProperty: PropertyBEModel | AttributeBEModel,
348                                                             parentPropertyList: Array<PropertyBEModel | AttributeBEModel> = []): void {
349         const dataTypeFound: DataTypeModel = this.dataTypeService.getDataTypeByModelAndTypeName(this.componentMetadata.model, inputProperty.type);
350         if (!dataTypeFound || !dataTypeFound.properties) {
351             return;
352         }
353         parentPropertyList.push(inputProperty);
354         dataTypeFound.properties.forEach(dataTypeProperty => {
355             if (this.hasSameType(dataTypeProperty)) {
356                 this.addPropertyToDropdown({
357                     propertyName: dataTypeProperty.name,
358                     propertyId: parentPropertyList[0].uniqueId,
359                     propertyLabel: parentPropertyList.map(property => property.name).join('->') + '->' + dataTypeProperty.name,
360                     propertyPath: [...parentPropertyList.map(property => property.name), dataTypeProperty.name]
361                 });
362             } else if (this.isComplexType(dataTypeProperty.type)) {
363                 this.fillPropertyDropdownWithMatchingChildProperties(dataTypeProperty, [...parentPropertyList])
364             }
365         });
366     }
367
368     private hasSameType(property: PropertyBEModel | AttributeBEModel): boolean {
369         if (this.typeHasSchema(this.property.type)) {
370             if (!property.schema || !property.schema.property) {
371                 return false;
372             }
373             return property.type === this.property.type && this.property.schema.property.type === property.schema.property.type;
374         }
375         if (this.property.schema.property.isDataType && this.property instanceof PropertyDeclareAPIModel && (<PropertyDeclareAPIModel>this.property).propertiesName){
376             let typeToMatch = this.getType((<PropertyDeclareAPIModel>this.property).propertiesName.split("#").slice(1),  this.property.type);
377             return property.type === typeToMatch;
378         }
379
380         return property.type === this.property.type;
381     }
382
383     private getType(propertyPath:string[], type: string): string {
384             const dataTypeFound: DataTypeModel = this.dataTypeService.getDataTypeByModelAndTypeName(this.componentMetadata.model, type);
385         let nestedProperty = dataTypeFound.properties.find(property => property.name === propertyPath[0]);
386         if (propertyPath.length === 1){
387                 return nestedProperty.type;
388         } 
389         return this.getType(propertyPath.slice(1), nestedProperty.type);
390     }
391
392     private isGetProperty(): boolean {
393         return this.functionType === ToscaGetFunctionType.GET_PROPERTY;
394     }
395
396     private isGetAttribute(): boolean {
397         return this.functionType === ToscaGetFunctionType.GET_ATTRIBUTE;
398     }
399
400     private isGetInput(): boolean {
401         return this.functionType === ToscaGetFunctionType.GET_INPUT;
402     }
403
404     private isComplexType(propertyType: string): boolean {
405         return PROPERTY_DATA.SIMPLE_TYPES.indexOf(propertyType) === -1;
406     }
407
408     private typeHasSchema(propertyType: string): boolean {
409         return PROPERTY_TYPES.MAP === propertyType || PROPERTY_TYPES.LIST === propertyType;
410     }
411
412     private stopLoading(): void {
413         this.isLoading = false;
414     }
415
416     private startLoading(): void {
417         this.isLoading = true;
418     }
419
420     showPropertyDropdown(): boolean {
421         if (this.isGetProperty() || this.isGetAttribute()) {
422             return this.propertySource.valid && !this.isLoading && !this.dropDownErrorMsg;
423         }
424
425         return this.functionType && !this.isLoading && !this.dropDownErrorMsg;
426     }
427
428     onPropertySourceChange(): void {
429         if (!this.functionType || !this.propertySource.valid) {
430             return;
431         }
432         this.loadPropertyDropdown();
433     }
434
435     showPropertySourceDropdown(): boolean {
436         return this.isGetProperty() || this.isGetAttribute();
437     }
438
439     private setSelfPropertySource(): void {
440         this.propertySource.setValue(PropertySource.SELF);
441     }
442
443     private get propertySource(): FormControl {
444         return this.formGroup.get('propertySource') as FormControl;
445     }
446
447     private get selectedProperty(): FormControl {
448         return this.formGroup.get('selectedProperty') as FormControl;
449     }
450
451 }
452
453 export interface PropertyDropdownValue {
454     propertyName: string;
455     propertyId: string;
456     propertyLabel: string;
457     propertyPath: Array<string>;
458 }
459
460 export interface ToscaGetFunctionValidationEvent {
461     isValid: boolean,
462     toscaGetFunction: ToscaGetFunction,
463 }