feat: number input and mutations for restart, stop and start

This commit is contained in:
JUDIT GRESKOVITS 2017-06-19 13:10:57 +01:00 committed by Sérgio Ramos
parent 0623c06fc7
commit 7e359e5836
17 changed files with 281 additions and 62 deletions

View File

@ -38,3 +38,5 @@ Gruntfile.js
# misc # misc
*.gz *.gz
*.md *.md
!nyc/node_modules/istanbul-reports/lib/html/assets

View File

@ -4,32 +4,44 @@ import styled from 'styled-components';
import unitcalc from 'unitcalc'; import unitcalc from 'unitcalc';
import { H2, P, Button } from 'joyent-ui-toolkit'; import { H2, P, Button } from 'joyent-ui-toolkit';
import { FormGroup, Input, NumberInput } from 'joyent-ui-toolkit'; import {
FormGroup,
NumberInput,
NumberInputNormalize,
FormMeta
} from 'joyent-ui-toolkit';
const StyledH2 = styled(H2)` const StyledH2 = styled(H2)`
margin: 0 0 ${unitcalc(2)} 0; margin: 0 0 ${unitcalc(2)} 0;
`; `;
const ServiceScale = ({ service, onConfirmClick, onCancelClick }) => { const ServiceScale = ({
const handleScaleClick = () => { service,
onConfirmClick(2); handleSubmit,
}; onCancelClick,
return ( invalid,
<div> pristine
<StyledH2>Scaling a service: <br />{service.name}</StyledH2> }) =>
<P>Choose how many instances of a service you want to have running.</P> <form onSubmit={handleSubmit}>
<form onSubmit={() => {}}> <StyledH2>Scaling a service: <br />{service.name}</StyledH2>
<NumberInput /> <P>Choose how many instances of a service you want to have running.</P>
<Button secondary onClick={onCancelClick}>Cancel</Button> <FormGroup
<Button secondary onClick={handleScaleClick}>Scale</Button> name="replicas"
</form> normalize={NumberInputNormalize({ minValue: 1 })}
</div> reduxForm
); >
}; <FormMeta />
<NumberInput minValue={1} />
</FormGroup>
<Button secondary onClick={onCancelClick}>Cancel</Button>
<Button type="submit" disabled={pristine || invalid} secondary>
Scale
</Button>
</form>;
ServiceScale.propTypes = { ServiceScale.propTypes = {
service: PropTypes.object, service: PropTypes.object,
onScaleClick: PropTypes.func, onSubmitClick: PropTypes.func,
onCancelClick: PropTypes.func onCancelClick: PropTypes.func
}; };

View File

