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