feat(cp-gql-mock-server, cp-frontend): Add missing dg and service error messaging

This commit is contained in:
JUDIT GRESKOVITS 2017-08-07 18:11:13 +01:00 committed by Judit Greskovits
parent 0917d67b07
commit 2eb7f4197f
18 changed files with 281 additions and 79 deletions

View File

@ -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';

View File

@ -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 }) =>
<div>
<StyledHeading>{title}</StyledHeading>
<ModalText marginBottom="3">{message}
</ModalText>
<Button onClick={onCloseClick} secondary>Close </Button>
</div>;
ModalErrorMessage.propTypes = {
title: PropTypes.string,
message: PropTypes.string.isRequired,
onCloseClick: PropTypes.func.isRequired
};
export default ModalErrorMessage;

View File

@ -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';

View File

@ -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 cant find what you are looking for. Next time, always follow your nose.',
link = 'Back to dashboard',
to = '/deployment-groups'
}) => (
<LayoutContainer>
<StyledContainer>
<StyledTitle>{title}</StyledTitle>
<StyledP>{message}</StyledP>
<Button to={to}>{link}</Button>
</StyledContainer>
</LayoutContainer>
);
NotFound.propTypes = {
title: PropTypes.string,
message: PropTypes.string,
link: PropTypes.string,
to: PropTypes.string
}
export default NotFound;

View File

@ -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 (
<Modal width={460} onCloseClick={handleCloseClick}>
<ErrorMessage
<ModalErrorMessage
title='Ooops!'
message='An error occurred while loading your deployment group.' />
message='An error occurred while loading your deployment group.'
onCloseClick={handleCloseClick} />
</Modal>
);
}
const {
deploymentGroup,
deleteDeploymentGroup,
history,
match
deleteDeploymentGroup
} = this.props;
if (this.state.error) {
return (
<Modal width={460} onCloseClick={handleCloseClick}>
<ModalHeading>
Deleting a deployment group: <br /> {deploymentGroup.name}
</ModalHeading>
<ErrorMessage
<ModalErrorMessage
title='Ooops!'
message='An error occurred while attempting to delete your deployment group.' />
<Button onClick={handleCloseClick} secondary>
Ok
</Button>
message={`An error occured while attempting to delete the ${deploymentGroup.name} deployment group.`}
onCloseClick={handleCloseClick} />
</Modal>
);
}
@ -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;

View File

@ -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);

View File

@ -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 = <Title>Instances</Title>;
@ -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);

View File

@ -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 <BreadcrumbComponent links={links} />;
};
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);

View File

@ -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';

View File

@ -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 <MenuComponent links={sectionsWithPathnames} />;
};
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);

View File

@ -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 doesnt exist';
to = match.url.split('/').slice(0, 3).join('/');
link = 'Back to services';
}
else if(notFound === 'deploymentGroup') {
title = 'This deployment group doesnt exist';
to = '/deployment-group';
link = 'Back to dashboard';
}
return (
<NotFound
title={title}
message='Sorry, but our princess is in another castle.'
to={to}
link={link}
/>
)
}
return null;
}
return <WrappedComponent {...this.props} />
}
}
}
}

View File

@ -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 (
<Modal width={460} onCloseClick={handleCloseClick}>
<ErrorMessage
<ModalErrorMessage
title='Ooops!'
message='An error occured while loading your service.' />
message='An error occured while loading your service.'
onCloseClick={handleCloseClick} />
</Modal>
);
}
const { service, deleteServices, history, match } = this.props;
const { service, deleteServices } = this.props;
if(this.state.error) {
return (
<Modal width={460} onCloseClick={handleCloseClick}>
<ModalHeading>
Deleting a service: <br /> {service.name}
</ModalHeading>
<ErrorMessage
<ModalErrorMessage
title='Ooops!'
message='An error occurred while attempting to delete your service.' />
<Button onClick={handleCloseClick} secondary>
Ok
</Button>
message={`An error occured while attempting to delete the ${service.name} service.`}
onCloseClick={handleCloseClick} />
</Modal>
);
}
@ -96,4 +93,8 @@ const DeleteServicesGql = graphql(ServicesDeleteMutation, {
})
});
export default compose(DeleteServicesGql, ServiceGql)(ServiceDelete);
export default compose(
DeleteServicesGql,
ServiceGql,
withNotFound([ GqlPaths.SERVICES ])
)(ServiceDelete);

