Updated 500 page copy and added button to reveal error message. (#1)

Updated the 500 page component to render a "SHOW ERROR" button.

This button is only rendered if the errors slice of the state contains an error and the base property exists. Otherwise, the 500 page will not render this button because there is no error message to show the user.

Created a errors500 reducer and actions to update the state tree when a 500 error occurs. When 500 error occurs, the errors slice of state is updated with the error object. When the 500 page component unmounts the error object is removed from state.

Demo: https://www.loom.com/share/b87c4aee42274e7bb553e703d3f950c6
This commit is contained in:
noahtalerman 2020-11-04 12:07:53 -08:00 committed by GitHub
parent 2e333a4e2e
commit d604c6a106
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 158 additions and 4 deletions

View File

@ -0,0 +1,6 @@
import PropTypes from 'prop-types';
export default PropTypes.shape({
http_status: PropTypes.number,
base: PropTypes.string,
});

View File

@ -1,12 +1,72 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { noop } from 'lodash';
import { resetErrors } from 'redux/nodes/errors500/actions';
import errorsInterface from 'interfaces/errors500';
import kolideLogo from '../../../assets/images/kolide-logo-condensed.svg';
import gopher from '../../../assets/images/500.svg';
const baseClass = 'kolide-500';
class Kolide404 extends Component {
class Kolide500 extends Component {
static propTypes = {
errors: errorsInterface,
dispatch: PropTypes.func,
};
static defaultProps = {
dispatch: noop,
};
constructor (props) {
super(props);
this.state = {
showErrorMessage: false,
};
}
componentWillUnmount() {
const { dispatch } = this.props;
dispatch(resetErrors());
}
onShowErrorMessage = () => {
this.setState({ showErrorMessage: true });
}
renderError = () => {
const { errors } = this.props;
const errorMessage = errors ? errors.base : null;
const { showErrorMessage } = this.state;
const { onShowErrorMessage } = this;
if (errorMessage && !showErrorMessage) {
// We only show the button when errorMessage exists
// and showErrorMessage is set to false
return (
<button className="button button--muted" onClick={onShowErrorMessage}>SHOW ERROR</button>
);
}
if (errorMessage && showErrorMessage) {
// We only show the error message when errorMessage exists
// and showErrorMessage is set to true
return (
<div className="error-message-container">
<p>{errorMessage}</p>
</div>
);
}
return false;
}
render () {
const { renderError } = this;
return (
<div className={baseClass}>
<header className="primary-header">
@ -18,10 +78,17 @@ class Kolide404 extends Component {
<h1>Uh oh!</h1>
<h2>Error 500</h2>
<p>Something went wrong on our end.</p>
<p>We have alerted the engineers and they are working on a solution.</p>
{renderError()}
<p>Please file an issue if you believe this is a bug.</p>
<a
href="https://github.com/fleetdm/fleet/issues"
target="_blank"
rel="noopener noreferrer"
>
File an issue
</a>
<div className="gopher-container">
<img src={gopher} alt="" />
<p>Need assistance? <a href="https://github.com/kolide/fleet/issues">File an issue</a>.</p>
</div>
</main>
</div>
@ -29,4 +96,11 @@ class Kolide404 extends Component {
}
}
export default Kolide404;
const mapStateToProps = (state) => {
const { errors } = state.errors500;
return {
errors,
};
};
export default connect(mapStateToProps)(Kolide500);

View File

@ -36,6 +36,10 @@
}
}
.error-message-container {
display: inline;
}
main {
text-align: center;

View File

@ -0,0 +1,7 @@
export const RESET_ERRORS = 'RESET_ERRORS';
export const resetErrors = () => {
return {
type: RESET_ERRORS,
};
};

View File

@ -0,0 +1,22 @@
import {
RESET_ERRORS,
} from './actions';
const initialState = {
errors: null,
};
const reducer = (state = initialState, { type, payload }) => {
if (payload && payload.errors) {
return {
errors: payload.errors,
};
} else if (type === RESET_ERRORS) {
return {
errors: null,
};
}
return state;
};
export default reducer;

View File

@ -0,0 +1,39 @@
import expect from 'expect';
import reducer from './reducer';
describe('Errors - reducer', () => {
it('Updates state with errors object when an action that has a payload with an errors object is dispatched', () => {
const payload = {
errors: {
base: "inserting pack: Error 1136: Column count doesn't match value count at row 1",
http_status: 500,
},
};
const packsCreateFailureAction = { type: 'packs_CREATE_FAILURE', payload };
const initialState = {
errors: null,
};
const newState = reducer(initialState, packsCreateFailureAction);
expect(newState).toEqual({
errors: {
base: "inserting pack: Error 1136: Column count doesn't match value count at row 1",
http_status: 500,
},
});
});
it('Updates state by setting errors to null when the RESET_ERRORS action is dipatched', () => {
const errorsState = {
errors: {
base: "inserting pack: Error 1136: Column count doesn't match value count at row 1",
http_status: 500,
},
};
const newState = reducer(errorsState, { type: 'RESET_ERRORS' });
expect(newState).toEqual({
errors: null,
});
});
});

View File

@ -6,6 +6,7 @@ import app from './nodes/app/reducer';
import auth from './nodes/auth/reducer';
import components from './nodes/components/reducer';
import entities from './nodes/entities/reducer';
import errors500 from './nodes/errors500/reducer';
import notifications from './nodes/notifications/reducer';
import persistentFlash from './nodes/persistent_flash/reducer';
import redirectLocation from './nodes/redirectLocation/reducer';
@ -15,6 +16,7 @@ export default combineReducers({
auth,
components,
entities,
errors500,
loadingBar: loadingBarReducer,
notifications,
persistentFlash,