// Copyright 2017 The Kubernetes Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import {HttpClient} from '@angular/common/http'; import {Component, EventEmitter, forwardRef, Input, OnInit, Output} from '@angular/core'; import { AbstractControl, ControlValueAccessor, FormArray, FormBuilder, FormControl, FormGroup, NG_ASYNC_VALIDATORS, NG_VALUE_ACCESSOR, Validators, } from '@angular/forms'; import {PortMapping} from '@api/backendapi'; import {Observable} from 'rxjs'; import {first, map, startWith} from 'rxjs/operators'; import {FormValidators} from '../validator/validators'; import {validateProtocol} from '../validator/validprotocol.validator'; const i18n = { MSG_PORT_MAPPINGS_SERVICE_TYPE_NONE_LABEL: 'None', MSG_PORT_MAPPINGS_SERVICE_TYPE_INTERNAL_LABEL: 'Internal', MSG_PORT_MAPPINGS_SERVICE_TYPE_EXTERNAL_LABEL: 'External', }; interface ServiceType { label: string; external: boolean; } const NO_SERVICE: ServiceType = { label: i18n.MSG_PORT_MAPPINGS_SERVICE_TYPE_NONE_LABEL, external: false, }; const INT_SERVICE: ServiceType = { label: i18n.MSG_PORT_MAPPINGS_SERVICE_TYPE_INTERNAL_LABEL, external: false, }; const EXT_SERVICE: ServiceType = { label: i18n.MSG_PORT_MAPPINGS_SERVICE_TYPE_EXTERNAL_LABEL, external: true, }; @Component({ selector: 'kd-port-mappings', templateUrl: './template.html', styleUrls: ['./style.scss'], providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => PortMappingsComponent), multi: true, }, { provide: NG_ASYNC_VALIDATORS, useExisting: forwardRef(() => PortMappingsComponent), multi: true, }, ], }) export class PortMappingsComponent implements OnInit, ControlValueAccessor { @Input() protocols: string[]; @Input() isExternal: boolean; @Output() changeExternal: EventEmitter = new EventEmitter(); serviceTypes: ServiceType[]; portMappingForm: FormGroup; constructor(private readonly fb_: FormBuilder, private readonly http_: HttpClient) {} ngOnInit(): void { this.serviceTypes = [NO_SERVICE, INT_SERVICE, EXT_SERVICE]; this.portMappingForm = this.fb_.group({ serviceType: NO_SERVICE, portMappings: this.fb_.array([]), }); this.serviceType.valueChanges.subscribe(() => { this.changeServiceType(); }); this.portMappingForm.valueChanges.subscribe(v => { this.propagateChange(v); }); } validate(_: FormControl): Observable<{[key: string]: boolean} | null> { return this.portMappingForm.statusChanges.pipe( startWith(this.portMappingForm.status), first(() => !this.portMappingForm.pending), map(() => { return this.portMappingForm.invalid ? {error: true} : null; }) ); } changeServiceType(): void { // add or remove port mappings if (this.serviceType.value === NO_SERVICE) { const length = this.portMappings.length; for (let i = 0; i < length; i++) { this.portMappings.removeAt(0); } } else if (this.portMappings.length === 0) { this.portMappings.push(this.newEmptyPortMapping(this.protocols[0])); } // set flag this.isExternal = this.serviceType.value.external; this.changeExternal.emit(this.isExternal); for (let i = 0; i < this.portMappings.length; i++) { const ele = this.portMappings.at(i).get('protocol'); ele.clearAsyncValidators(); ele.setAsyncValidators(validateProtocol(this.http_, this.isExternal)); ele.updateValueAndValidity(); } } get portMappings(): FormArray { return this.portMappingForm.get('portMappings') as FormArray; } get serviceType(): AbstractControl { return this.portMappingForm.get('serviceType') as AbstractControl; } private newEmptyPortMapping(defaultProtocol: string): FormGroup { return this.fb_.group({ port: ['', Validators.compose([FormValidators.isInteger, Validators.min(1), Validators.max(65535)])], targetPort: ['', Validators.compose([FormValidators.isInteger, Validators.min(1), Validators.max(65535)])], protocol: [defaultProtocol], }); } /** * Call checks on port mapping: * - adds new port mapping when last empty port mapping has been filled * - validates port mapping */ checkPortMapping(portMappingIndex: number): void { this.addProtocolIfNeeed(); this.validatePortMapping(portMappingIndex); this.portMappings.updateValueAndValidity(); } addProtocolIfNeeed(): void { const lastPortMapping = this.portMappings.controls[this.portMappings.length - 1]; if (this.isPortMappingFilled(lastPortMapping)) { this.portMappings.push(this.newEmptyPortMapping(this.protocols[0])); } } /** * Returns true when the given port mapping is filled by the user, i.e., is not empty. */ private isPortMappingFilled(portMapping: AbstractControl): boolean { return !!portMapping.get('port').value && !!portMapping.get('targetPort').value; } /** * Validates port mapping. In case when only one port is specified it is considered as invalid. */ private validatePortMapping(portIndex: number): void { if (portIndex === 0) { return; } const portMapping = this.portMappings.at(portIndex); const portElem = portMapping.get('port'); const targetPortElem = portMapping.get('targetPort'); const port = portElem.value; const targetPort = targetPortElem.value; const filledOrEmpty = this.isPortMappingFilledOrEmpty(port, targetPort); const isValidPort = filledOrEmpty || !!port; const isValidTargetPort = filledOrEmpty || !!targetPort; portElem.setErrors(isValidPort ? null : {required: true}); targetPortElem.setErrors(isValidTargetPort ? null : {required: true}); this.portMappingForm.updateValueAndValidity(); } /** * Returns true when the given port mapping is filled or empty (both ports), false otherwise. */ private isPortMappingFilledOrEmpty(port: number, targetPort: number): boolean { return !port === !targetPort; } isRemovable(index: number): boolean { return index !== this.portMappings.length - 1; } remove(index: number): void { this.portMappings.removeAt(index); } /** * Returns true if the given port mapping is the first in the list. * @param {number} index */ isFirst(index: number): boolean { return index === 0; } propagateChange = (_: {portMappings: PortMapping[]}) => {}; writeValue(): void {} registerOnChange(fn: (_: {portMappings: PortMapping[]}) => void): void { this.propagateChange = fn; } registerOnTouched(): void {} }