import type { FormikErrors, FormikProps } from 'formik'; import { filter, flatten, groupBy, set, uniq } from 'lodash'; import React from 'react'; import { from as observableFrom, Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; import type { Application, IWizardPageComponent } from '@spinnaker/core'; import { CheckboxInput, FormValidator, HelpField, spelNumberCheck, SpelNumberInput, SpInput, ValidationMessage, Validators, } from '@spinnaker/core'; import { isNameInUse, isNameLong } from '../common/targetGroupValidators'; import type { IAmazonApplicationLoadBalancer, IAmazonNetworkLoadBalancerUpsertCommand } from '../../../domain'; export interface ITargetGroupsProps { app: Application; formik: FormikProps; isNew: boolean; loadBalancer: IAmazonApplicationLoadBalancer; } export interface ITargetGroupsState { existingTargetGroupNames: { [account: string]: { [region: string]: string[] } }; oldTargetGroupCount: number; } export class TargetGroups extends React.Component implements IWizardPageComponent { public protocols = ['TCP', 'UDP']; public healthProtocols = ['TCP', 'HTTP', 'HTTPS']; public targetTypes = ['instance', 'ip']; private destroy$ = new Subject(); constructor(props: ITargetGroupsProps) { super(props); const oldTargetGroupCount = !props.isNew ? props.formik.initialValues.targetGroups.length : 0; this.state = { existingTargetGroupNames: {}, oldTargetGroupCount, }; } public validate( values: IAmazonNetworkLoadBalancerUpsertCommand, ): FormikErrors { const duplicateTargetGroups = uniq( flatten(filter(groupBy(values.targetGroups, 'name'), (count) => count.length > 1)).map((tg) => tg.name), ); const formValidator = new FormValidator(values); const { arrayForEach } = formValidator; formValidator.field('targetGroups').withValidators( arrayForEach((builder) => { builder .field('name', 'Name') .required() .withValidators( isNameInUse(this.state.existingTargetGroupNames, values.credentials, values.region), isNameLong(this.props.app.name.length), Validators.valueUnique( duplicateTargetGroups, 'There is already a target group in this load balancer with the same name.', ), ); builder .field('port', 'Port') .required() .spelAware() .withValidators((value) => spelNumberCheck(value)); builder .field('healthCheckInterval', 'Health Check Interval') .required() .spelAware() .withValidators((value) => spelNumberCheck(value)); builder .field('healthyThreshold', 'Healthy Threshold') .required() .spelAware() .withValidators((value) => spelNumberCheck(value)); builder .field('unhealthyThreshold', 'Unhealthy Threshold') .required() .spelAware() .withValidators((value) => spelNumberCheck(value)); builder .field('healthCheckPort', 'Health Check Port') .required() .spelAware() .withValidators((value) => (value === 'traffic-port' ? null : spelNumberCheck(value))); builder.field('protocol', 'Protocol').required(); builder.field('healthCheckProtocol', 'Health Check Protocol').required(); }), ); return formValidator.validateForm(); } private removeAppName(name: string): string { return name.replace(`${this.props.app.name}-`, ''); } protected updateLoadBalancerNames(props: ITargetGroupsProps): void { const { app, loadBalancer } = props; const targetGroupsByAccountAndRegion: { [account: string]: { [region: string]: string[] } } = {}; observableFrom(app.getDataSource('loadBalancers').refresh(true)) .pipe(takeUntil(this.destroy$)) .subscribe(() => { app.getDataSource('loadBalancers').data.forEach((lb: IAmazonApplicationLoadBalancer) => { if (lb.loadBalancerType !== 'classic') { if (!loadBalancer || lb.name !== loadBalancer.name) { lb.targetGroups.forEach((targetGroup) => { targetGroupsByAccountAndRegion[lb.account] = targetGroupsByAccountAndRegion[lb.account] || {}; targetGroupsByAccountAndRegion[lb.account][lb.region] = targetGroupsByAccountAndRegion[lb.account][lb.region] || []; targetGroupsByAccountAndRegion[lb.account][lb.region].push(this.removeAppName(targetGroup.name)); }); } } }); this.setState({ existingTargetGroupNames: targetGroupsByAccountAndRegion }, () => this.props.formik.validateForm(), ); }); } private targetGroupFieldChanged(index: number, field: string, value: string | boolean): void { const { setFieldValue, values } = this.props.formik; const targetGroup = values.targetGroups[index]; set(targetGroup, field, value); if (field === 'healthyThreshold') { set(targetGroup, 'unhealthyThreshold', value); } setFieldValue('targetGroups', values.targetGroups); } private addTargetGroup = (): void => { const { setFieldValue, values } = this.props.formik; const tgLength = values.targetGroups.length; values.targetGroups.push({ name: `targetgroup${tgLength ? `${tgLength}` : ''}`, protocol: 'TCP', port: 7001, targetType: 'instance', healthCheckProtocol: 'TCP', healthCheckPort: 'traffic-port', healthCheckPath: '/healthcheck', healthCheckTimeout: 5, healthCheckInterval: 10, healthyThreshold: 10, unhealthyThreshold: 10, attributes: { deregistrationDelay: 300, preserveClientIp: true, }, }); setFieldValue('targetGroups', values.targetGroups); }; private removeTargetGroup(index: number): void { const { setFieldValue, values } = this.props.formik; const { oldTargetGroupCount } = this.state; values.targetGroups.splice(index, 1); if (index < oldTargetGroupCount) { this.setState({ oldTargetGroupCount: oldTargetGroupCount - 1 }); } setFieldValue('targetGroups', values.targetGroups); } public componentDidMount(): void { this.updateLoadBalancerNames(this.props); } public componentWillUnmount(): void { this.destroy$.next(); this.destroy$.complete(); } public render() { const { app } = this.props; const { errors, values } = this.props.formik; const { oldTargetGroupCount } = this.state; const ProtocolOptions = this.protocols.map((p) => ); const HealthProtocolOptions = this.healthProtocols.map((p) => ); const TargetTypeOptions = this.targetTypes.map((p) => ); return (
{values.targetGroups.map((targetGroup, index) => { const tgErrors = (errors.targetGroups && errors.targetGroups[index]) || {}; return (
Group Name
{app.name}- this.targetGroupFieldChanged(index, 'name', event.target.value)} required={true} disabled={index < oldTargetGroupCount} /> this.removeTargetGroup(index)}>
{tgErrors.name && (
)}
Target Type 
Backend Connection
{' '} {' '} this.targetGroupFieldChanged(index, 'port', event.target.value)} type="text" required={true} disabled={index < oldTargetGroupCount} />
Healthcheck
this.targetGroupFieldChanged(index, 'healthCheckPort', event.target.value) } /> {targetGroup.healthCheckProtocol !== 'TCP' && ( this.targetGroupFieldChanged(index, 'healthCheckPath', event.target.value) } /> )} this.targetGroupFieldChanged(index, 'healthCheckTimeout', value) } /> this.targetGroupFieldChanged(index, 'healthCheckInterval', value) } />
{' '} Healthcheck Threshold 
this.targetGroupFieldChanged(index, 'healthyThreshold', value) } />
Attributes
{' '} this.targetGroupFieldChanged( index, 'attributes.deregistrationDelay', event.target.value, ) } /> { this.targetGroupFieldChanged( index, 'attributes.deregistrationDelayConnectionTermination', event.target.checked, ); }} /> {targetGroup.targetType !== 'instance' && ( { this.targetGroupFieldChanged( index, 'attributes.preserveClientIp', event.target.checked, ); }} /> )}
); })}
); } }