import type { FormikErrors, FormikProps } from 'formik'; import { get, isEqual, partition, uniq } from 'lodash'; import React from 'react'; import VirtualizedSelect from 'react-virtualized-select'; import { combineLatest as observableCombineLatest, Subject } from 'rxjs'; import { distinctUntilChanged, map, mergeMap, switchMap, takeUntil, tap, withLatestFrom } from 'rxjs/operators'; import type { ISecurityGroup, IWizardPageComponent } from '@spinnaker/core'; import { FirewallLabels, InfrastructureCaches, ReactInjector, Spinner, timestamp } from '@spinnaker/core'; import { AWSProviderSettings } from '../../../aws.settings'; import type { IAmazonLoadBalancerUpsertCommand } from '../../../domain'; export interface ISecurityGroupsProps { formik: FormikProps; isNew?: boolean; onLoadingChanged(isLoading: boolean): void; } export interface ISecurityGroupsState { availableSecurityGroups: Array<{ label: string; value: string }>; defaultSecurityGroups: string[]; loaded: boolean; refreshing: boolean; removed: string[]; refreshTime: number; } export class SecurityGroups extends React.Component implements IWizardPageComponent { private destroy$ = new Subject(); private props$ = new Subject(); private refresh$ = new Subject(); constructor(props: ISecurityGroupsProps) { super(props); const defaultSecurityGroups = get(AWSProviderSettings, 'defaultSecurityGroups', []); this.state = { availableSecurityGroups: [], defaultSecurityGroups, loaded: false, refreshing: false, removed: [], refreshTime: InfrastructureCaches.get('securityGroups').getStats().ageMax, }; } public validate(): FormikErrors { const { removed } = this.state; if (removed && removed.length) { const label = FirewallLabels.get('Firewalls'); return { securityGroupsRemoved: `${label} removed: ${removed.join(', ')}` }; } return {}; } private clearRemoved = (): void => { this.setState({ removed: [] }, () => this.props.formik.validateForm()); }; private updateRemovedSecurityGroups(selectedGroups: string[], availableGroups: ISecurityGroup[]): void { const { isNew } = this.props; const { defaultSecurityGroups, removed } = this.state; const getDesiredGroupNames = (): string[] => { const desired = selectedGroups.concat(removed).sort(); const defaults = isNew ? defaultSecurityGroups : []; return uniq(defaults.concat(desired)); }; const getAvailableSecurityGroup = (name: string) => availableGroups.find((sg) => sg.name === name || sg.id === name); // Organize selected security groups into available/not available const [available, notAvailable] = partition(getDesiredGroupNames(), (name) => !!getAvailableSecurityGroup(name)); // Normalize available security groups from [name or id] to name const securityGroups = available.map((name) => getAvailableSecurityGroup(name).name); if (!isEqual(selectedGroups, securityGroups)) { this.props.formik.setFieldValue('securityGroups', securityGroups); } this.setState({ removed: notAvailable }, () => this.props.formik.validateForm()); } private handleSecurityGroupsChanged = (newValues: Array<{ label: string; value: string }>): void => { this.props.formik.setFieldValue( 'securityGroups', newValues.map((sg) => sg.value), ); }; private onRefreshStart() { this.props.onLoadingChanged(true); this.setState({ refreshing: true }); } private onRefreshComplete() { this.props.onLoadingChanged(false); const refreshTime = InfrastructureCaches.get('securityGroups').getStats().ageMax; this.setState({ refreshing: false, loaded: true, refreshTime }); } public componentDidMount(): void { const allSecurityGroups$ = this.refresh$.pipe( tap(() => this.onRefreshStart()), switchMap(() => ReactInjector.cacheInitializer.refreshCache('securityGroups')), mergeMap(() => ReactInjector.securityGroupReader.getAllSecurityGroups()), tap(() => this.onRefreshComplete()), ); const formValues$ = this.props$.pipe(map((props) => props.formik.values)); const vpcId$ = formValues$.pipe( map((values) => values.vpcId), distinctUntilChanged(), ); const availableSecurityGroups$ = observableCombineLatest([vpcId$, allSecurityGroups$]).pipe( withLatestFrom(formValues$), map(([[vpcId, allSecurityGroups], formValues]) => { const forAccount = allSecurityGroups[formValues.credentials] || {}; const forRegion = (forAccount.aws && forAccount.aws[formValues.region]) || []; return forRegion.filter((securityGroup) => vpcId === securityGroup.vpcId).sort(); }), ); availableSecurityGroups$ .pipe(withLatestFrom(formValues$), takeUntil(this.destroy$)) .subscribe(([availableSecurityGroups, formValues]) => { const makeOption = (sg: ISecurityGroup) => ({ label: `${sg.name} (${sg.id})`, value: sg.name }); this.setState({ availableSecurityGroups: availableSecurityGroups.map(makeOption) }); this.updateRemovedSecurityGroups(formValues.securityGroups, availableSecurityGroups); }); this.refresh$.next(); } public componentDidUpdate(): void { this.props$.next(this.props); } public componentWillUnmount() { this.destroy$.next(); this.destroy$.complete(); } public render() { const { securityGroups } = this.props.formik.values; const { availableSecurityGroups, loaded, refreshing, removed, refreshTime } = this.state; return (
{removed.length > 0 && (

The following {FirewallLabels.get('firewalls')} could not be found in the selected account/region/VPC and were removed:

    {removed.map((sg) => (
  • {sg}
  • ))}

Okay

)}
{FirewallLabels.get('Firewalls')}
{!loaded && (
)} {loaded && ( )}

{refreshing && ( {' '} )} {FirewallLabels.get('Firewalls')} {!refreshing && last refreshed {timestamp(refreshTime)}} {refreshing && refreshing...}

If you're not finding a {FirewallLabels.get('firewall')} that was recently added,{' '} this.refresh$.next()}> click here {' '} to refresh the list.

); } }