@ -66,7 +66,7 @@ const ServiceListItem = ({
const subtitle = <CardSubTitle>{service.instances} instances</CardSubTitle>; const subtitle = <CardSubTitle>{service.instances} instances</CardSubTitle>;
const handleCardOptionsClick = evt => { const handleCardOptionsClick = evt => {
onQuickActionsClick(evt, { service }); onQuickActionsClick(evt, service);
}; };
const header = isChild const header = isChild

View File

@ -2,7 +2,16 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Tooltip, TooltipButton, TooltipDivider } from 'joyent-ui-toolkit'; import { Tooltip, TooltipButton, TooltipDivider } from 'joyent-ui-toolkit';
const ServicesQuickActions = ({ show, position, service, url, onBlur }) => { const ServicesQuickActions = ({
show,
position,
service,
url,
onBlur,
onRestartClick,
onStopClick,
onStartClick
}) => {
if (!show) { if (!show) {
return null; return null;
} }
@ -19,11 +28,25 @@ const ServicesQuickActions = ({ show, position, service, url, onBlur }) => {
const scaleUrl = `${url}/${service.slug}/scale`; const scaleUrl = `${url}/${service.slug}/scale`;
const deleteUrl = `${url}/${service.slug}/delete`; const deleteUrl = `${url}/${service.slug}/delete`;
const handleRestartClick = evt => {
onRestartClick(evt, service);
};
const handleStopClick = evt => {
onStopClick(evt, service);
};
const handleStartClick = evt => {
onStartClick(evt, service);
};
// TODO we'll need to check for service status and diplay start or restart & stop accordingly
return ( return (
<Tooltip {...p} onBlur={onBlur}> <Tooltip {...p} onBlur={onBlur}>
<TooltipButton to={scaleUrl}>Scale</TooltipButton> <TooltipButton to={scaleUrl}>Scale</TooltipButton>
<TooltipButton>Restart</TooltipButton> <TooltipButton onClick={handleRestartClick}>Restart</TooltipButton>
<TooltipButton>Stop</TooltipButton> <TooltipButton onClick={handleStopClick}>Stop</TooltipButton>
<TooltipDivider /> <TooltipDivider />
<TooltipButton to={deleteUrl}>Delete</TooltipButton> <TooltipButton to={deleteUrl}>Delete</TooltipButton>
</Tooltip> </Tooltip>
@ -35,7 +58,10 @@ ServicesQuickActions.propTypes = {
url: PropTypes.string.isRequired, url: PropTypes.string.isRequired,
position: PropTypes.object, position: PropTypes.object,
show: PropTypes.bool, show: PropTypes.bool,
onBlur: PropTypes.func onBlur: PropTypes.func,
onRestartClick: PropTypes.func,
onStopClick: PropTypes.func,
onStartClick: PropTypes.func
}; };
export default ServicesQuickActions; export default ServicesQuickActions;

View File

@ -1,7 +1,7 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { compose, graphql, gql } from 'react-apollo'; import { compose, graphql, gql } from 'react-apollo';
import ServiceScaleMutation from '@graphql/ServiceScale.gql'; import ServicesDeleteMutation from '@graphql/ServicesDeleteMutation.gql';
import { Loader, ErrorMessage } from '@components/messaging'; import { Loader, ErrorMessage } from '@components/messaging';
import { ServiceDelete as ServiceDeleteComponent } from '@components/service'; import { ServiceDelete as ServiceDeleteComponent } from '@components/service';
import { Modal } from 'joyent-ui-toolkit'; import { Modal } from 'joyent-ui-toolkit';
@ -47,16 +47,7 @@ ServiceDelete.propTypes = {
deleteServices: PropTypes.func.isRequired deleteServices: PropTypes.func.isRequired
}; };
const DeleteGql = gql` const DeleteServicesGql = graphql(ServicesDeleteMutation, {
mutation deleteServices($ids: [ID]!) {
deleteServices(ids: $ids) {
id
slug
}
}
`;
const DeleteServicesGql = graphql(DeleteGql, {
props: ({ mutate }) => ({ props: ({ mutate }) => ({
deleteServices: serviceId => mutate({ variables: { ids: [serviceId] } }) deleteServices: serviceId => mutate({ variables: { ids: [serviceId] } })
}) })

View File

@ -1,6 +1,7 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { compose, graphql } from 'react-apollo'; import { compose, graphql } from 'react-apollo';
import { reduxForm } from 'redux-form';
import ServiceScaleMutation from '@graphql/ServiceScale.gql'; import ServiceScaleMutation from '@graphql/ServiceScale.gql';
import { Loader, ErrorMessage } from '@components/messaging'; import { Loader, ErrorMessage } from '@components/messaging';
import { ServiceScale as ServiceScaleComponent } from '@components/service'; import { ServiceScale as ServiceScaleComponent } from '@components/service';
@ -20,21 +21,40 @@ class ServiceScale extends Component {
const { service, scale, history, match } = this.props; const { service, scale, history, match } = this.props;
const validateReplicas = ({ replicas }) => {
if (replicas === '') {
return {
replicas:
'Please enter the number of instances you would like to scale to.'
};
}
};
const ServiceScaleForm = reduxForm({
form: 'scale-service',
destroyOnUnmount: true,
forceUnregisterOnUnmount: true,
validate: validateReplicas,
initialValues: {
replicas: service.instances.length
}
})(ServiceScaleComponent);
const handleCloseClick = evt => { const handleCloseClick = evt => {
const closeUrl = match.url.split('/').slice(0, -2).join('/'); const closeUrl = match.url.split('/').slice(0, -2).join('/');
history.replace(closeUrl); history.replace(closeUrl);
}; };
const handleConfirmClick = evt => { const handleSubmitClick = values => {
scale(service.id, 2); scale(service.id, values.replicas);
}; };
return ( return (
<Modal width={460} onCloseClick={handleCloseClick}> <Modal width={460} onCloseClick={handleCloseClick}>
<ServiceScaleComponent <ServiceScaleForm
service={service} service={service}
onConfirmClick={handleConfirmClick} onSubmit={handleSubmitClick.bind(this)}
onCancelClick={handleCloseClick} onCancel={handleCloseClick}
/> />
</Modal> </Modal>
); );

View File

@ -3,6 +3,9 @@ import { compose, graphql } from 'react-apollo';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import styled from 'styled-components'; import styled from 'styled-components';
import ServicesQuery from '@graphql/Services.gql'; import ServicesQuery from '@graphql/Services.gql';
import ServicesRestartMutation from '@graphql/ServicesRestartMutation.gql';
import ServicesStopMutation from '@graphql/ServicesStopMutation.gql';
import ServicesStartMutation from '@graphql/ServicesStartMutation.gql';
import { processServices } from '@root/state/selectors'; import { processServices } from '@root/state/selectors';
import { toggleServicesQuickActions } from '@root/state/actions'; import { toggleServicesQuickActions } from '@root/state/actions';
@ -34,7 +37,10 @@ class ServiceList extends Component {
error, error,
servicesQuickActions, servicesQuickActions,
toggleServicesQuickActions, toggleServicesQuickActions,
url url,
restartServices,
stopServices,
startServices
} = this.props; } = this.props;
if (loading) { if (loading) {
@ -71,6 +77,18 @@ class ServiceList extends Component {
}); });
}; };
const handleRestartClick = (evt, service) => {
restartServices(service.id);
};
const handleStopClick = (evt, service) => {
stopServices(service.id);
};
const handleStartClick = (evt, service) => {
startServices(service.id);
};
const handleQuickActionsBlur = o => { const handleQuickActionsBlur = o => {
toggleServicesQuickActions({ show: false }); toggleServicesQuickActions({ show: false });
}; };
@ -95,6 +113,9 @@ class ServiceList extends Component {
show={servicesQuickActions.show} show={servicesQuickActions.show}
url={url} url={url}
onBlur={handleQuickActionsBlur} onBlur={handleQuickActionsBlur}
onRestartClick={handleRestartClick}
onStopClick={handleStopClick}
onStartClick={handleStartClick}
/> />
</div> </div>
</StyledContainer> </StyledContainer>
@ -132,6 +153,30 @@ const ServicesGql = graphql(ServicesQuery, {
}) })
}); });
const ServiceListWithData = compose(ServicesGql, UiConnect)(ServiceList); const ServicesRestartGql = graphql(ServicesRestartMutation, {
props: ({ mutate }) => ({
restartServices: serviceId => mutate({ variables: { ids: [serviceId] } })
})
});
const ServicesStopGql = graphql(ServicesStopMutation, {
props: ({ mutate }) => ({
stopServices: serviceId => mutate({ variables: { ids: [serviceId] } })
})
});
const ServicesStartGql = graphql(ServicesStartMutation, {
props: ({ mutate }) => ({
startServices: serviceId => mutate({ variables: { ids: [serviceId] } })
})
});
const ServiceListWithData = compose(
ServicesGql,
ServicesStopGql,
ServicesStartGql,
ServicesGql,
UiConnect
)(ServiceList);
export default ServiceListWithData; export default ServiceListWithData;

