diff --git a/packages/cp-frontend/src/components/messaging/index.js b/packages/cp-frontend/src/components/messaging/index.js
index c18b5fe4..8267241d 100644
--- a/packages/cp-frontend/src/components/messaging/index.js
+++ b/packages/cp-frontend/src/components/messaging/index.js
@@ -1,3 +1,4 @@
export { default as Loader } from './loader';
export { default as ErrorMessage } from './error';
export { default as WarningMessage } from './warning';
+export { default as ModalErrorMessage } from './modal-error';
diff --git a/packages/cp-frontend/src/components/messaging/modal-error.js b/packages/cp-frontend/src/components/messaging/modal-error.js
new file mode 100644
index 00000000..f2fbddcf
--- /dev/null
+++ b/packages/cp-frontend/src/components/messaging/modal-error.js
@@ -0,0 +1,24 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import styled from 'styled-components';
+import { ModalHeading, ModalText, Button } from 'joyent-ui-toolkit';
+
+const StyledHeading = styled(ModalHeading)`
+ color: ${props => props.theme.red};
+`;
+
+const ModalErrorMessage = ({ title, message, onCloseClick }) =>
+
+ {title}
+ {message}
+
+
+
;
+
+ModalErrorMessage.propTypes = {
+ title: PropTypes.string,
+ message: PropTypes.string.isRequired,
+ onCloseClick: PropTypes.func.isRequired
+};
+
+export default ModalErrorMessage;
diff --git a/packages/cp-frontend/src/components/navigation/index.js b/packages/cp-frontend/src/components/navigation/index.js
index a69ba5ad..abc989cc 100644
--- a/packages/cp-frontend/src/components/navigation/index.js
+++ b/packages/cp-frontend/src/components/navigation/index.js
@@ -2,3 +2,4 @@ export { default as Breadcrumb } from './breadcrumb';
export { default as Menu } from './menu';
export { default as Header } from './header';
export { default as Title } from './title';
+export { default as NotFound } from './not-found';
diff --git a/packages/cp-frontend/src/components/navigation/not-found.js b/packages/cp-frontend/src/components/navigation/not-found.js
new file mode 100644
index 00000000..fbfecf6d
--- /dev/null
+++ b/packages/cp-frontend/src/components/navigation/not-found.js
@@ -0,0 +1,44 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import styled from 'styled-components';
+import remcalc from 'remcalc';
+import { H1, H2, P, Button } from 'joyent-ui-toolkit';
+import { LayoutContainer } from '@components/layout';
+
+const StyledContainer = styled.div`
+ margin-top: ${remcalc(60)};
+`;
+
+const StyledTitle = styled(H1)`
+ font-weight: normal;
+ font-size: ${remcalc(32)};
+`;
+
+const StyledP = styled(P)`
+ margin-bottom: ${remcalc(30)};
+ max-width: ${remcalc(490)};
+`;
+
+const NotFound = ({
+ title = 'I have no memory of this place',
+ message = 'HTTP 404: We can’t find what you are looking for. Next time, always follow your nose.',
+ link = 'Back to dashboard',
+ to = '/deployment-groups'
+}) => (
+
+
+ {title}
+ {message}
+
+
+
+);
+
+NotFound.propTypes = {
+ title: PropTypes.string,
+ message: PropTypes.string,
+ link: PropTypes.string,
+ to: PropTypes.string
+}
+
+export default NotFound;
diff --git a/packages/cp-frontend/src/containers/deployment-group/delete.js b/packages/cp-frontend/src/containers/deployment-group/delete.js
index b35250c3..8ba301b9 100644
--- a/packages/cp-frontend/src/containers/deployment-group/delete.js
+++ b/packages/cp-frontend/src/containers/deployment-group/delete.js
@@ -3,9 +3,10 @@ import PropTypes from 'prop-types';
import { compose, graphql } from 'react-apollo';
import DeploymentGroupDeleteMutation from '@graphql/DeploymentGroupDeleteMutation.gql';
import DeploymentGroupQuery from '@graphql/DeploymentGroup.gql';
-import { Loader, ErrorMessage } from '@components/messaging';
+import { Loader, ModalErrorMessage } from '@components/messaging';
import { DeploymentGroupDelete as DeploymentGroupDeleteComponent } from '@components/deployment-group';
-import { Modal, ModalHeading, Button } from 'joyent-ui-toolkit';
+import { Modal, ModalHeading, Button } from 'joyent-ui-toolkit'
+import { withNotFound, GqlPaths } from '@containers/navigation';
class DeploymentGroupDelete extends Component {
@@ -18,7 +19,7 @@ class DeploymentGroupDelete extends Component {
}
render() {
- const { loading, error } = this.props;
+ const { location, history, match, loading, error } = this.props;
const handleCloseClick = evt => {
const closeUrl = match.url.split('/').slice(0, -2).join('/');
@@ -36,32 +37,26 @@ class DeploymentGroupDelete extends Component {
if (error) {
return (
-
+ message='An error occurred while loading your deployment group.'
+ onCloseClick={handleCloseClick} />
);
}
const {
deploymentGroup,
- deleteDeploymentGroup,
- history,
- match
+ deleteDeploymentGroup
} = this.props;
if (this.state.error) {
return (
-
- Deleting a deployment group:
{deploymentGroup.name}
-
-
-
+ message={`An error occured while attempting to delete the ${deploymentGroup.name} deployment group.`}
+ onCloseClick={handleCloseClick} />
);
}
@@ -70,6 +65,7 @@ class DeploymentGroupDelete extends Component {
deleteDeploymentGroup(deploymentGroup.id)
.then(() => handleCloseClick())
.catch((err) => {
+ console.log('err = ', err);
this.setState({ error: err });
});
};
@@ -120,7 +116,8 @@ const DeploymentGroupGql = graphql(DeploymentGroupQuery, {
const DeploymentGroupDeleteWithData = compose(
DeleteDeploymentGroupGql,
- DeploymentGroupGql
+ DeploymentGroupGql,
+ withNotFound([ GqlPaths.DEPLOYMENT_GROUP ])
)(DeploymentGroupDelete);
export default DeploymentGroupDeleteWithData;
diff --git a/packages/cp-frontend/src/containers/deployment-groups/list.js b/packages/cp-frontend/src/containers/deployment-groups/list.js
index 2dd1f607..dbab8e55 100644
--- a/packages/cp-frontend/src/containers/deployment-groups/list.js
+++ b/packages/cp-frontend/src/containers/deployment-groups/list.js
@@ -13,6 +13,7 @@ import { ErrorMessage, Loader } from '@components/messaging';
import DeploymentGroupsQuery from '@graphql/DeploymentGroups.gql';
import DeploymentGroupsImportableQuery from '@graphql/DeploymentGroupsImportable.gql';
import { H2, H3, Small, IconButton, BinIcon } from 'joyent-ui-toolkit';
+import { withNotFound, GqlPaths } from '@containers/navigation';
const DGsRows = Row.extend`
margin-top: ${remcalc(-7)};
@@ -216,5 +217,6 @@ export default compose(
props: ({ data: { importableDeploymentGroups } }) => ({
importable: importableDeploymentGroups
})
- })
+ }),
+ withNotFound([ GqlPaths.DEPLOYMENT_GROUP ])
)(DeploymentGroupList);
diff --git a/packages/cp-frontend/src/containers/instances/list.js b/packages/cp-frontend/src/containers/instances/list.js
index 04963226..0b42ca05 100644
--- a/packages/cp-frontend/src/containers/instances/list.js
+++ b/packages/cp-frontend/src/containers/instances/list.js
@@ -11,6 +11,8 @@ import { Title } from '@components/navigation';
import { Loader, ErrorMessage } from '@components/messaging';
import { InstanceListItem, EmptyInstances } from '@components/instances';
+import { withNotFound, GqlPaths } from '@containers/navigation';
+
const InstanceList = ({ deploymentGroup, instances = [], loading, error }) => {
const _title = Instances;
@@ -75,7 +77,7 @@ const InstanceListGql = graphql(InstancesQuery, {
}
};
},
- props: ({ data: { deploymentGroup, loading, error } }) => ({
+ props: ({ data: { deploymentGroup, loading, error }}) => ({
deploymentGroup,
instances: sortBy(
forceArray(
@@ -92,4 +94,10 @@ const InstanceListGql = graphql(InstancesQuery, {
})
});
-export default compose(InstanceListGql)(InstanceList);
+export default compose(
+ InstanceListGql,
+ withNotFound([
+ GqlPaths.DEPLOYMENT_GROUP,
+ GqlPaths.SERVICES
+ ])
+)(InstanceList);
diff --git a/packages/cp-frontend/src/containers/navigation/breadcrumb.js b/packages/cp-frontend/src/containers/navigation/breadcrumb.js
index d8f60479..50e59871 100644
--- a/packages/cp-frontend/src/containers/navigation/breadcrumb.js
+++ b/packages/cp-frontend/src/containers/navigation/breadcrumb.js
@@ -1,12 +1,15 @@
import React from 'react';
import { connect } from 'react-redux';
+import { compose } from 'react-apollo';
import { Breadcrumb as BreadcrumbComponent } from '@components/navigation';
+import withNotFound from './not-found-hoc';
import {
deploymentGroupBySlugSelector,
serviceBySlugSelector
} from '@root/state/selectors';
const Breadcrumb = ({ deploymentGroup, service, location }) => {
+
const path = location.pathname.split('/');
const links = [
@@ -33,7 +36,7 @@ const Breadcrumb = ({ deploymentGroup, service, location }) => {
return ;
};
-const ConnectedBreadcrumb = connect(
+const connectBreadcrumb = connect(
(state, ownProps) => {
const params = ownProps.match.params;
const deploymentGroupSlug = params.deploymentGroup;
@@ -47,6 +50,9 @@ const ConnectedBreadcrumb = connect(
};
},
dispatch => ({})
-)(Breadcrumb);
+);
-export default ConnectedBreadcrumb;
+export default compose(
+ connectBreadcrumb,
+ withNotFound()
+)(Breadcrumb);
diff --git a/packages/cp-frontend/src/containers/navigation/index.js b/packages/cp-frontend/src/containers/navigation/index.js
index 570ccc4d..0394e39b 100644
--- a/packages/cp-frontend/src/containers/navigation/index.js
+++ b/packages/cp-frontend/src/containers/navigation/index.js
@@ -1,3 +1,4 @@
export { default as Header } from './header';
export { default as Breadcrumb } from './breadcrumb';
export { default as Menu } from './menu';
+export { default as withNotFound, GqlPaths } from './not-found-hoc';
diff --git a/packages/cp-frontend/src/containers/navigation/menu.js b/packages/cp-frontend/src/containers/navigation/menu.js
index 480bdea5..fb1a399a 100644
--- a/packages/cp-frontend/src/containers/navigation/menu.js
+++ b/packages/cp-frontend/src/containers/navigation/menu.js
@@ -1,8 +1,11 @@
import React from 'react';
import { connect } from 'react-redux';
+import { compose } from 'react-apollo';
+import withNotFound from './not-found-hoc';
import { Menu as MenuComponent } from '@components/navigation';
-const Menu = ({ match, sections }) => {
+const Menu = ({ location, match, sections }) => {
+
if (!sections || !sections.length) {
return null;
}
@@ -16,7 +19,7 @@ const Menu = ({ match, sections }) => {
return ;
};
-const ConnectedMenu = connect(
+const connectMenu = connect(
(state, ownProps) => {
const params = ownProps.match.params;
const deploymentGroupSlug = params.deploymentGroup;
@@ -35,6 +38,9 @@ const ConnectedMenu = connect(
};
},
dispatch => ({})
-)(Menu);
+);
-export default ConnectedMenu;
+export default compose(
+ connectMenu,
+ withNotFound()
+)(Menu);
diff --git a/packages/cp-frontend/src/containers/navigation/not-found-hoc.js b/packages/cp-frontend/src/containers/navigation/not-found-hoc.js
new file mode 100644
index 00000000..f772d0f3
--- /dev/null
+++ b/packages/cp-frontend/src/containers/navigation/not-found-hoc.js
@@ -0,0 +1,81 @@
+import React, { Component } from 'react';
+import { NotFound } from '@components/navigation';
+
+export const GqlPaths = {
+ DEPLOYMENT_GROUP: 'deploymentGroup',
+ SERVICES: 'services'
+}
+
+export default (paths) => {
+ return (WrappedComponent) => {
+
+ return class extends Component {
+
+ constructor(props) {
+ super(props);
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if(paths) {
+ const {
+ error,
+ location,
+ history,
+ match
+ } = nextProps;
+
+ if (error && (!location.state || !location.state.notFound)) {
+ if(error.graphQLErrors && error.graphQLErrors.length) {
+ const graphQLError = error.graphQLErrors[0];
+ if(graphQLError.message === 'Not Found') {
+ const notFound = graphQLError.path.pop();
+ if(paths.indexOf(notFound) > -1) {
+ history.replace(location.pathname, { notFound });
+ }
+ }
+ }
+ }
+ }
+ }
+
+ render() {
+
+ const {
+ error,
+ location,
+ match
+ } = this.props;
+
+ if(location.state && location.state.notFound) {
+ const notFound = location.state.notFound;
+ if(paths && paths.indexOf(notFound) > -1) {
+ let title;
+ let to;
+ let link;
+ if(notFound === 'services' || notFound === 'service') {
+ title = 'This service doesn’t exist';
+ to = match.url.split('/').slice(0, 3).join('/');
+ link = 'Back to services';
+ }
+ else if(notFound === 'deploymentGroup') {
+ title = 'This deployment group doesn’t exist';
+ to = '/deployment-group';
+ link = 'Back to dashboard';
+ }
+ return (
+
+ )
+ }
+ return null;
+ }
+
+ return
+ }
+ }
+ }
+}
diff --git a/packages/cp-frontend/src/containers/service/delete.js b/packages/cp-frontend/src/containers/service/delete.js
index d6c40d6e..e2d076e5 100644
--- a/packages/cp-frontend/src/containers/service/delete.js
+++ b/packages/cp-frontend/src/containers/service/delete.js
@@ -2,10 +2,11 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { compose, graphql } from 'react-apollo';
import ServicesDeleteMutation from '@graphql/ServicesDeleteMutation.gql';
-import { Loader, ErrorMessage } from '@components/messaging';
+import { Loader, ModalErrorMessage } from '@components/messaging';
import { ServiceDelete as ServiceDeleteComponent } from '@components/service';
import { Modal, ModalHeading, Button } from 'joyent-ui-toolkit';
import ServiceGql from './service-gql';
+import { withNotFound, GqlPaths } from '@containers/navigation';
class ServiceDelete extends Component {
@@ -18,7 +19,7 @@ class ServiceDelete extends Component {
}
render() {
- const { loading, error } = this.props;
+ const { loading, error, match, history, location } = this.props;
const handleCloseClick = evt => {
const closeUrl = match.url.split('/').slice(0, -2).join('/');
@@ -36,27 +37,23 @@ class ServiceDelete extends Component {
if (error) {
return (
-
+ message='An error occured while loading your service.'
+ onCloseClick={handleCloseClick} />
);
}
- const { service, deleteServices, history, match } = this.props;
+ const { service, deleteServices } = this.props;
if(this.state.error) {
return (
-
- Deleting a service:
{service.name}
-
-
-
+ message={`An error occured while attempting to delete the ${service.name} service.`}
+ onCloseClick={handleCloseClick} />
);
}
@@ -96,4 +93,8 @@ const DeleteServicesGql = graphql(ServicesDeleteMutation, {
})
});
-export default compose(DeleteServicesGql, ServiceGql)(ServiceDelete);
+export default compose(
+ DeleteServicesGql,
+ ServiceGql,
+ withNotFound([ GqlPaths.SERVICES ])
+)(ServiceDelete);
diff --git a/packages/cp-frontend/src/containers/service/scale.js b/packages/cp-frontend/src/containers/service/scale.js
index ae53adba..fc87e0f1 100644
--- a/packages/cp-frontend/src/containers/service/scale.js
+++ b/packages/cp-frontend/src/containers/service/scale.js
@@ -3,10 +3,11 @@ 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 { Loader, ModalErrorMessage } from '@components/messaging';
import { ServiceScale as ServiceScaleComponent } from '@components/service';
import { Modal, ModalHeading, Button } from 'joyent-ui-toolkit';
import ServiceGql from './service-gql';
+import { withNotFound, GqlPaths } from '@containers/navigation';
class ServiceScale extends Component {
@@ -19,7 +20,7 @@ class ServiceScale extends Component {
}
render() {
- const { loading, error } = this.props;
+ const { loading, error, match, history, location } = this.props;
const handleCloseClick = evt => {
const closeUrl = match.url.split('/').slice(0, -2).join('/');
@@ -37,27 +38,23 @@ class ServiceScale extends Component {
if (error) {
return (
-
+ message='An error occured while loading your service.'
+ onCloseClick={handleCloseClick} />
);
}
- const { service, scale, history, match } = this.props;
+ const { service, scale } = this.props;
if(this.state.error) {
return (
-
- Scaling a service:
{service.name}
-
-
-
+ message={`An error occured while attempting to scale the ${service.name} service.`}
+ onCloseClick={handleCloseClick} />
);
}
@@ -124,4 +121,8 @@ const ServiceScaleGql = graphql(ServiceScaleMutation, {
})
});
-export default compose(ServiceScaleGql, ServiceGql)(ServiceScale);
+export default compose(
+ ServiceScaleGql,
+ ServiceGql,
+ withNotFound([ GqlPaths.SERVICES ])
+)(ServiceScale);
diff --git a/packages/cp-frontend/src/containers/services/list.js b/packages/cp-frontend/src/containers/services/list.js
index 2c1dd821..4043703a 100644
--- a/packages/cp-frontend/src/containers/services/list.js
+++ b/packages/cp-frontend/src/containers/services/list.js
@@ -21,6 +21,8 @@ import { ServicesQuickActions } from '@components/services';
import { Message } from 'joyent-ui-toolkit';
+import { withNotFound, GqlPaths } from '@containers/navigation';
+
const StyledContainer = styled.div`
position: relative;
`;
@@ -55,7 +57,8 @@ class ServiceList extends Component {
push,
restartServices,
stopServices,
- startServices
+ startServices,
+ location
} = this.props;
if (loading && !forceArray(services).length) {
@@ -77,6 +80,7 @@ class ServiceList extends Component {
}
if (
+ deploymentGroup &&
deploymentGroup.status === 'PROVISIONING' &&
!forceArray(services).length
) {
@@ -222,7 +226,7 @@ const ServicesGql = graphql(ServicesQuery, {
}
};
},
- props: ({ data: { deploymentGroup, loading, error } }) => ({
+ props: ({ data: { deploymentGroup, loading, error }}) => ({
deploymentGroup,
services: deploymentGroup
? processServices(deploymentGroup.services, null)
@@ -256,7 +260,8 @@ const ServiceListWithData = compose(
ServicesStopGql,
ServicesStartGql,
ServicesGql,
- UiConnect
+ UiConnect,
+ withNotFound([ GqlPaths.DEPLOYMENT_GROUP ])
)(ServiceList);
export default ServiceListWithData;
diff --git a/packages/cp-frontend/src/containers/services/menu.js b/packages/cp-frontend/src/containers/services/menu.js
index cc5aef47..260fd4fe 100644
--- a/packages/cp-frontend/src/containers/services/menu.js
+++ b/packages/cp-frontend/src/containers/services/menu.js
@@ -1,10 +1,12 @@
import React from 'react';
import PropTypes from 'prop-types';
+import { compose } from 'react-apollo';
import { Col, Row } from 'react-styled-flexboxgrid';
import remcalc from 'remcalc';
import unitcalc from 'unitcalc';
import { LayoutContainer } from '@components/layout';
import { Title } from '@components/navigation';
+import { withNotFound } from '@containers/navigation';
import { H2, FormGroup, Toggle, ToggleList, Legend } from 'joyent-ui-toolkit';
@@ -19,6 +21,11 @@ const PaddedRow = Row.extend`
`;
const ServicesMenu = ({ location, history: { push } }) => {
+
+ if(location.state && location.state.notFound) {
+ return null;
+ }
+
const toggleValue = location.pathname.split('-').pop();
const handleToggle = evt => {
@@ -57,4 +64,4 @@ ServicesMenu.propTypes = {
history: PropTypes.object.isRequired
};
-export default ServicesMenu;
+export default compose(withNotFound())(ServicesMenu);
diff --git a/packages/cp-frontend/src/containers/services/topology.js b/packages/cp-frontend/src/containers/services/topology.js
index 4ddc661a..6b703bed 100644
--- a/packages/cp-frontend/src/containers/services/topology.js
+++ b/packages/cp-frontend/src/containers/services/topology.js
@@ -2,6 +2,7 @@ import React, { Component } from 'react';
import { compose, graphql } from 'react-apollo';
import { connect } from 'react-redux';
import styled from 'styled-components';
+import forceArray from 'force-array';
import ServicesQuery from '@graphql/Services.gql';
import ServicesRestartMutation from '@graphql/ServicesRestartMutation.gql';
import ServicesStopMutation from '@graphql/ServicesStopMutation.gql';
@@ -17,6 +18,8 @@ import { ServicesQuickActions } from '@components/services';
import { Topology } from 'joyent-ui-toolkit';
+import { withNotFound, GqlPaths } from '@containers/navigation';
+
const StyledBackground = styled.div`
padding: ${unitcalc(4)};
background-color: ${props => props.theme.whiteActive};
@@ -50,10 +53,11 @@ class ServicesTopology extends Component {
toggleServicesQuickActions,
restartServices,
stopServices,
- startServices
+ startServices,
+ location
} = this.props;
- if (loading) {
+ if (loading && !forceArray(services).length) {
return (
@@ -62,7 +66,6 @@ class ServicesTopology extends Component {
}
if (error) {
-
return (
;
-const deploymentGroupRedirect = p =>
+const servicesListRedirect = p =>
;
+const servicesTopologyRedirect = p =>
+ ;
+
const serviceRedirect = p =>
;
-// TODO component to be designed
-const notFound = p => {
- return
- NOT FOUND
-
;
-}
-
-const APP = p => (
+const App = p => (
@@ -141,7 +141,7 @@ const APP = p => (
@@ -171,11 +171,11 @@ const APP = p => (
@@ -186,9 +186,9 @@ const Router = (
-
+
-
+
diff --git a/packages/cp-gql-mock-server/src/resolvers.js b/packages/cp-gql-mock-server/src/resolvers.js
index 302b4519..b9c1e893 100644
--- a/packages/cp-gql-mock-server/src/resolvers.js
+++ b/packages/cp-gql-mock-server/src/resolvers.js
@@ -9,6 +9,7 @@ const random = require('lodash.random');
const uniq = require('lodash.uniq');
const yaml = require('js-yaml');
const hasha = require('hasha');
+const Boom = require('boom');
const wpData = require('./wp-data.json');
const cpData = require('./cp-data.json');
@@ -96,7 +97,13 @@ const getServices = query => {
).map(serviceId => lfind(services, ['id', serviceId]));
});
- return Promise.resolve(services);
+ return Promise.resolve(services)
+ .then((services) => {
+ if(!services || !services.length) {
+ throw Boom.notFound();
+ }
+ return services;
+ });
};
const getDeploymentGroups = query => {
@@ -114,7 +121,12 @@ const getDeploymentGroups = query => {
return Promise.resolve(
deploymentGroups.filter(find(cleanQuery(query))).map(addNestedResolvers)
- );
+ ).then((deploymentGroups) => {
+ if(!deploymentGroups || !deploymentGroups.length) {
+ throw Boom.notFound();
+ }
+ return deploymentGroups;
+ });
};
const getPortal = () =>