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
+}) =>
+ ;
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 {