View File

@ -3,6 +3,9 @@ import { compose, graphql } from 'react-apollo';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import styled from 'styled-components'; import styled from 'styled-components';
import ServicesQuery from '@graphql/ServicesTopology.gql'; import ServicesQuery from '@graphql/ServicesTopology.gql';
import ServicesRestartMutation from '@graphql/ServicesRestartMutation.gql';
import ServicesStopMutation from '@graphql/ServicesStopMutation.gql';
import ServicesStartMutation from '@graphql/ServicesStartMutation.gql';
import unitcalc from 'unitcalc'; import unitcalc from 'unitcalc';
import { processServices } from '@root/state/selectors'; import { processServices } from '@root/state/selectors';
@ -31,7 +34,10 @@ const ServicesTopology = ({
loading, loading,
error, error,
servicesQuickActions, servicesQuickActions,
toggleServicesQuickActions toggleServicesQuickActions,
restartServices,
stopServices,
startServices
}) => { }) => {
if (loading) { if (loading) {
return ( return (
@ -55,6 +61,18 @@ const ServicesTopology = ({
toggleServicesQuickActions({ show: false }); toggleServicesQuickActions({ show: false });
}; };
const handleRestartClick = (evt, service) => {
restartServices(service.id);
};
const handleStopClick = (evt, service) => {
stopServices(service.id);
};
const handleStartClick = (evt, service) => {
startServices(service.id);
};
const handleNodeTitleClick = (evt, { service }) => { const handleNodeTitleClick = (evt, { service }) => {
push(`${url.split('/').slice(0, 3).join('/')}/services/${service.slug}`); push(`${url.split('/').slice(0, 3).join('/')}/services/${service.slug}`);
}; };
@ -73,6 +91,9 @@ const ServicesTopology = ({
position={servicesQuickActions.position} position={servicesQuickActions.position}
url={url} url={url}
onBlur={handleTooltipBlur} onBlur={handleTooltipBlur}
onRestartClick={handleRestartClick}
onStopClick={handleStopClick}
onStartClick={handleStartClick}
/> />
</StyledContainer> </StyledContainer>
</StyledBackground> </StyledBackground>
@ -108,8 +129,30 @@ const ServicesGql = graphql(ServicesQuery, {
}) })
}); });
const ServicesTopologyWithData = compose(ServicesGql, UiConnect)( const ServicesRestartGql = graphql(ServicesRestartMutation, {
ServicesTopology props: ({ mutate }) => ({
); restartServices: serviceId => mutate({ variables: { ids: [serviceId] } })
})
});
const ServicesStopGql = graphql(ServicesStopMutation, {
props: ({ mutate }) => ({
stopServices: serviceId => mutate({ variables: { ids: [serviceId] } })
})
});
const ServicesStartGql = graphql(ServicesStartMutation, {
props: ({ mutate }) => ({
startServices: serviceId => mutate({ variables: { ids: [serviceId] } })
})
});
const ServicesTopologyWithData = compose(
ServicesRestartGql,
ServicesStopGql,
ServicesStartGql,
ServicesGql,
UiConnect
)(ServicesTopology);
export default ServicesTopologyWithData; export default ServicesTopologyWithData;

View File

@ -0,0 +1,6 @@
mutation DeleteServices($ids: [ID]!) {
deleteServices(ids: $ids) {
id
slug
}
}

View File

@ -0,0 +1,6 @@
mutation RestartServices($ids: [ID]!) {
restartServices(ids: $ids) {
id
slug
}
}

View File

@ -0,0 +1,6 @@
mutation StartServices($ids: [ID]!) {
startServices(ids: $ids) {
id
slug
}
}

View File

@ -0,0 +1,6 @@
mutation StopServices($ids: [ID]!) {
stopServices(ids: $ids) {
id
slug
}
}

View File

@ -100,10 +100,14 @@ const createServicesFromManifest = ({ deploymentGroupId, raw }) => {
return Promise.resolve(undefined); return Promise.resolve(undefined);
}; };
const deleteServices = options => getServices({ id: options.ids[0] }); const deleteServices = options => {
const service = getServices({ id: options.ids[0] });
return service;
};
const scale = options => { const scale = options => {
const service = getServices({ id: options.serviceId })[0]; const service = getServices({ id: options.serviceId })[0];
return { return {
scale: [ scale: [
{ {
@ -115,6 +119,21 @@ const scale = options => {
}; };
}; };
const restartServices = options => {
const service = getServices({ id: options.ids[0] });
return service;
};
const stopServices = options => {
const service = getServices({ id: options.ids[0] });
return service;
};
const startServices = options => {
const service = getServices({ id: options.ids[0] });
return service;
};
module.exports = { module.exports = {
portal: getPortal, portal: getPortal,
deploymentGroups: getDeploymentGroups, deploymentGroups: getDeploymentGroups,
@ -131,5 +150,8 @@ module.exports = {
format: options.format format: options.format
})), })),
deleteServices: (options, request, fn) => fn(null, deleteServices(options)), deleteServices: (options, request, fn) => fn(null, deleteServices(options)),
scale: (options, reguest, fn) => fn(null, scale(options)) scale: (options, reguest, fn) => fn(null, scale(options)),
restartServices: (options, request, fn) => fn(null, restartServices(options)),
stopServices: (options, request, fn) => fn(null, stopServices(options)),
startServices: (options, request, fn) => fn(null, startServices(options))
}; };

