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 < IAmazonLoadBalancerUpsertCommand > ; isNew ? : boolean ; onLoadingChanged ( isLoading ) : 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 < ISecurityGroupsProps , ISecurityGroupsState > implements IWizardPageComponent < IAmazonLoadBalancerUpsertCommand > { private destroy$ = new Subject < void > ( ) ; private props$ = new Subject < ISecurityGroupsProps > ( ) ; private refresh$ = new Subject < void > ( ) ; constructor ( props ) { super ( props ) ; const defaultSecurityGroups = get < string [ ] > ( AWSProviderSettings , 'defaultSecurityGroups' , [ ] ) ; this . state = { availableSecurityGroups : [ ] , defaultSecurityGroups , loaded : false , refreshing : false , removed : [ ] , refreshTime : InfrastructureCaches . get ( 'securityGroups' ) . getStats ( ) . ageMax , } ; } public validate ( ) { 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 , availableGroups ) { 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 ) => availableGroups . find ( ( sg ) => sg . name === name || sg . id === name ) ; const [ available , notAvailable ] = partition ( getDesiredGroupNames ( ) , ( name ) => ! ! getAvailableSecurityGroup ( 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 ) : 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 ( ) { 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 ) => ( { label : `${ sg . name } (${ sg . id })` , value : sg . name } ) ; this . setState ( { availableSecurityGroups : availableSecurityGroups . map ( makeOption ) } ) ; this . updateRemovedSecurityGroups ( formValues . securityGroups , availableSecurityGroups ) ; } ) ; this . refresh$ . next ( ) ; } public componentDidUpdate ( ) { 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 ( < div className = "container-fluid form-horizontal" > < div > { removed . length > 0 && ( < div className = "form-group" > < div className = "col-md-12" > < div className = "alert alert-warning" > < p > < i className = "fa fa-exclamation-triangle" /> The following { FirewallLabels . get ( 'firewalls' ) } could not be found in the selected account / region / VPC and were removed : < /p> < ul > { removed . map ( ( sg ) => ( < li key = { sg } > { sg } < /li>))} < /ul> < p className = "text-right" > < a className = "btn btn-sm btn-default dirty-flag-dismiss clickable" onClick = { this . clearRemoved } > Okay < /a> < /p> < /div> < /div> < /div>)} < div className = "form-group" > < div className = "col-md-3 sm-label-right" > { FirewallLabels . get ( 'Firewalls' ) } < /div> < div className = "col-md-9" > { ! loaded && ( < div style = { { paddingTop : '13px' } } > < Spinner size = "small" / > < /div>)} { loaded && ( < VirtualizedSelect multi = { true } value = { securityGroups } options = { availableSecurityGroups } onChange = { this . handleSecurityGroupsChanged } clearable = { false } / > ) } < /div> < /div> < div className = "form-group small" style = { { marginTop : '20px' } } > < div className = "col-md-9 col-md-offset-3" > < p > { refreshing && ( < span > < span className = "fa fa-sync-alt fa-spin" / > { ' ' } < /span>)} { FirewallLabels . get ( 'Firewalls' ) } { ! refreshing && < span > last refreshed { timestamp ( refreshTime ) } < /span>} { refreshing && < span > refreshing ... < /span>} < /p> < p > If you 're not finding a {FirewallLabels.get(' firewall ')} that was recently added,{' '} < a className = "clickable" onClick = { ( ) => this . refresh$ . next ( ) } > click here < /a>{' '} to refresh the list . < /p> < /div> < /div> < /div> < /div>); } }