diff --git a/.yarnclean b/.yarnclean index c9b848f0..9259ff58 100644 --- a/.yarnclean +++ b/.yarnclean @@ -38,3 +38,5 @@ Gruntfile.js # misc *.gz *.md + +!nyc/node_modules/istanbul-reports/lib/html/assets diff --git a/packages/cp-frontend/src/components/service/scale.js b/packages/cp-frontend/src/components/service/scale.js index 2c43b70a..cd67e8a2 100644 --- a/packages/cp-frontend/src/components/service/scale.js +++ b/packages/cp-frontend/src/components/service/scale.js @@ -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 ( -
- Scaling a service:
{service.name}
-

Choose how many instances of a service you want to have running.

-
{}}> - - - - -
- ); -}; +const ServiceScale = ({ + service, + handleSubmit, + onCancelClick, + invalid, + pristine +}) => +
+ Scaling a service:
{service.name}
+

Choose how many instances of a service you want to have running.

+ + + + + + +
; ServiceScale.propTypes = { service: PropTypes.object, - onScaleClick: PropTypes.func, + onSubmitClick: PropTypes.func, onCancelClick: PropTypes.func }; diff --git a/packages/cp-frontend/src/components/services/list-item.js b/packages/cp-frontend/src/components/services/list-item.js index 9438c353..254f07e3 100644 --- a/packages/cp-frontend/src/components/services/list-item.js +++ b/packages/cp-frontend/src/components/services/list-item.js @@ -66,7 +66,7 @@ const ServiceListItem = ({ const subtitle = {service.instances} instances; const handleCardOptionsClick = evt => { - onQuickActionsClick(evt, { service }); + onQuickActionsClick(evt, service); }; const header = isChild diff --git a/packages/cp-frontend/src/components/services/quick-actions.js b/packages/cp-frontend/src/components/services/quick-actions.js index 86707bbd..f6a3ce78 100644 --- a/packages/cp-frontend/src/components/services/quick-actions.js +++ b/packages/cp-frontend/src/components/services/quick-actions.js @@ -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 ( Scale - Restart - Stop + Restart + Stop Delete @@ -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; diff --git a/packages/cp-frontend/src/containers/service/delete.js b/packages/cp-frontend/src/containers/service/delete.js index 6a425d0d..a2a0c8d3 100644 --- a/packages/cp-frontend/src/containers/service/delete.js +++ b/packages/cp-frontend/src/containers/service/delete.js @@ -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] } }) }) diff --git a/packages/cp-frontend/src/containers/service/scale.js b/packages/cp-frontend/src/containers/service/scale.js index d54b9172..fdc512ad 100644 --- a/packages/cp-frontend/src/containers/service/scale.js +++ b/packages/cp-frontend/src/containers/service/scale.js @@ -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 ( - ); diff --git a/packages/cp-frontend/src/containers/services/list.js b/packages/cp-frontend/src/containers/services/list.js index 0257dc35..74f47e95 100644 --- a/packages/cp-frontend/src/containers/services/list.js +++ b/packages/cp-frontend/src/containers/services/list.js @@ -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} /> @@ -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; diff --git a/packages/cp-frontend/src/containers/services/topology.js b/packages/cp-frontend/src/containers/services/topology.js index 82e5bd42..12915b82 100644 --- a/packages/cp-frontend/src/containers/services/topology.js +++ b/packages/cp-frontend/src/containers/services/topology.js @@ -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} /> @@ -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; diff --git a/packages/cp-frontend/src/graphql/ServicesDeleteMutation.gql b/packages/cp-frontend/src/graphql/ServicesDeleteMutation.gql new file mode 100644 index 00000000..0790ea31 --- /dev/null +++ b/packages/cp-frontend/src/graphql/ServicesDeleteMutation.gql @@ -0,0 +1,6 @@ +mutation DeleteServices($ids: [ID]!) { + deleteServices(ids: $ids) { + id + slug + } +} diff --git a/packages/cp-frontend/src/graphql/ServicesRestartMutation.gql b/packages/cp-frontend/src/graphql/ServicesRestartMutation.gql new file mode 100644 index 00000000..a1d0a662 --- /dev/null +++ b/packages/cp-frontend/src/graphql/ServicesRestartMutation.gql @@ -0,0 +1,6 @@ +mutation RestartServices($ids: [ID]!) { + restartServices(ids: $ids) { + id + slug + } +} diff --git a/packages/cp-frontend/src/graphql/ServicesStartMutation.gql b/packages/cp-frontend/src/graphql/ServicesStartMutation.gql new file mode 100644 index 00000000..a47e541c --- /dev/null +++ b/packages/cp-frontend/src/graphql/ServicesStartMutation.gql @@ -0,0 +1,6 @@ +mutation StartServices($ids: [ID]!) { + startServices(ids: $ids) { + id + slug + } +} diff --git a/packages/cp-frontend/src/graphql/ServicesStopMutation.gql b/packages/cp-frontend/src/graphql/ServicesStopMutation.gql new file mode 100644 index 00000000..1ef7ebe1 --- /dev/null +++ b/packages/cp-frontend/src/graphql/ServicesStopMutation.gql @@ -0,0 +1,6 @@ +mutation StopServices($ids: [ID]!) { + stopServices(ids: $ids) { + id + slug + } +} diff --git a/packages/cp-gql-mock-server/src/resolvers.js b/packages/cp-gql-mock-server/src/resolvers.js index 5941f60e..13764483 100644 --- a/packages/cp-gql-mock-server/src/resolvers.js +++ b/packages/cp-gql-mock-server/src/resolvers.js @@ -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)) }; diff --git a/packages/cp-gql-schema/schema.gql b/packages/cp-gql-schema/schema.gql index b48d2718..c747075d 100644 --- a/packages/cp-gql-schema/schema.gql +++ b/packages/cp-gql-schema/schema.gql @@ -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] diff --git a/packages/ui-toolkit/src/form/index.js b/packages/ui-toolkit/src/form/index.js index 9ed59a19..085f810d 100644 --- a/packages/ui-toolkit/src/form/index.js +++ b/packages/ui-toolkit/src/form/index.js @@ -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'; diff --git a/packages/ui-toolkit/src/form/number-input.js b/packages/ui-toolkit/src/form/number-input.js index e37f1fc7..040597f3 100644 --- a/packages/ui-toolkit/src/form/number-input.js +++ b/packages/ui-toolkit/src/form/number-input.js @@ -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 ( + + + + + + + + + + ); + }; return ( {render} ); -}; +}); 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); + } + }; +}; diff --git a/packages/ui-toolkit/src/index.js b/packages/ui-toolkit/src/index.js index 5a0df51a..1761b0f6 100644 --- a/packages/ui-toolkit/src/index.js +++ b/packages/ui-toolkit/src/index.js @@ -67,7 +67,8 @@ export { Select, Toggle, ToggleList, - NumberInput + NumberInput, + NumberInputNormalize } from './form'; export {