View File

@ -191,7 +191,7 @@ type Mutation {
updateDeploymentGroup(id: ID!, name: String!) : DeploymentGroup updateDeploymentGroup(id: ID!, name: String!) : DeploymentGroup
provisionManifest(deploymentGroupId: ID!, type: ManifestType!, format: ManifestFormat!, raw: String!) : Manifest provisionManifest(deploymentGroupId: ID!, type: ManifestType!, format: ManifestFormat!, raw: String!) : Manifest
scale(service: ID!, replicas: Int!) : Version scale(serviceId: ID!, replicas: Int!) : Version
stopServices(ids: [ID]!) : [Service] stopServices(ids: [ID]!) : [Service]
startServices(ids: [ID]!) : [Service] startServices(ids: [ID]!) : [Service]

View File

@ -9,3 +9,4 @@ export { default as Radio, RadioList } from './radio';
export { default as Select } from './select'; export { default as Select } from './select';
export { default as Toggle, ToggleList } from './toggle'; export { default as Toggle, ToggleList } from './toggle';
export { default as NumberInput } from './number-input'; export { default as NumberInput } from './number-input';
export { NumberInputNormalize } from './number-input';

View File

@ -12,7 +12,7 @@ const StyledContainer = styled.div`
margin-bottom: ${unitcalc(4)}; margin-bottom: ${unitcalc(4)};
`; `;
const StyledNumberInput = styled(Baseline(BaseInput(Stylable('input'))))` const StyledNumberInput = styled(BaseInput(Stylable('input')))`
width: ${unitcalc(20)}; width: ${unitcalc(20)};
margin: 0 ${unitcalc(1)} 0 0; margin: 0 ${unitcalc(1)} 0 0;
vertical-align: middle; vertical-align: middle;
@ -21,24 +21,41 @@ const StyledNumberInput = styled(Baseline(BaseInput(Stylable('input'))))`
/** /**
* @example ./usage-number-input.md * @example ./usage-number-input.md
*/ */
const NumberInput = ({ value, ...rest }) => { const NumberInput = BaseInput(props => {
const render = value => const { children, minValue, maxValue, ...rest } = props;
<StyledContainer>
<StyledNumberInput value={value} /> const render = value => {
<IconButton onClick={() => {}}> const handleMinusClick = evt => {
<MinusIcon verticalAlign="middle" /> evt.preventDefault();
</IconButton> const nextValue = value.input.value - 1;
<IconButton onClick={() => {}}> value.input.onChange(nextValue);
<PlusIcon verticalAlign="middle" /> };
</IconButton>
</StyledContainer>; const handlePlusClick = evt => {
evt.preventDefault();
const nextValue = value.input.value + 1;
value.input.onChange(nextValue);
};
return (
<StyledContainer>
<StyledNumberInput {...props} />
<IconButton onClick={handleMinusClick}>
<MinusIcon verticalAlign="middle" />
</IconButton>
<IconButton onClick={handlePlusClick}>
<PlusIcon verticalAlign="middle" />
</IconButton>
</StyledContainer>
);
};
return ( return (
<Subscriber channel="input-group"> <Subscriber channel="input-group">
{render} {render}
</Subscriber> </Subscriber>
); );
}; });
NumberInput.propTypes = { NumberInput.propTypes = {
value: PropTypes.number, value: PropTypes.number,
@ -48,3 +65,18 @@ NumberInput.propTypes = {
}; };
export default Baseline(NumberInput); export default Baseline(NumberInput);
export const NumberInputNormalize = ({ minValue, maxValue }) => {
return value => {
if (value === '') {
return '';
}
if (
!isNaN(value) &&
(isNaN(minValue) || value >= minValue) &&
(isNaN(maxValue) || value <= maxValue)
) {
return Number(value);
}
};
};

View File

@ -67,7 +67,8 @@ export {
Select, Select,
Toggle, Toggle,
ToggleList, ToggleList,
NumberInput NumberInput,
NumberInputNormalize
} from './form'; } from './form';
export { export {