Skip to content

Commit

Permalink
Merge pull request #227 from 3pillarlabs/develop
Browse files Browse the repository at this point in the history
Check JMeter and AWS forms for change of state and enable submit button
  • Loading branch information
sayantam authored Jan 28, 2021
2 parents db69cf1 + 7416b90 commit d0875af
Show file tree
Hide file tree
Showing 13 changed files with 209 additions and 102 deletions.
8 changes: 8 additions & 0 deletions hailstorm-web-client/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,11 @@ validate:
git diff ${TRAVIS_COMMIT_RANGE} -- package.json | grep -e '[+\-].*version' > /dev/null; \
[ $$? -eq 0 ]; \
fi

integration_test_bed_up:
cd ../ && docker-compose -f docker-compose.yml -f docker-compose.dc-sim.yml -f docker-compose.web-ci.yml up -d \
hailstorm-api hailstorm-agent-1 hailstorm-agent-2 file-server


integration_test_bed_down:
cd ../ && docker-compose -f docker-compose.yml -f docker-compose.dc-sim.yml -f docker-compose.web-ci.yml down
2 changes: 1 addition & 1 deletion hailstorm-web-client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion hailstorm-web-client/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "hailstorm-web-client",
"version": "1.5.7",
"version": "1.5.8",
"private": true,
"dependencies": {
"date-fns": "^2.6.0",
Expand Down
33 changes: 33 additions & 0 deletions hailstorm-web-client/src/ClusterConfiguration/AWSForm.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,4 +142,37 @@ describe('<AWSForm />', () => {
const action = dispatch.mock.calls[0][0] as {payload: Cluster};
expect(action.payload).toEqual(savedCluster);
});

it('should update number of instances', async () => {
const {findByTestId, debug} = render(createComponent());
await fetchRegions;
await fetchPricing;

const maxPlannedUsers = await findByTestId('MaxPlannedUsers');
fireEvent.change(maxPlannedUsers, {target: {value: '400'}});

const maxThreadsByInst = await findByTestId('Max. Users / Instance');
fireEvent.change(maxThreadsByInst, {target: {value: '80'}});

const numInstances = await findByTestId('# Instances');
expect(numInstances.textContent).toEqual('5');

const hourlyCost = await findByTestId('Hourly Cluster Cost');
expect(hourlyCost.textContent).toMatch(/0.46/);
});

it('should not set number of instances below 1', async () => {
const {findByTestId, debug} = render(createComponent());
await fetchRegions;
await fetchPricing;

const maxPlannedUsers = await findByTestId('MaxPlannedUsers');
fireEvent.change(maxPlannedUsers, {target: {value: '5000'}});

const maxThreadsByInst = await findByTestId('Max. Users / Instance');
fireEvent.change(maxThreadsByInst, {target: {value: '10000'}});

const numInstances = await findByTestId('# Instances');
expect(numInstances.textContent).toEqual('1');
});
});
8 changes: 4 additions & 4 deletions hailstorm-web-client/src/ClusterConfiguration/AWSForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ export function AWSForm({ dispatch, activeProject }: {
return ApiFactory().awsInstancePricing().list(regionCode);
};

const handleAWSInstanceChange = (value: AWSInstanceChoiceOption) => {
setSelectedInstanceType(value);
if (value.hourlyCostByInstance > 0) {
setHourlyCostByCluster(value.hourlyCostByCluster());
const handleAWSInstanceChange = (choice: AWSInstanceChoiceOption) => {
setSelectedInstanceType(choice);
if (choice.hourlyCostByInstance > 0) {
setHourlyCostByCluster(choice.hourlyCostByCluster());
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { render, mount } from 'enzyme';
import { AWSInstanceChoice } from './AWSInstanceChoice';
import { AWSInstanceChoiceOption } from './domain';
import { render as renderComponent, fireEvent } from '@testing-library/react';
import { Form, Formik } from 'formik';

jest.mock('./NonLinearSlider', () => ({
__esModule: true,
Expand All @@ -15,6 +16,34 @@ describe('<AWSInstanceChoice />', () => {
const advanceModeTrigger = /specify aws instance type/i;
let fetchPricing: Promise<AWSInstanceChoiceOption[]>;

function FormComponent({
fetchPricing,
disabled,
onChange,
regionCode,
hourlyCostByCluster,
key,
setHourlyCostByCluster
}: {
fetchPricing: (s: string) => Promise<AWSInstanceChoiceOption[]>,
disabled?: boolean,
onChange: (c: AWSInstanceChoiceOption) => void,
regionCode: string,
hourlyCostByCluster?: number | undefined,
key?: string | number | undefined,
setHourlyCostByCluster?: React.Dispatch<React.SetStateAction<number | undefined>> | undefined
}) {
return (
<Formik isInitialValid={false} initialValues={{}} onSubmit={jest.fn()}>
<Form>
<AWSInstanceChoice
{...{fetchPricing, disabled, onChange, regionCode, hourlyCostByCluster, key, setHourlyCostByCluster}}
/>
</Form>
</Formik>
)
}

beforeEach(() => {
jest.resetAllMocks();
});
Expand All @@ -28,18 +57,18 @@ describe('<AWSInstanceChoice />', () => {
});

it('should render without crashing', () => {
render(<AWSInstanceChoice onChange={jest.fn()} fetchPricing={jest.fn()} regionCode="us-east-1" />);
render(<FormComponent onChange={jest.fn()} fetchPricing={jest.fn()} regionCode="us-east-1" />);
});

it('should render a component to select max number of users', async () => {
const component = mount(<AWSInstanceChoice onChange={jest.fn()} fetchPricing={() => fetchPricing} regionCode="us-east-1" />);
const component = mount(<FormComponent onChange={jest.fn()} fetchPricing={() => fetchPricing} regionCode="us-east-1" />);
await fetchPricing;
component.update();
expect(component).toContainExactlyOneMatchingElement('NonLinearSlider');
});

it('should show default values', async () => {
const component = mount(<AWSInstanceChoice onChange={jest.fn()} fetchPricing={() => fetchPricing} regionCode="us-east-1" />);
const component = mount(<FormComponent onChange={jest.fn()} fetchPricing={() => fetchPricing} regionCode="us-east-1" />);
await fetchPricing;
component.update();
expect(component.find('NonLinearSlider')).toHaveProp('initialValue');
Expand All @@ -55,7 +84,7 @@ describe('<AWSInstanceChoice />', () => {
hourlyCostByCluster: jest.fn().mockReturnValue(8.34567)
});

const component = mount(<AWSInstanceChoice onChange={jest.fn()} fetchPricing={() => fetchPricing} regionCode="us-east-1" />);
const component = mount(<FormComponent onChange={jest.fn()} fetchPricing={() => fetchPricing} regionCode="us-east-1" />);
await fetchPricing;
component.update();
const onChange = component.find('NonLinearSlider').prop('onChange') as unknown as (value: number) => void;
Expand All @@ -65,7 +94,7 @@ describe('<AWSInstanceChoice />', () => {

it('should switch to advanced mode', async () => {
const {findByText, findByTestId} = renderComponent(
<AWSInstanceChoice onChange={jest.fn()} fetchPricing={() => fetchPricing} regionCode="us-east-1" />
<FormComponent onChange={jest.fn()} fetchPricing={() => fetchPricing} regionCode="us-east-1" />
);

await fetchPricing;
Expand All @@ -78,7 +107,7 @@ describe('<AWSInstanceChoice />', () => {
describe('when in advanced mode', () => {
it('should edit AWS instance type and max users by instance', async () => {
const {findByText, findByTestId, findByDisplayValue} = renderComponent(
<AWSInstanceChoice onChange={jest.fn()} fetchPricing={() => fetchPricing} regionCode="us-east-1" />
<FormComponent onChange={jest.fn()} fetchPricing={() => fetchPricing} regionCode="us-east-1" />
);

await fetchPricing;
Expand All @@ -105,7 +134,7 @@ describe('<AWSInstanceChoice />', () => {
});

const {findByText, findByTestId} = renderComponent(
<AWSInstanceChoice onChange={jest.fn()} fetchPricing={() => fetchPricing} regionCode="us-east-1" />
<FormComponent onChange={jest.fn()} fetchPricing={() => fetchPricing} regionCode="us-east-1" />
);

await fetchPricing;
Expand All @@ -127,7 +156,7 @@ describe('<AWSInstanceChoice />', () => {
it('should not report hourly cost', async () => {
const setHourlyCostByCluster = jest.fn();
const {findByText} = renderComponent(
<AWSInstanceChoice
<FormComponent
onChange={jest.fn()}
fetchPricing={() => fetchPricing}
regionCode="us-east-1"
Expand All @@ -152,7 +181,7 @@ describe('<AWSInstanceChoice />', () => {
hourlyCostByCluster: jest.fn().mockReturnValue(8.34567)
});

const component = mount(<AWSInstanceChoice onChange={jest.fn()} fetchPricing={() => fetchPricing} regionCode="us-east-1" />);
const component = mount(<FormComponent onChange={jest.fn()} fetchPricing={() => fetchPricing} regionCode="us-east-1" />);
await fetchPricing;
component.update();
const onChange = component.find('NonLinearSlider').prop('onChange') as unknown as (value: number) => void;
Expand All @@ -173,7 +202,7 @@ describe('<AWSInstanceChoice />', () => {

it('should be possible to disable the control', async () => {
const component = mount(
<AWSInstanceChoice
<FormComponent
onChange={jest.fn()}
fetchPricing={() => fetchPricing}
regionCode="us-east-1"
Expand Down
60 changes: 42 additions & 18 deletions hailstorm-web-client/src/ClusterConfiguration/AWSInstanceChoice.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import { NonLinearSlider } from './NonLinearSlider';
import { computeChoice, maxThreadsByCluster } from './AWSInstanceCalculator';
import { AWSInstanceChoiceOption } from './domain';
import { Loader } from '../Loader/Loader';
import { Field } from 'formik';

const MIN_VALUE = 50;
const MIN_PLANNED_USERS = 50;
const DEFAULT_THREADS_BY_INST = 10;

export function AWSInstanceChoice({
regionCode,
Expand All @@ -23,13 +25,13 @@ export function AWSInstanceChoice({
}) {
const [pricingData, setPricingData] = useState<AWSInstanceChoiceOption[]>([]);
const [instanceType, setInstanceType] = useState<string>('');
const [maxThreadsByInstance, setMaxThreadsByInstance] = useState<number>(0);
const [numInstances, setNumInstances] = useState<number>(0);
const [maxThreadsByInstance, setMaxThreadsByInstance] = useState<number>(DEFAULT_THREADS_BY_INST);
const [numInstances, setNumInstances] = useState<number>(1);
const [quickMode, setQuickMode] = useState(true);
const [sliderValue, setSliderValue] = useState(MIN_VALUE);
const [maxPlannedThreads, setMaxPlannedThreads] = useState(MIN_PLANNED_USERS);

const handleSliderChange = (value: number, data?: AWSInstanceChoiceOption[]) => {
setSliderValue(value);
setMaxPlannedThreads(value);
const choice = computeChoice(value, pricingData && pricingData.length > 0 ? pricingData : data!);
setInstanceType(choice.instanceType);
setMaxThreadsByInstance(choice.maxThreadsByInstance);
Expand All @@ -45,7 +47,7 @@ export function AWSInstanceChoice({
fetchPricing(regionCode)
.then((data) => {
setPricingData(data);
const choice = handleSliderChange(sliderValue, data);
const choice = handleSliderChange(maxPlannedThreads, data);
setHourlyCostByCluster && setHourlyCostByCluster(choice.hourlyCostByCluster());
})
.catch((reason) => console.error(reason));
Expand All @@ -55,6 +57,31 @@ export function AWSInstanceChoice({
return <Loader />;
}

const handleMaxUsersByInstChange = (event: React.ChangeEvent<HTMLInputElement>) => {
let new_value = DEFAULT_THREADS_BY_INST;
if (event.target.value) {
new_value = parseInt(event.target.value);
} else if (pricingData.length > 0) {
new_value = computeChoice(maxPlannedThreads, pricingData).maxThreadsByInstance;
}

setMaxThreadsByInstance(new_value);

const nextNumInstances = Math.ceil(maxPlannedThreads / new_value);
setNumInstances(nextNumInstances);

const matchingOption = pricingData.find((option) => option.instanceType === instanceType);

const nextOption = new AWSInstanceChoiceOption({
maxThreadsByInstance: new_value,
instanceType,
numInstances: nextNumInstances,
hourlyCostByInstance: matchingOption ? matchingOption.hourlyCostByInstance : undefined
});

onChange(nextOption);
};

return (
<>
<InstanceTypeChoice {...{
Expand All @@ -73,13 +100,7 @@ export function AWSInstanceChoice({
<MaxUsersByInstance
value={maxThreadsByInstance}
{...{disabled}}
onChange={(event: { target: { value: string; }; }) => {
setMaxThreadsByInstance(parseInt(event.target.value));
onChange(new AWSInstanceChoiceOption({
maxThreadsByInstance: parseInt(event.target.value),
instanceType
}));
}}
onChange={handleMaxUsersByInstChange}
/>

{quickMode && (<InstanceTypeMeter {...{
Expand Down Expand Up @@ -157,11 +178,11 @@ function InstanceTypeByUsage({
<div className="field">
<div className="control">
<NonLinearSlider
initialValue={MIN_VALUE}
initialValue={MIN_PLANNED_USERS}
onChange={handleSliderChange}
step={50}
maximum={maxThreadsByCluster(pricingData)}
minimum={MIN_VALUE}
minimum={MIN_PLANNED_USERS}
{...{ disabled }}
/>
</div>
Expand Down Expand Up @@ -219,7 +240,7 @@ function InstanceTypeInput({
</div>
{!disabled && (<SwitchMessage
onClick={() => {
handleSliderChange(MIN_VALUE);
handleSliderChange(MIN_PLANNED_USERS);
setQuickMode(true);
}}
>
Expand All @@ -246,7 +267,7 @@ function InstanceTypeMeter({
<CenteredLevelItem title="# Instances">
{numInstances}
</CenteredLevelItem>
{hourlyCostByCluster && (<CenteredLevelItem title="Cluster Cost" starred={true}>
{hourlyCostByCluster && (<CenteredLevelItem title="Hourly Cluster Cost" starred={true}>
${hourlyCostByCluster.toFixed(4)}
</CenteredLevelItem>)}
</div>
Expand Down Expand Up @@ -297,14 +318,17 @@ export function MaxUsersByInstance({
<div className="field">
<label className="label">Max. Users / Instance *</label>
<div className="control">
<input
<Field
required
type="text"
className="input"
name="maxThreadsByInstance"
data-testid="Max. Users / Instance"
value={value}
onChange={onChange}
onFocus={(event: React.FocusEvent<HTMLInputElement>) => {
event.target.setSelectionRange(0, event.target.value.length);
}}
{...{ disabled }} />
</div>
</div>
Expand Down
22 changes: 13 additions & 9 deletions hailstorm-web-client/src/ClusterConfiguration/AWSView.test.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React from 'react';
import { findByRole, fireEvent, render } from '@testing-library/react';
import { fireEvent, render, wait } from '@testing-library/react';
import { AmazonCluster, Project } from '../domain';
import { AWSView } from './AWSView';
import { CreateClusterAction, UpdateClusterAction } from './actions';
import { UpdateClusterAction } from './actions';
import { ClusterService } from '../services/ClusterService';

describe('<AWSView />', () => {
Expand Down Expand Up @@ -34,20 +34,24 @@ describe('<AWSView />', () => {
it('should update Max users per instance', async () => {
const promise: Promise<AmazonCluster> = Promise.resolve({...cluster, maxThreadsByInstance: 50});
const apiSpy = jest.spyOn(ClusterService.prototype, 'update').mockReturnValue(promise);
const { findByRole, findByTestId } = render(<AWSView {...{cluster, activeProject, dispatch}} />);
const { findByRole, findByTestId, debug } = render(<AWSView {...{cluster, activeProject, dispatch}} />);
const input = await findByTestId('Max. Users / Instance');
fireEvent.focus(input);
fireEvent.change(input, {target: {value: '50'}});
fireEvent.change(input, {target: {value: '200'}});
fireEvent.blur(input);

debug(input);
const updateTrigger = await findByRole('Update Cluster');
debug(updateTrigger);
fireEvent.click(updateTrigger);

expect(apiSpy).toHaveBeenCalled();
await promise;
expect(dispatch).toHaveBeenCalled();
const action = dispatch.mock.calls[0][0];
expect(action).toBeInstanceOf(UpdateClusterAction);
await wait(async () => {
await promise;
expect(apiSpy).toHaveBeenCalled();
expect(dispatch).toHaveBeenCalled();
const action = dispatch.mock.calls[0][0];
expect(action).toBeInstanceOf(UpdateClusterAction);
}, {timeout: 1000});
});
});

Expand Down
Loading

0 comments on commit d0875af

Please sign in to comment.