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 = () =>