Consider schema in filtering function properties
[sdc.git] / catalog-ui / src / app / ng2 / pages / properties-assignment / tosca-function / tosca-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, OnInit, Output} from '@angular/core';
21 import {ComponentMetadata, DataTypeModel, PropertyBEModel, PropertyModel} 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} 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 {AbstractControl, FormControl, FormGroup, ValidationErrors, ValidatorFn} from "@angular/forms";
35
36 @Component({
37     selector: 'tosca-function',
38     templateUrl: './tosca-function.component.html',
39     styleUrls: ['./tosca-function.component.less'],
40 })
41 export class ToscaFunctionComponent implements OnInit {
42
43     @Input() property: PropertyBEModel;
44     @Input() componentInstanceMap: Map<string, InstanceFeDetails> = new Map<string, InstanceFeDetails>();
45     @Input() allowClear: boolean = true;
46     @Output() onValidFunction: EventEmitter<ToscaGetFunction> = new EventEmitter<ToscaGetFunction>();
47     @Output() onValidityChange: EventEmitter<boolean> = new EventEmitter<boolean>();
48
49     toscaGetFunctionValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
50         const toscaGetFunction: ToscaGetFunction = control.value;
51         const errors: ValidationErrors = {};
52         if (!toscaGetFunction.sourceName) {
53             errors.sourceName = { required: true };
54         }
55         if (!toscaGetFunction.functionType) {
56             errors.functionType = { required: true };
57         }
58         if (!toscaGetFunction.sourceUniqueId) {
59             errors.sourceUniqueId = { required: true };
60         }
61         if (!toscaGetFunction.sourceName) {
62             errors.sourceName = { required: true };
63         }
64         if (!toscaGetFunction.propertyPathFromSource) {
65             errors.propertyPathFromSource = { required: true };
66         }
67         if (!toscaGetFunction.propertyName) {
68             errors.propertyName = { required: true };
69         }
70         if (!toscaGetFunction.propertySource) {
71             errors.propertySource = { required: true };
72         }
73         return errors ? errors : null;
74     };
75
76     toscaGetFunctionForm: FormControl = new FormControl(new ToscaGetFunction(undefined), [this.toscaGetFunctionValidator]);
77     formGroup: FormGroup = new FormGroup({
78         'toscaGetFunction': this.toscaGetFunctionForm
79     });
80
81     TOSCA_FUNCTION_GET_PROPERTY = ToscaGetFunctionType.GET_PROPERTY;
82
83     selectedProperty: PropertyDropdownValue;
84     isLoading: boolean = false;
85     propertyDropdownList: Array<PropertyDropdownValue> = [];
86     toscaFunctions: Array<string> = [];
87     propertySourceList: Array<string> = [];
88     instanceNameAndIdMap: Map<string, string> = new Map<string, string>();
89     dropdownValuesLabel: string;
90     dropDownErrorMsg: string;
91     propertySource: string
92     toscaGetFunction: ToscaGetFunction = new ToscaGetFunction(undefined);
93
94     private componentMetadata: ComponentMetadata;
95
96     constructor(private topologyTemplateService: TopologyTemplateService,
97                 private workspaceService: WorkspaceService,
98                 private propertiesService: PropertiesService,
99                 private dataTypeService: DataTypeService,
100                 private translateService: TranslateService) {
101     }
102
103     ngOnInit(): void {
104         this.componentMetadata = this.workspaceService.metadata;
105         this.loadToscaFunctions();
106         this.loadPropertySourceDropdown();
107         this.initToscaGetFunction();
108         this.toscaGetFunctionForm.valueChanges.subscribe(toscaGetFunction => {
109             this.onValidityChange.emit(this.toscaGetFunctionForm.valid);
110             if (this.toscaGetFunctionForm.valid) {
111                 this.onValidFunction.emit(toscaGetFunction);
112             }
113         })
114     }
115
116     private initToscaGetFunction(): void {
117         if (!this.property.isToscaGetFunction()) {
118             return;
119         }
120         this.toscaGetFunction = new ToscaGetFunction(this.property.toscaGetFunction);
121         this.toscaGetFunctionForm.setValue(this.toscaGetFunction);
122         if (this.toscaGetFunction.functionType === ToscaGetFunctionType.GET_PROPERTY) {
123             if (this.toscaGetFunction.propertySource === PropertySource.SELF) {
124                 this.propertySource = PropertySource.SELF;
125             } else {
126                 this.propertySource = this.toscaGetFunction.sourceName;
127             }
128         }
129         if (this.toscaGetFunction.propertyName) {
130             this.loadPropertyDropdown(() => {
131                 this.selectedProperty = this.propertyDropdownList.find(property => property.propertyName === this.toscaGetFunction.propertyName)
132             });
133         }
134     }
135
136     private loadToscaFunctions(): void {
137         this.toscaFunctions.push(ToscaGetFunctionType.GET_INPUT);
138         this.toscaFunctions.push(ToscaGetFunctionType.GET_PROPERTY);
139     }
140
141     private loadPropertySourceDropdown(): void {
142         this.propertySourceList.push(PropertySource.SELF);
143         this.componentInstanceMap.forEach((value, key) => {
144             const instanceName = value.name;
145             this.instanceNameAndIdMap.set(instanceName, key);
146             if (instanceName !== PropertySource.SELF) {
147                 this.addToPropertySource(instanceName);
148             }
149         });
150     }
151
152     private addToPropertySource(source: string): void {
153         this.propertySourceList.push(source);
154         this.propertySourceList.sort((a, b) => {
155             if (a === PropertySource.SELF) {
156                 return -1;
157             } else if (b === PropertySource.SELF) {
158                 return 1;
159             }
160
161             return a.localeCompare(b);
162         });
163     }
164
165     onToscaFunctionChange(): void {
166         this.resetPropertySource();
167         this.resetPropertyDropdown();
168         if (this.isGetInputSelected()) {
169             this.setSelfPropertySource();
170             this.loadPropertyDropdown();
171         }
172     }
173
174     private loadPropertyDropdown(onComplete?: () => any): void  {
175         this.loadPropertyDropdownLabel();
176         this.loadPropertyDropdownValues(onComplete);
177     }
178
179     private resetForm(): void {
180         this.toscaGetFunction = new ToscaGetFunction(undefined);
181         this.toscaGetFunctionForm.setValue(new ToscaGetFunction(undefined));
182         this.propertySource = undefined;
183         this.selectedProperty = undefined;
184     }
185
186     private resetPropertySource(): void {
187         this.toscaGetFunction.propertyUniqueId = undefined;
188         this.toscaGetFunction.propertyName = undefined;
189         this.toscaGetFunction.propertySource = undefined;
190         this.toscaGetFunction.sourceUniqueId = undefined;
191         this.toscaGetFunction.sourceName = undefined;
192         this.toscaGetFunction.propertyPathFromSource = undefined;
193         this.propertySource = undefined;
194         this.selectedProperty = undefined;
195
196         const toscaGetFunction1 = new ToscaGetFunction(undefined);
197         toscaGetFunction1.functionType = this.toscaGetFunction.functionType;
198         this.toscaGetFunctionForm.setValue(toscaGetFunction1);
199     }
200
201     private loadPropertyDropdownLabel(): void {
202         if (!this.toscaGetFunction.functionType) {
203             return;
204         }
205         if (this.isGetInputSelected()) {
206             this.dropdownValuesLabel = this.translateService.translate('INPUT_DROPDOWN_LABEL');
207         } else if (this.isGetPropertySelected()) {
208             this.dropdownValuesLabel = this.translateService.translate('TOSCA_FUNCTION_PROPERTY_DROPDOWN_LABEL');
209         }
210     }
211
212     private loadPropertyDropdownValues(onComplete?: () => any): void {
213         if (!this.toscaGetFunction.functionType) {
214             return;
215         }
216         this.resetPropertyDropdown();
217         this.fillPropertyDropdownValues(onComplete);
218     }
219
220     private resetPropertyDropdown(): void {
221         this.dropDownErrorMsg = undefined;
222         this.selectedProperty = undefined;
223         this.propertyDropdownList = [];
224     }
225
226     private fillPropertyDropdownValues(onComplete?: () => any): void {
227         this.startLoading();
228         const propertiesObservable: Observable<ComponentGenericResponse> = this.getPropertyObservable();
229         propertiesObservable.subscribe( (response: ComponentGenericResponse) => {
230             const properties: PropertyBEModel[] = this.extractProperties(response);
231             if (!properties || properties.length === 0) {
232                 const msgCode = this.isGetInputSelected() ? 'TOSCA_FUNCTION_NO_INPUT_FOUND' : 'TOSCA_FUNCTION_NO_PROPERTY_FOUND';
233                 this.dropDownErrorMsg = this.translateService.translate(msgCode, {type: this.propertyTypeToString()});
234                 return;
235             }
236             this.addPropertiesToDropdown(properties);
237             if (this.propertyDropdownList.length == 0) {
238                 const msgCode = this.isGetInputSelected() ? 'TOSCA_FUNCTION_NO_INPUT_FOUND' : 'TOSCA_FUNCTION_NO_PROPERTY_FOUND';
239                 this.dropDownErrorMsg = this.translateService.translate(msgCode, {type: this.propertyTypeToString()});
240             }
241         }, (error) => {
242             console.error('An error occurred while loading properties.', error);
243         }, () => {
244             if (onComplete) {
245                 onComplete();
246             }
247             this.stopLoading();
248         });
249     }
250
251     private propertyTypeToString() {
252         if (this.property.schemaType) {
253             return `${this.property.type} of ${this.property.schemaType}`;
254         }
255         return this.property.type;
256     }
257
258     private extractProperties(componentGenericResponse: ComponentGenericResponse): PropertyBEModel[] {
259         if (this.isGetInputSelected()) {
260             return componentGenericResponse.inputs;
261         }
262         if (this.isGetPropertySelected()) {
263             if (this.propertySource === PropertySource.SELF) {
264                 return componentGenericResponse.properties;
265             }
266             const componentInstanceProperties: PropertyModel[] = componentGenericResponse.componentInstancesProperties[this.instanceNameAndIdMap.get(this.propertySource)];
267             return this.removeSelectedProperty(componentInstanceProperties);
268         }
269     }
270
271     private getPropertyObservable(): Observable<ComponentGenericResponse> {
272         if (this.isGetInputSelected()) {
273             return this.topologyTemplateService.getComponentInputsValues(this.componentMetadata.componentType, this.componentMetadata.uniqueId);
274         }
275         if (this.isGetPropertySelected()) {
276             if (this.propertySource === PropertySource.SELF) {
277                 return this.topologyTemplateService.findAllComponentProperties(this.componentMetadata.componentType, this.componentMetadata.uniqueId);
278             }
279             return this.topologyTemplateService.getComponentInstanceProperties(this.componentMetadata.componentType, this.componentMetadata.uniqueId);
280         }
281     }
282
283     private removeSelectedProperty(componentInstanceProperties: PropertyModel[]): PropertyModel[] {
284         if (!componentInstanceProperties) {
285             return [];
286         }
287         return componentInstanceProperties.filter(property =>
288             (property.uniqueId !== this.property.uniqueId) ||
289             (property.uniqueId === this.property.uniqueId && property.resourceInstanceUniqueId !== this.property.parentUniqueId)
290         );
291     }
292
293     private addPropertyToDropdown(propertyDropdownValue: PropertyDropdownValue): void {
294         this.propertyDropdownList.push(propertyDropdownValue);
295         this.propertyDropdownList.sort((a, b) => a.propertyLabel.localeCompare(b.propertyLabel));
296     }
297
298     private addPropertiesToDropdown(properties: PropertyBEModel[]): void {
299         for (const property of properties) {
300             if (this.hasSameType(property)) {
301                 this.addPropertyToDropdown({
302                     propertyName: property.name,
303                     propertyId: property.uniqueId,
304                     propertyLabel: property.name,
305                     propertyPath: [property.name]
306                 });
307             } else if (this.isComplexType(property.type)) {
308                 this.fillPropertyDropdownWithMatchingChildProperties(property);
309             }
310         }
311     }
312
313     private fillPropertyDropdownWithMatchingChildProperties(inputProperty: PropertyBEModel, parentPropertyList: Array<PropertyBEModel> = []): void {
314         const dataTypeFound: DataTypeModel = this.dataTypeService.getDataTypeByModelAndTypeName(this.componentMetadata.model, inputProperty.type);
315         if (!dataTypeFound || !dataTypeFound.properties) {
316             return;
317         }
318         parentPropertyList.push(inputProperty);
319         dataTypeFound.properties.forEach(dataTypeProperty => {
320             if (this.hasSameType(dataTypeProperty)) {
321                 this.addPropertyToDropdown({
322                     propertyName: dataTypeProperty.name,
323                     propertyId: parentPropertyList[0].uniqueId,
324                     propertyLabel: parentPropertyList.map(property => property.name).join('->') + '->' + dataTypeProperty.name,
325                     propertyPath: [...parentPropertyList.map(property => property.name), dataTypeProperty.name]
326                 });
327             } else if (this.isComplexType(dataTypeProperty.type)) {
328                 this.fillPropertyDropdownWithMatchingChildProperties(dataTypeProperty, [...parentPropertyList])
329             }
330         });
331     }
332
333     private hasSameType(property: PropertyBEModel) {
334         if (this.property.schema && this.property.schema.property) {
335             if (!property.schema || !property.schema.property) {
336                 return false;
337             }
338             return property.type === this.property.type && this.property.schema.property.type === property.schema.property.type;
339         }
340         return property.type === this.property.type;
341     }
342
343     private isGetPropertySelected(): boolean {
344         return this.toscaGetFunction.functionType === ToscaGetFunctionType.GET_PROPERTY;
345     }
346
347     private isGetInputSelected(): boolean {
348         return this.toscaGetFunction.functionType === ToscaGetFunctionType.GET_INPUT;
349     }
350
351     private isComplexType(propertyType: string): boolean {
352         return PROPERTY_DATA.SIMPLE_TYPES.indexOf(propertyType) === -1;
353     }
354
355     private stopLoading(): void {
356         this.isLoading = false;
357     }
358
359     private startLoading(): void {
360         this.isLoading = true;
361     }
362
363     showDropdown(): boolean {
364         if (this.toscaGetFunction.functionType === ToscaGetFunctionType.GET_PROPERTY) {
365             return this.toscaGetFunction.propertySource && !this.isLoading && !this.dropDownErrorMsg;
366         }
367
368         return this.toscaGetFunction.functionType && !this.isLoading && !this.dropDownErrorMsg;
369     }
370
371     onPropertySourceChange(): void {
372         if (!this.toscaGetFunction.functionType || !this.propertySource) {
373             return;
374         }
375         this.toscaGetFunction.propertyUniqueId = undefined;
376         this.toscaGetFunction.propertyName = undefined;
377         this.toscaGetFunction.propertyPathFromSource = undefined;
378         if (this.propertySource === PropertySource.SELF) {
379             this.setSelfPropertySource();
380         } else {
381             this.toscaGetFunction.propertySource = PropertySource.INSTANCE;
382             this.toscaGetFunction.sourceName = this.propertySource;
383             this.toscaGetFunction.sourceUniqueId = this.instanceNameAndIdMap.get(this.propertySource);
384         }
385         this.toscaGetFunctionForm.setValue(this.toscaGetFunction);
386         this.loadPropertyDropdown();
387     }
388
389     private setSelfPropertySource(): void {
390         this.toscaGetFunction.propertySource = PropertySource.SELF;
391         this.toscaGetFunction.sourceName = this.componentMetadata.name;
392         this.toscaGetFunction.sourceUniqueId = this.componentMetadata.uniqueId;
393         this.toscaGetFunctionForm.setValue(this.toscaGetFunction);
394     }
395
396     onPropertyChange(): void {
397         this.toscaGetFunction.propertyUniqueId = this.selectedProperty.propertyId;
398         this.toscaGetFunction.propertyName = this.selectedProperty.propertyName;
399         this.toscaGetFunction.propertyPathFromSource = this.selectedProperty.propertyPath;
400         this.toscaGetFunctionForm.setValue(this.toscaGetFunction);
401     }
402
403     onClearValues() {
404         this.resetForm();
405     }
406
407     showClearButton(): boolean {
408         return this.allowClear && this.toscaGetFunction.functionType !== undefined;
409     }
410 }
411
412 export interface PropertyDropdownValue {
413     propertyName: string;
414     propertyId: string;
415     propertyLabel: string;
416     propertyPath: Array<string>;
417 }