View File

@ -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 (
<Modal width={460} onCloseClick={handleCloseClick}>
<ErrorMessage
<ModalErrorMessage
title='Ooops!'
message='An error occured while loading your service.' />
message='An error occured while loading your service.'
onCloseClick={handleCloseClick} />
</Modal>
);
}
const { service, scale, history, match } = this.props;
const { service, scale } = this.props;
if(this.state.error) {
return (
<Modal width={460} onCloseClick={handleCloseClick}>
<ModalHeading>
Scaling a service: <br /> {service.name}
</ModalHeading>
<ErrorMessage
<ModalErrorMessage
title='Ooops!'
message='An error occurred while attempting to scale your service.' />
<Button onClick={handleCloseClick} secondary>
Ok
</Button>
message={`An error occured while attempting to scale the ${service.name} service.`}
onCloseClick={handleCloseClick} />
</Modal>
);
}
@ -124,4 +121,8 @@ const ServiceScaleGql = graphql(ServiceScaleMutation, {
})
});
export default compose(ServiceScaleGql, ServiceGql)(ServiceScale);
export default compose(
ServiceScaleGql,
ServiceGql,
withNotFound([ GqlPaths.SERVICES ])
)(ServiceScale);

View File

@ -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;

View File

@ -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);

View File

@ -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 (
<LayoutContainer center>
<Loader />
@ -62,7 +66,6 @@ class ServicesTopology extends Component {
}
if (error) {
return (
<LayoutContainer>
<ErrorMessage
@ -73,6 +76,7 @@ class ServicesTopology extends Component {
}
if (
deploymentGroup &&
deploymentGroup.status === 'PROVISIONING' &&
!forceArray(services).length
) {
@ -232,7 +236,8 @@ const ServicesTopologyWithData = compose(
ServicesStopGql,
ServicesStartGql,
ServicesGql,
UiConnect
UiConnect,
withNotFound([ GqlPaths.DEPLOYMENT_GROUP ])
)(ServicesTopology);
export default ServicesTopologyWithData;

View File

@ -21,6 +21,8 @@ import {
import { DeploymentGroupDelete } from '@containers/deployment-group';
import { NotFound } from '@components/navigation';
const Container = styled.div`
display: flex;
flex: 1 1 auto;
@ -30,25 +32,23 @@ const Container = styled.div`
const rootRedirect = p => <Redirect to="/deployment-groups" />;
const deploymentGroupRedirect = p =>
const servicesListRedirect = p =>
<Redirect
to={`/deployment-groups/${p.match.params.deploymentGroup}/services-list`}
/>;
const servicesTopologyRedirect = p =>
<Redirect
to={`/deployment-groups/${p.match.params.deploymentGroup}/services-topology`}
/>;
const serviceRedirect = p =>
<Redirect
to={`/deployment-groups/${p.match.params.deploymentGroup}/services/${p.match
.params.service}/instances`}
/>;
// TODO component to be designed
const notFound = p => {
return <p>
NOT FOUND
</p>;
}
const APP = p => (
const App = p => (
<div>
<Switch>
@ -141,7 +141,7 @@ const APP = p => (
<Route
path="/deployment-groups/:deploymentGroup"
component={deploymentGroupRedirect}
component={servicesListRedirect}
/>
</Switch>
@ -171,11 +171,11 @@ const APP = p => (
<Route
path="/deployment-groups/:deploymentGroup/services-list"
component={deploymentGroupRedirect}
component={servicesListRedirect}
/>
<Route
path="/deployment-groups/:deploymentGroup/services-topology"
component={deploymentGroupRedirect}
component={servicesTopologyRedirect}
/>
</Switch>
</div>
@ -186,9 +186,9 @@ const Router = (
<Container>
<Route path="/" component={Header} />
<Switch>
<Route path="/deployment-groups" component={APP} />
<Route path="/deployment-groups" component={App} />
<Route path="/" exact component={rootRedirect} />
<Route path="/*" component={notFound} />
<Route path="/*" component={NotFound} />
</Switch>
</Container>
</BrowserRouter>

View File

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