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
*.gz
*.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 { 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)`
margin: 0 0 ${unitcalc(2)} 0;
`;
const ServiceScale = ({ service, onConfirmClick, onCancelClick }) => {
const handleScaleClick = () => {
onConfirmClick(2);
};
return (
<div>
const ServiceScale = ({
service,
handleSubmit,
onCancelClick,
invalid,
pristine
}) =>
<form onSubmit={handleSubmit}>
<StyledH2>Scaling a service: <br />{service.name}</StyledH2>
<P>Choose how many instances of a service you want to have running.</P>
<form onSubmit={() => {}}>
<NumberInput />
<FormGroup
name="replicas"
normalize={NumberInputNormalize({ minValue: 1 })}
reduxForm
>
<FormMeta />
<NumberInput minValue={1} />
</FormGroup>
<Button secondary onClick={onCancelClick}>Cancel</Button>
<Button secondary onClick={handleScaleClick}>Scale</Button>
</form>
</div>
);
};
<Button type="submit" disabled={pristine || invalid} secondary>
Scale
</Button>
</form>;
ServiceScale.propTypes = {
service: PropTypes.object,
onScaleClick: PropTypes.func,
onSubmitClick: PropTypes.func,
onCancelClick: PropTypes.func
};

View File

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

View File

@ -2,7 +2,16 @@ import React from 'react';
import PropTypes from 'prop-types';
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) {
return null;
}
@ -19,11 +28,25 @@ const ServicesQuickActions = ({ show, position, service, url, onBlur }) => {
const scaleUrl = `${url}/${service.slug}/scale`;
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 (
<Tooltip {...p} onBlur={onBlur}>
<TooltipButton to={scaleUrl}>Scale</TooltipButton>
<TooltipButton>Restart</TooltipButton>
<TooltipButton>Stop</TooltipButton>
<TooltipButton onClick={handleRestartClick}>Restart</TooltipButton>
<TooltipButton onClick={handleStopClick}>Stop</TooltipButton>
<TooltipDivider />
<TooltipButton to={deleteUrl}>Delete</TooltipButton>
</Tooltip>
@ -35,7 +58,10 @@ ServicesQuickActions.propTypes = {
url: PropTypes.string.isRequired,
position: PropTypes.object,
show: PropTypes.bool,
onBlur: PropTypes.func
onBlur: PropTypes.func,
onRestartClick: PropTypes.func,
onStopClick: PropTypes.func,
onStartClick: PropTypes.func
};
export default ServicesQuickActions;

View File

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

View File

@ -1,6 +1,7 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { compose, graphql } from 'react-apollo';
import { reduxForm } from 'redux-form';
import ServiceScaleMutation from '@graphql/ServiceScale.gql';
import { Loader, ErrorMessage } from '@components/messaging';
import { ServiceScale as ServiceScaleComponent } from '@components/service';
@ -20,21 +21,40 @@ class ServiceScale extends Component {
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 closeUrl = match.url.split('/').slice(0, -2).join('/');
history.replace(closeUrl);
};
const handleConfirmClick = evt => {
scale(service.id, 2);
const handleSubmitClick = values => {
scale(service.id, values.replicas);
};
return (
<Modal width={460} onCloseClick={handleCloseClick}>
<ServiceScaleComponent
<ServiceScaleForm
service={service}
onConfirmClick={handleConfirmClick}
onCancelClick={handleCloseClick}
onSubmit={handleSubmitClick.bind(this)}
onCancel={handleCloseClick}
/>
</Modal>
);

View File

@ -3,6 +3,9 @@ import { compose, graphql } from 'react-apollo';
import { connect } from 'react-redux';
import styled from 'styled-components';
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 { toggleServicesQuickActions } from '@root/state/actions';
@ -34,7 +37,10 @@ class ServiceList extends Component {
error,
servicesQuickActions,
toggleServicesQuickActions,
url
url,
restartServices,
stopServices,
startServices
} = this.props;
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 => {
toggleServicesQuickActions({ show: false });
};
@ -95,6 +113,9 @@ class ServiceList extends Component {
show={servicesQuickActions.show}
url={url}
onBlur={handleQuickActionsBlur}
onRestartClick={handleRestartClick}
onStopClick={handleStopClick}
onStartClick={handleStartClick}
/>
</div>
</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;

View File

@ -3,6 +3,9 @@ import { compose, graphql } from 'react-apollo';
import { connect } from 'react-redux';
import styled from 'styled-components';
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 { processServices } from '@root/state/selectors';
@ -31,7 +34,10 @@ const ServicesTopology = ({
loading,
error,
servicesQuickActions,
toggleServicesQuickActions
toggleServicesQuickActions,
restartServices,
stopServices,
startServices
}) => {
if (loading) {
return (
@ -55,6 +61,18 @@ const ServicesTopology = ({
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 }) => {
push(`${url.split('/').slice(0, 3).join('/')}/services/${service.slug}`);
};
@ -73,6 +91,9 @@ const ServicesTopology = ({
position={servicesQuickActions.position}
url={url}
onBlur={handleTooltipBlur}
onRestartClick={handleRestartClick}
onStopClick={handleStopClick}
onStartClick={handleStartClick}
/>
</StyledContainer>
</StyledBackground>
@ -108,8 +129,30 @@ const ServicesGql = graphql(ServicesQuery, {
})
});
const ServicesTopologyWithData = compose(ServicesGql, UiConnect)(
ServicesTopology
);
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 ServicesTopologyWithData = compose(
ServicesRestartGql,
ServicesStopGql,
ServicesStartGql,
ServicesGql,
UiConnect
)(ServicesTopology);
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);
};
const deleteServices = options => getServices({ id: options.ids[0] });
const deleteServices = options => {
const service = getServices({ id: options.ids[0] });
return service;
};
const scale = options => {
const service = getServices({ id: options.serviceId })[0];
return {
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 = {
portal: getPortal,
deploymentGroups: getDeploymentGroups,
@ -131,5 +150,8 @@ module.exports = {
format: options.format
})),
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
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]
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 Toggle, ToggleList } from './toggle';
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)};
`;
const StyledNumberInput = styled(Baseline(BaseInput(Stylable('input'))))`
const StyledNumberInput = styled(BaseInput(Stylable('input')))`
width: ${unitcalc(20)};
margin: 0 ${unitcalc(1)} 0 0;
vertical-align: middle;
@ -21,24 +21,41 @@ const StyledNumberInput = styled(Baseline(BaseInput(Stylable('input'))))`
/**
* @example ./usage-number-input.md
*/
const NumberInput = ({ value, ...rest }) => {
const render = value =>
const NumberInput = BaseInput(props => {
const { children, minValue, maxValue, ...rest } = props;
const render = value => {
const handleMinusClick = evt => {
evt.preventDefault();
const nextValue = value.input.value - 1;
value.input.onChange(nextValue);
};
const handlePlusClick = evt => {
evt.preventDefault();
const nextValue = value.input.value + 1;
value.input.onChange(nextValue);
};
return (
<StyledContainer>
<StyledNumberInput value={value} />
<IconButton onClick={() => {}}>
<StyledNumberInput {...props} />
<IconButton onClick={handleMinusClick}>
<MinusIcon verticalAlign="middle" />
</IconButton>
<IconButton onClick={() => {}}>
<IconButton onClick={handlePlusClick}>
<PlusIcon verticalAlign="middle" />
</IconButton>
</StyledContainer>;
</StyledContainer>
);
};
return (
<Subscriber channel="input-group">
{render}
</Subscriber>
);
};
});
NumberInput.propTypes = {
value: PropTypes.number,
@ -48,3 +65,18 @@ NumberInput.propTypes = {
};
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,
Toggle,
ToggleList,
NumberInput
NumberInput,
NumberInputNormalize
} from './form';
export {