Skip to content

Commit

Permalink
refactor(titus/serverGroup): Reactify Titus Resize Server Group Modal (
Browse files Browse the repository at this point in the history
…#7175)

* refactor(titus/serverGroup): Reactify Titus Resize Server Group Modal
Also reactify the server group capacity details section.
  • Loading branch information
christopherthielen committed Jul 3, 2019
1 parent 4d9a192 commit 20df06e
Show file tree
Hide file tree
Showing 11 changed files with 415 additions and 266 deletions.
1 change: 1 addition & 0 deletions app/scripts/modules/titus/src/domain/ITitusServerGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export interface ITitusServerGroup extends IServerGroup {
image?: ITitusImage;
scalingPolicies?: ITitusPolicy[];
targetGroups?: string[];
capacityGroup?: string;
}

export interface ITitusImage {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import * as React from 'react';

import { ReactModal, Application, Overridable } from '@spinnaker/core';
import { ITitusServerGroup } from 'titus/domain';
import { ITitusResizeServerGroupModalProps, TitusResizeServerGroupModal } from './resize/TitusResizeServerGroupModal';

interface ICapacityDetailsSectionProps {
app: Application;
serverGroup: ITitusServerGroup;
}

export const TitusSimpleMinMaxDesired = ({ serverGroup }: ICapacityDetailsSectionProps) => (
<>
<dt>Min/Max</dt>
<dd>{serverGroup.capacity.desired}</dd>
<dt>Current</dt>
<dd>{serverGroup.instances.length}</dd>
</>
);

export const TitusAdvancedMinMaxDesired = ({ serverGroup }: ICapacityDetailsSectionProps) => (
<>
<dt>Min</dt>
<dd>{serverGroup.capacity.min}</dd>
<dt>Desired</dt>
<dd>{serverGroup.capacity.desired}</dd>
<dt>Max</dt>
<dd>{serverGroup.capacity.max}</dd>
<dt>Current</dt>
<dd>{serverGroup.instances.length}</dd>
</>
);

export const TitusCapacityGroup = ({ serverGroup }: ICapacityDetailsSectionProps) => (
<>
<dt>Cap. Group</dt>
<dd>{serverGroup.capacityGroup}</dd>
</>
);

@Overridable('titus.serverGroup.CapacityDetailsSection')
export class TitusCapacityDetailsSection extends React.Component<ICapacityDetailsSectionProps> {
public render(): JSX.Element {
const { serverGroup, app: application } = this.props;
const isSimpleMode = serverGroup.capacity.min === serverGroup.capacity.max;
const resizeServerGroup = () =>
ReactModal.show<ITitusResizeServerGroupModalProps>(TitusResizeServerGroupModal, { serverGroup, application });

return (
<>
<dl className="dl-horizontal dl-flex">
{isSimpleMode ? <TitusSimpleMinMaxDesired {...this.props} /> : <TitusAdvancedMinMaxDesired {...this.props} />}
{serverGroup.capacityGroup && <TitusCapacityGroup {...this.props} />}
</dl>

<div>
<a className="clickable" onClick={resizeServerGroup}>
Resize Server Group
</a>
</div>
</>
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { module } from 'angular';
import { react2angular } from 'react2angular';

import { TitusCapacityDetailsSection } from './TitusCapacityDetailsSection';

export const TITUS_SERVERGROUP_DETAILS_CAPACITYDETAILSSECTION = 'titus.servergroup.details.capacitydetailssection';
module(TITUS_SERVERGROUP_DETAILS_CAPACITYDETAILSSECTION, []).component(
'titusCapacityDetailsSection',
react2angular(TitusCapacityDetailsSection, ['serverGroup', 'app']),
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
import * as React from 'react';
import { Modal } from 'react-bootstrap';
import { Form, Formik, FormikContext } from 'formik';
import { ITitusServerGroup } from 'titus/domain';
import {
Application,
FormikFormField,
ICapacity,
IModalComponentProps,
MinMaxDesiredChanges,
ModalClose,
NgReact,
NumberInput,
PlatformHealthOverride,
ReactInjector,
UserVerification,
ValidationMessage,
} from '@spinnaker/core';
import { useTaskMonitor } from 'titus/serverGroup/details/resize/useTaskMonitor';

const { useState, useEffect, useMemo } = React;

export interface ITitusResizeServerGroupModalProps extends IModalComponentProps {
application: Application;
serverGroup: ITitusServerGroup;
}

interface ITitusResizeServerGroupCommand {
capacity: ICapacity;
serverGroupName: string;
instances: number;
interestingHealthProviderNames: string[];
region: string;
}

function surfacedErrorMessage(formik: FormikContext<ITitusResizeServerGroupCommand>) {
const capacityErrors = formik.errors.capacity || ({} as any);
const { min, max, desired } = capacityErrors;
return [min, max, desired].find(x => !!x);
}

function SimpleMode({ formik, serverGroup, toggleMode }: IAdvancedModeProps) {
useEffect(() => {
formik.setFieldValue('capacity.min', formik.values.capacity.desired);
formik.setFieldValue('capacity.max', formik.values.capacity.desired);
}, [formik.values.capacity.desired]);

const errorMessage = surfacedErrorMessage(formik);

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={toggleMode}>
Advanced Mode
</a>
.
</p>

<div className="form-group row">
<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={serverGroup.capacity.desired}
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="capacity.desired"
input={props => <NumberInput {...props} min={0} />}
layout={({ input }) => <>{input}</>}
touched={true}
onChange={() => {}}
/>
<div className="sp-padding-xs-xaxis">instances</div>
</div>
</div>
</div>

{!!errorMessage && (
<div className="col-md-offset-3 col-md-9">
<ValidationMessage message={errorMessage} type="error" />
</div>
)}

<div className="form-group">
<div className="col-md-3 sm-label-right">Changes</div>
<div className="col-md-9 sm-control-field">
<MinMaxDesiredChanges current={serverGroup.capacity} next={formik.values.capacity} />
</div>
</div>
</div>
);
}

interface IAdvancedModeProps {
formik: FormikContext<ITitusResizeServerGroupCommand>;
serverGroup: ITitusServerGroup;
toggleMode: () => void;
}

function AdvancedMode({ formik, serverGroup, toggleMode }: IAdvancedModeProps) {
const { min, max } = formik.values.capacity || ({} as any);

const DisabledNumberField = ({ value }: { value: string | number }) => (
<div className="col-md-2">
<input className="NumberInput form-control" type="number" disabled={true} value={value} />
</div>
);
const errorMessage = surfacedErrorMessage(formik);

return (
<div>
<p>Sets up auto-scaling for this server group.</p>
<p>
To disable auto-scaling, use the{' '}
<a className="clickable" onClick={toggleMode}>
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>
<DisabledNumberField value={serverGroup.capacity.min} />
<DisabledNumberField value={serverGroup.capacity.max} />
<DisabledNumberField value={serverGroup.capacity.desired} />
</div>

<div className="form-group">
<div className="col-md-3 sm-label-right">Resize to</div>
<div className="col-md-2">
<FormikFormField
fastField={false}
name="capacity.min"
input={props => <NumberInput {...props} min={0} max={max} />}
layout={({ input }) => <>{input}</>}
touched={true}
/>
</div>

<div className="col-md-2">
<FormikFormField
fastField={false}
name="capacity.max"
input={props => <NumberInput {...props} min={min} />}
layout={({ input }) => <>{input}</>}
touched={true}
/>
</div>

<div className="col-md-2">
<FormikFormField
fastField={false}
name="capacity.desired"
input={props => <NumberInput {...props} min={min} max={max} />}
layout={({ input }) => <>{input}</>}
touched={true}
/>
</div>
</div>

{!!errorMessage && (
<div className="col-md-offset-3 col-md-9">
<ValidationMessage message={errorMessage} type="error" />
</div>
)}

<div className="form-group">
<div className="col-md-3 sm-label-right">Changes</div>
<div className="col-md-9 sm-control-field">
<MinMaxDesiredChanges current={serverGroup.capacity} next={formik.values.capacity} />
</div>
</div>
</div>
);
}

function validateResizeCommand(values: ITitusResizeServerGroupCommand) {
const { min, max, desired } = values.capacity;
const capacityErrors = {} as any;

// try to only show one error message at a time
if (min > max) {
capacityErrors.min = capacityErrors.max = 'Min cannot be larger than Max';
} else if (desired < min) {
capacityErrors.desired = capacityErrors.min = 'Desired cannot be smaller than Min';
} else if (desired > max) {
capacityErrors.desired = capacityErrors.max = 'Desired cannot be larger than Max';
}

if (Object.keys(capacityErrors).length) {
return { capacity: capacityErrors };
}

return {};
}

export function TitusResizeServerGroupModal(props: ITitusResizeServerGroupModalProps) {
const { TaskMonitorWrapper } = NgReact;
const { serverGroup, application, dismissModal } = props;

const initialAdvancedMode = useMemo(() => {
const { min, max, desired } = serverGroup.capacity;
return desired !== max || desired !== min;
}, []);
const [advancedMode, setAdvancedMode] = useState(initialAdvancedMode);

const platformHealthOnlyShowOverride =
application.attributes && application.attributes.platformHealthOnlyShowOverride;
const [verified, setVerified] = useState();

const taskMonitor = useTaskMonitor(
{
application,
title: `Resizing ${serverGroup.name}`,
onTaskComplete: () => application.getDataSource('serverGroups').refresh(true),
},
dismissModal,
);
const submit = (command: ITitusResizeServerGroupCommand) =>
taskMonitor.submit(() => ReactInjector.serverGroupWriter.resizeServerGroup(serverGroup, application, command));

const initialValues = { capacity: serverGroup.capacity } as ITitusResizeServerGroupCommand;

return (
<>
<TaskMonitorWrapper monitor={taskMonitor} />

<Formik<ITitusResizeServerGroupCommand>
initialValues={initialValues}
validate={validateResizeCommand}
onSubmit={submit}
render={formik => {
return (
<>
<Modal.Header>
<h3>Resize {serverGroup.name}</h3>
</Modal.Header>

<ModalClose dismiss={dismissModal} />

<Modal.Body>
<Form className="form-horizontal">
{advancedMode ? (
<AdvancedMode formik={formik} serverGroup={serverGroup} toggleMode={() => setAdvancedMode(false)} />
) : (
<SimpleMode formik={formik} serverGroup={serverGroup} toggleMode={() => setAdvancedMode(true)} />
)}

{platformHealthOnlyShowOverride && (
<PlatformHealthOverride
interestingHealthProviderNames={formik.values.interestingHealthProviderNames}
platformHealthType="Titus"
showHelpDetails={true}
onChange={names =>
formik.setFieldValue('interestingHealthProviderNames', names ? names : undefined)
}
/>
)}
</Form>
</Modal.Body>

<Modal.Footer>
<UserVerification account={serverGroup.account} onValidChange={setVerified} />

<button className="btn btn-default" onClick={dismissModal}>
Cancel
</button>

<button
type="submit"
disabled={!verified || !formik.isValid}
className="btn btn-primary"
onClick={() => submit(formik.values)}
>
Submit
</button>
</Modal.Footer>
</>
);
}}
/>
</>
);
}
Loading

0 comments on commit 20df06e

Please sign in to comment.