import type { Formik , FormikErrors , FormikProps } from 'formik' ; import { Form } from 'formik' ; import { pickBy } from 'lodash' ; import React from 'react' ; import { Modal } from 'react-bootstrap' ; import type { Application , ICapacity , IModalComponentProps , IServerGroupJob } from '@spinnaker/core' ; import { CheckboxInput , confirmNotManaged , FormikFormField , HelpField , MinMaxDesiredChanges , ModalClose , noop , NumberInput , PlatformHealthOverride , ReactInjector , ReactModal , SpinFormik , TaskMonitor , TaskMonitorWrapper , TaskReason , ValidationMessage , } from '@spinnaker/core' ; import { AwsModalFooter } from '../../../common' ; import type { IAmazonServerGroup } from '../../../domain' ; export interface IAmazonResizeServerGroupModalProps extends IModalComponentProps { application : Application ; serverGroup : IAmazonServerGroup ; } export interface IAmazonResizeServerGroupModalState { advancedMode : boolean ; interestingHealthProviderNames : string [ ] ; initialValues : IAmazonResizeServerGroupValues ; taskMonitor : TaskMonitor ; platformHealthOnlyShowOverride ? : boolean ; } export interface IAmazonResizeServerGroupValues { min : number ; max : number ; desired : number ; enforceCapacityConstraints : boolean ; reason ? : string ; } export interface IResizeJob extends IServerGroupJob { constraints ? : { capacity : ICapacity ; } ; reason ? : string ; interestingHealthProviderNames : string [ ] ; } export class AmazonResizeServerGroupModal extends React . Component < IAmazonResizeServerGroupModalProps , IAmazonResizeServerGroupModalState > { public static defaultProps : Partial < IAmazonResizeServerGroupModalProps > = { closeModal : noop , dismissModal : noop , } ; private formikRef = React . createRef < Formik < IAmazonResizeServerGroupValues > > ( ) ; public static show ( props ) { const modalProps = { } ; const { serverGroup , application } = props ; return confirmNotManaged ( serverGroup , application ) . then ( ( notManaged ) => { notManaged && ReactModal . show ( AmazonResizeServerGroupModal , props , modalProps ) ; } ) ; } constructor ( props ) { super ( props ) ; const { minSize , maxSize , desiredCapacity } = props . serverGroup . asg ; const { attributes } = props . application ; this . state = { advancedMode : minSize !== maxSize , initialValues : { min : minSize , max : maxSize , desired : desiredCapacity , enforceCapacityConstraints : false , } , taskMonitor : new TaskMonitor ( { application : props . application , title : 'Resizing your server group' , modalInstance : TaskMonitor . modalInstanceEmulation ( ( ) => this . props . dismissModal ( ) ) , onTaskComplete : ( ) => this . props . application . serverGroups . refresh ( ) , } ) , platformHealthOnlyShowOverride : attributes . platformHealthOnlyShowOverride , interestingHealthProviderNames : attributes . platformHealthOnlyShowOverride && attributes . platformHealthOnly ? [ 'Amazon' ] : null , } ; } private validate = ( values ) : Partial < FormikErrors < IAmazonResizeServerGroupValues > > => { const { min , max , desired } = values ; const errors = { } ; if ( ! this . state . advancedMode ) { return errors ; } if ( min > max && min > desired ) { errors . min = 'Min cannot be larger than Max/Desired' ; } else if ( max < min && max < desired ) { errors . max = 'Max cannot be smaller than Min/Desired' ; } else { if ( min > max ) { errors . min = 'Min cannot be larger than Max' ; } if ( ! this . isDesiredControlledByAutoscaling ( ) ) { if ( desired < min ) { errors . desired = 'Desired cannot be smaller than Min' ; } if ( desired > max ) { errors . desired = 'Desired cannot be larger than Max' ; } } } return errors ; } ; private toggleAdvancedMode = ( ) : void => { const { desired } = this . formikRef . current . getFormikContext ( ) . values ; this . formikRef . current . setFieldValue ( 'min' , desired ) ; this . formikRef . current . setFieldValue ( 'max' , desired ) ; this . setState ( { advancedMode : ! this . state . advancedMode } ) ; } ; private close = ( args ? ) : void => { this . props . dismissModal . apply ( null , args ) ; } ; private autoIncrementDesiredIfNeeded ( ) { if ( ! this . isDesiredControlledByAutoscaling ( ) ) { return ; } const formik = this . formikRef . current . getFormikContext ( ) ; const { asg } = this . props . serverGroup ; const { min , max , desired } = formik . values ; const newDesired = Math . min ( max , Math . max ( min , asg . desiredCapacity ) ) ; if ( desired !== newDesired ) { formik . setFieldValue ( 'desired' , newDesired ) ; } } private platformHealthOverrideChanged = ( interestingHealthProviderNames ) => { this . setState ( { interestingHealthProviderNames } ) ; } ; private isDesiredControlledByAutoscaling = ( ) : boolean => { const { serverGroup } = this . props ; const { suspendedProcesses } = serverGroup . asg ; const { advancedMode } = this . state ; const scalingPolicies = serverGroup . scalingPolicies || [ ] ; return ( scalingPolicies . length && advancedMode && suspendedProcesses . every ( ( p ) => p . processName !== 'AlarmNotification' ) ) ; } ; private submit = ( values ) : void => { const { min , max , desired , enforceCapacityConstraints , reason } = values ; const { interestingHealthProviderNames } = this . state ; const { serverGroup , application } = this . props ; const { asg } = serverGroup ; const capacity = pickBy ( { min : min !== asg . minSize ? min : undefined , max : max !== asg . maxSize ? max : undefined , desired : desired !== asg . desiredCapacity ? desired : undefined , } , ( x ) => x !== undefined ) ; const command = { capacity , reason , interestingHealthProviderNames , } ; if ( enforceCapacityConstraints ) { command . constraints = { capacity : { min : asg . minSize , max : asg . maxSize , desired : asg . desiredCapacity , } , } ; } this . state . taskMonitor . submit ( ( ) => { return ReactInjector . serverGroupWriter . resizeServerGroup ( serverGroup , application , command ) ; } ) ; } ; private renderSimpleMode ( formik ) { const { serverGroup } = this . props ; const { asg } = serverGroup ; return ( < div > < p > Sets min , max , and desired instance counts to the same value . < /p> < p > To allow autoscaling , use the { ' ' } < a className = "clickable" onClick = { ( ) => this . toggleAdvancedMode ( ) } > Advanced Mode < /a> . < /p> < div className = "form-group" > < div className = "col-md-3 sm-label-right" > Current size < /div> < div className = "col-md-4" > < div className = "horizontal middle" > < input type = "number" className = "NumberInput form-control" value = { asg . desiredCapacity } disabled = { true } / > < div className = "sp-padding-xs-xaxis" > instances < /div> < /div> < /div> < /div> < div className = "form-group" > < div className = "col-md-3 sm-label-right" > Resize to < /div> < div className = "col-md-4" > < div className = "horizontal middle" > < FormikFormField name = "desired" input = { ( props ) => < NumberInput { ... props } min = { 0 } / > } touched = { true } onChange = { ( value ) => { formik . setFieldValue ( 'min' , value ) ; formik . setFieldValue ( 'max' , value ) ; } } /> < div className = "sp-padding-xs-xaxis" > instances < /div> < /div> < /div> < /div> < /div>); } private renderAdvancedMode ( formik ) { const { serverGroup } = this . props ; const { errors } = formik ; const { asg } = serverGroup ; const surfacedErrorMessage = errors . min || errors . max || errors . desired ; return ( < div > < p > Sets up autoscaling for this server group . < /p> < p > To disable autoscaling , use the { ' ' } < a className = "clickable" onClick = { ( ) => this . toggleAdvancedMode ( ) } > Simple Mode < /a> . < /p> < div className = "form-group bold" > < div className = "col-md-2 col-md-offset-3" > Min < /div> < div className = "col-md-2" > Max < /div> < div className = "col-md-2" > Desired < /div> < /div> < div className = "form-group" > < div className = "col-md-3 sm-label-right" > Current < /div> < div className = "col-md-2" > < input type = "number" className = "NumberInput form-control" value = { asg . minSize } disabled = { true } / > < /div> < div className = "col-md-2" > < input type = "number" className = "NumberInput form-control" value = { asg . maxSize } disabled = { true } / > < /div> < div className = "col-md-2" > < input type = "number" className = "NumberInput form-control" value = { asg . desiredCapacity } disabled = { true } / > < /div> < /div> < div className = "form-group" > < div className = "col-md-3 sm-label-right" > Resize to < /div> < div className = "col-md-2" > < FormikFormField name = "min" input = { ( props ) => < NumberInput { ... props } min = { 0 } / > } onChange = { ( ) => this . autoIncrementDesiredIfNeeded ( ) } layout = { ( { input } ) => < > { input } < />} touched={true}/ > < /div> < div className = "col-md-2" > < FormikFormField name = "max" input = { ( props ) => < NumberInput { ... props } min = { 0 } / > } onChange = { ( ) => this . autoIncrementDesiredIfNeeded ( ) } layout = { ( { input } ) => < > { input } < />} touched={true}/ > < /div> < div className = "col-md-2" > < FormikFormField name = "desired" input = { ( props ) => < NumberInput { ... props } min = { 0 } disabled = { this . isDesiredControlledByAutoscaling ( ) } / > } layout = { ( { input } ) => < > { input } < />} touched={true}/ > < /div> < /div> { ! ! surfacedErrorMessage && ( < div className = "col-md-offset-3 col-md-9" > < ValidationMessage message = { surfacedErrorMessage } type = "error" / > < /div>)} < /div>); } private renderCapacityConstraintSelector ( ) { return ( < div className = "form-group" > < div className = "col-md-7 col-md-offset-3" > < FormikFormField name = "enforceCapacityConstraints" input = { ( props ) => ( < > < CheckboxInput { ... props } text = "Enforce Capacity Constraints" / > < HelpField id = "aws.serverGroup.capacityConstraint" / > < />)}/ > < /div> < /div>); } private renderScalingPolicyWarning ( formik ) { const { serverGroup } = this . props ; const { min , max } = formik . values ; const { advancedMode } = this . state ; const scalingPolicies = serverGroup . scalingPolicies || [ ] ; if ( scalingPolicies . length && min === max ) { return ( < div className = "form-group" > < div className = "col-md-7 col-md-offset-3" > < div className = "well-compact alert alert-warning" > < b > Warning < /b>: this server group has { scalingPolicies . length === 1 && < span > a scaling policy . < /span>} { scalingPolicies . length > 1 && < span > scaling policies . < /span>} { ! advancedMode && ( < span > Scaling policies will not take effect in Simple Mode . Switch to { ' ' } < a className = "clickable" onClick = { ( ) => this . toggleAdvancedMode ( ) } > Advanced Mode < /a> . < /span>)} { advancedMode && ( < span > Scaling policies will not take effect when < b > Min < /b> is the same as <b>Max</b > . < /span>)} < /div> < /div> < /div>); } if ( this . isDesiredControlledByAutoscaling ( ) ) { return ( < div className = "form-group" > < div className = "col-md-8 col-md-offset-1" > < div className = "well-compact alert alert-warning" > < p > < b > Desired < /b> capacity is managed by Autoscaling Policies. < /p> < p > If you need to scale < b > down < /b> this server group, set <b>Max</b > to the new desired size . < /p> < p > If you need to scale < b > up < /b> this server group, set <b>Min</b > to the new desired size . < /p> < /div> < /div> < /div>); } return null ; } public render ( ) { const { serverGroup } = this . props ; const { advancedMode , initialValues , platformHealthOnlyShowOverride } = this . state ; return ( < > < TaskMonitorWrapper monitor = { this . state . taskMonitor } / > < SpinFormik < IAmazonResizeServerGroupValues > ref = { this . formikRef } initialValues = { initialValues } validate = { this . validate } onSubmit = { this . submit } render = { ( formik ) => { const { asg } = serverGroup ; const currentCapacity = { min : asg . minSize , max : asg . maxSize , desired : asg . desiredCapacity } ; const nextCapacity = formik . values ; return ( < > < ModalClose dismiss = { this . close } / > < Modal . Header > < Modal . Title > Resize { serverGroup . name } < /Modal.Title> < /Modal.Header> < Modal . Body > < Form className = "form-horizontal" > { advancedMode && this . renderAdvancedMode ( formik ) } { ! advancedMode && this . renderSimpleMode ( formik ) } { this . renderScalingPolicyWarning ( formik ) } { this . renderCapacityConstraintSelector ( ) } { platformHealthOnlyShowOverride && ( < div className = "form-group" > < div className = "col-md-8 col-md-offset-3" > < PlatformHealthOverride interestingHealthProviderNames = { this . state . interestingHealthProviderNames } platformHealthType = "Amazon" onChange = { this . platformHealthOverrideChanged } showHelpDetails = { true } / > < /div> < /div>)} < TaskReason reason = { formik . values . reason } onChange = { ( val ) => formik . setFieldValue ( 'reason' , val ) } /> < div className = "form-group" > < div className = "col-md-3 sm-label-right" > Changes < /div> < div className = "col-md-9 sm-control-field" > < MinMaxDesiredChanges current = { currentCapacity } next = { nextCapacity } / > < /div> < /div> < /Form> < /Modal.Body> < AwsModalFooter onSubmit = { ( ) => this . submit ( formik . values ) } onCancel = { this . close } isValid = { formik . isValid } account = { serverGroup . account } / > < />); } } /> < />); } }