From 496df34f745c8e2f91d909ea19730439bf79ccdf Mon Sep 17 00:00:00 2001
From: CD Cabrera
Date: Fri, 15 Mar 2019 15:29:28 -0400
Subject: [PATCH] fix(confirmation, formState, app, about, masthead, redux):
issues/1 (#10)
Support updates for
* confirmationModal, component switched from modal to messageDialog
* confirmationModal, unit tests, styles updated, cleaned up
* formState, correction to isValid check
* app, remove aboutModal and support methods, update auth logic
* app styles, fadein support styling for component transitions
* aboutModal, convert to connected redux state
* mastheadOptions, props update related to auth logic
* redux, actions, reducers, constants updated user and status trees
* apiConstants, user and status
---
.../__snapshots__/about.test.js.snap | 106 ---------
src/components/about/__tests__/about.test.js | 18 --
src/components/about/about.js | 60 -----
.../__snapshots__/aboutModal.test.js.snap | 124 +++++++++++
.../aboutModal/__tests__/aboutModal.test.js | 40 ++++
src/components/aboutModal/aboutModal.js | 95 ++++++++
src/components/app.js | 154 +++++--------
.../confirmationModal.test.js.snap | 206 +++++++++---------
.../__tests__/confirmationModal.test.js | 58 ++++-
.../confirmationModal/confirmationModal.js | 69 +++---
src/components/formState/formState.js | 6 +-
.../mastheadOptions.test.js.snap | 2 +-
.../__tests__/mastheadOptions.test.js | 4 +-
.../mastheadOptions/mastheadOptions.js | 8 +-
.../__snapshots__/apiConstants.test.js.snap | 16 ++
src/constants/apiConstants.js | 14 ++
src/index.js | 13 +-
.../actions/__tests__/statusActions.test.js | 5 +-
.../actions/__tests__/userActions.test.js | 23 +-
src/redux/actions/userActions.js | 8 +-
.../__snapshots__/index.test.js.snap | 16 ++
src/redux/constants/aboutModalConstants.js | 4 +
src/redux/constants/index.js | 3 +
.../aboutModalReducer.test.js.snap | 19 ++
.../__snapshots__/statusReducer.test.js.snap | 14 +-
.../__snapshots__/userReducer.test.js.snap | 110 ++--------
.../__tests__/aboutModalReducer.test.js | 26 +++
src/redux/reducers/aboutModalReducer.js | 41 ++++
src/redux/reducers/index.js | 3 +
src/redux/reducers/statusReducer.js | 55 +++--
src/redux/reducers/userReducer.js | 74 +------
src/styles/entitlements/_common.scss | 9 +
src/styles/entitlements/_views.scss | 40 ----
src/styles/index.scss | 1 +
34 files changed, 753 insertions(+), 691 deletions(-)
delete mode 100644 src/components/about/__tests__/__snapshots__/about.test.js.snap
delete mode 100644 src/components/about/__tests__/about.test.js
delete mode 100644 src/components/about/about.js
create mode 100644 src/components/aboutModal/__tests__/__snapshots__/aboutModal.test.js.snap
create mode 100644 src/components/aboutModal/__tests__/aboutModal.test.js
create mode 100644 src/components/aboutModal/aboutModal.js
create mode 100644 src/redux/constants/aboutModalConstants.js
create mode 100644 src/redux/reducers/__tests__/__snapshots__/aboutModalReducer.test.js.snap
create mode 100644 src/redux/reducers/__tests__/aboutModalReducer.test.js
create mode 100644 src/redux/reducers/aboutModalReducer.js
create mode 100644 src/styles/entitlements/_common.scss
diff --git a/src/components/about/__tests__/__snapshots__/about.test.js.snap b/src/components/about/__tests__/__snapshots__/about.test.js.snap
deleted file mode 100644
index 95dd594d..00000000
--- a/src/components/about/__tests__/__snapshots__/about.test.js.snap
+++ /dev/null
@@ -1,106 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`About Component should render a basic display 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
- Version
-
- unknown (Build: unknown)
-
- -
-
- Username
-
- admin
-
- -
-
- Browser Version
-
-
-
- -
-
- Browser OS
-
-
-
-
-
-
-
-
-
-
-
-
-`;
diff --git a/src/components/about/__tests__/about.test.js b/src/components/about/__tests__/about.test.js
deleted file mode 100644
index a20f6ef3..00000000
--- a/src/components/about/__tests__/about.test.js
+++ /dev/null
@@ -1,18 +0,0 @@
-import React from 'react';
-import { mount } from 'enzyme';
-import About from '../about';
-
-describe('About Component', () => {
- it('should render a basic display', () => {
- const props = {
- user: { currentUser: { username: 'admin' } },
- status: {},
- shown: true,
- onClose: jest.fn()
- };
-
- const component = mount();
-
- expect(component.render()).toMatchSnapshot();
- });
-});
diff --git a/src/components/about/about.js b/src/components/about/about.js
deleted file mode 100644
index cb39a0bf..00000000
--- a/src/components/about/about.js
+++ /dev/null
@@ -1,60 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { detect } from 'detect-browser';
-import { AboutModal } from 'patternfly-react';
-import _get from 'lodash/get';
-import helpers from '../../common/helpers';
-import logo from '../../styles/images/logo.svg';
-import productTitle from '../../styles/images/title.svg';
-import rhLogo from '../../styles/images/logo-brand.svg';
-import rhProductTitle from '../../styles/images/title-brand.svg';
-
-const About = ({ user, status, shown, onClose }) => {
- const versionText = `${_get(status, 'api_version', 'unknown')} (Build: ${_get(status, 'build', 'unknown')})`;
- const browser = detect();
-
- const props = {
- show: shown,
- onHide: onClose,
- logo,
- productTitle: ,
- altLogo: 'ER'
- };
-
- if (helpers.RH_BRAND) {
- props.logo = rhLogo;
- props.productTitle = ;
- props.altLogo = 'RH ER';
- props.trademarkText = 'Copyright (c) 2018 Red Hat Inc.';
- }
-
- return (
-
-
-
-
-
-
-
-
- );
-};
-
-About.propTypes = {
- user: PropTypes.object,
- status: PropTypes.object,
- shown: PropTypes.bool,
- onClose: PropTypes.func
-};
-
-About.defaultProps = {
- user: {},
- status: {},
- shown: false,
- onClose: helpers.noop
-};
-
-export { About as default, About };
diff --git a/src/components/aboutModal/__tests__/__snapshots__/aboutModal.test.js.snap b/src/components/aboutModal/__tests__/__snapshots__/aboutModal.test.js.snap
new file mode 100644
index 00000000..03e97c8c
--- /dev/null
+++ b/src/components/aboutModal/__tests__/__snapshots__/aboutModal.test.js.snap
@@ -0,0 +1,124 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`AboutModal Component should contain brand: brand 1`] = `
+
+ }
+ show={true}
+ trademarkText="Copyright (c) 2019 Red Hat Inc."
+>
+
+
+
+
+`;
+
+exports[`AboutModal Component should render a connected component with default props: connected 1`] = `
+
+`;
+
+exports[`AboutModal Component should render a non-connected component: hidden modal 1`] = `
+
+
+ }
+ show={false}
+ trademarkText=""
+ >
+
+
+
+
+
+`;
diff --git a/src/components/aboutModal/__tests__/aboutModal.test.js b/src/components/aboutModal/__tests__/aboutModal.test.js
new file mode 100644
index 00000000..d26997f7
--- /dev/null
+++ b/src/components/aboutModal/__tests__/aboutModal.test.js
@@ -0,0 +1,40 @@
+import React from 'react';
+import configureMockStore from 'redux-mock-store';
+import { mount, shallow } from 'enzyme';
+import { ConnectedAboutModal, AboutModal } from '../aboutModal';
+
+describe('AboutModal Component', () => {
+ const generateEmptyStore = (obj = {}) => configureMockStore()(obj);
+
+ it('should render a connected component with default props', () => {
+ const store = generateEmptyStore({
+ aboutModal: { show: true },
+ user: { session: { username: 'lorem' } },
+ status: { apiVersion: 1 }
+ });
+ const component = shallow(, { context: { store } });
+
+ expect(component).toMatchSnapshot('connected');
+ });
+
+ it('should render a non-connected component', () => {
+ const props = {
+ show: false,
+ apiVersion: 1,
+ username: 'admin'
+ };
+
+ const component = mount();
+ expect(component).toMatchSnapshot('hidden modal');
+ });
+
+ it('should contain brand', () => {
+ const props = {
+ show: true,
+ brand: true
+ };
+
+ const component = shallow();
+ expect(component).toMatchSnapshot('brand');
+ });
+});
diff --git a/src/components/aboutModal/aboutModal.js b/src/components/aboutModal/aboutModal.js
new file mode 100644
index 00000000..b1130967
--- /dev/null
+++ b/src/components/aboutModal/aboutModal.js
@@ -0,0 +1,95 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { detect } from 'detect-browser';
+import { AboutModal as PfAboutModal } from 'patternfly-react';
+import { connect, reduxActions, reduxTypes, store } from '../../redux';
+import helpers from '../../common/helpers';
+import logoImg from '../../styles/images/logo.svg';
+import titleImg from '../../styles/images/title.svg';
+import logoImgBrand from '../../styles/images/logo-brand.svg';
+import titleImgBrand from '../../styles/images/title-brand.svg';
+
+class AboutModal extends React.Component {
+ componentDidUpdate() {
+ const { apiVersion, getStatus, show } = this.props;
+
+ if (show && apiVersion === null) {
+ getStatus();
+ }
+ }
+
+ onClose = () => {
+ store.dispatch({
+ type: reduxTypes.aboutModal.ABOUT_MODAL_HIDE
+ });
+ };
+
+ render() {
+ const { apiVersion, brand, build, show, username } = this.props;
+ const versionText = `${apiVersion || 'unknown'} (Build: ${build || 'unknown'})`;
+ const browser = detect();
+
+ const props = {
+ logo: logoImg,
+ productTitle: ,
+ altLogo: 'ER'
+ };
+
+ if (brand) {
+ props.logo = logoImgBrand;
+ props.productTitle = ;
+ props.altLogo = 'RH ER';
+ props.trademarkText = 'Copyright (c) 2019 Red Hat Inc.';
+ }
+
+ return (
+
+
+
+ {username && }
+ {browser && (
+
+ )}
+ {browser && }
+
+
+ );
+ }
+}
+
+AboutModal.propTypes = {
+ apiVersion: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ brand: PropTypes.bool,
+ build: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ getStatus: PropTypes.func,
+ getUser: PropTypes.func,
+ show: PropTypes.bool.isRequired,
+ username: PropTypes.string
+};
+
+AboutModal.defaultProps = {
+ apiVersion: null,
+ brand: helpers.RH_BRAND,
+ build: null,
+ getStatus: helpers.noop,
+ getUser: helpers.noop,
+ username: null
+};
+
+const mapDispatchToProps = dispatch => ({
+ getStatus: () => dispatch(reduxActions.status.getStatus())
+});
+
+const mapStateToProps = state => ({
+ apiVersion: state.status.apiVersion,
+ build: state.status.build,
+ show: state.aboutModal.show,
+ username: state.user.session.username
+});
+
+const ConnectedAboutModal = connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(AboutModal);
+
+export { ConnectedAboutModal as default, ConnectedAboutModal, AboutModal };
diff --git a/src/components/app.js b/src/components/app.js
index a92f2519..9a7e4aeb 100644
--- a/src/components/app.js
+++ b/src/components/app.js
@@ -2,45 +2,34 @@ import React from 'react';
import PropTypes from 'prop-types';
import { withRouter } from 'react-router';
import { Alert, EmptyState, Modal, VerticalNav } from 'patternfly-react';
-import _get from 'lodash/get';
import _startsWith from 'lodash/startsWith';
import { routes } from '../routes';
-import { connect, reduxActions } from '../redux';
+import { connect, reduxActions, reduxTypes, store } from '../redux';
import helpers from '../common/helpers';
-import About from './about/about';
+import AboutModal from './aboutModal/aboutModal';
import AddSourceWizard from './addSourceWizard/addSourceWizard';
import CreateCredentialDialog from './createCredentialDialog/createCredentialDialog';
import Content from './content/content';
import ToastNotificationsList from './toastNotificationsList/toastNotificationsList';
import ConfirmationModal from './confirmationModal/confirmationModal';
import MastheadOptions from './mastheadOptions/mastheadOptions';
-import productTitle from '../styles/images/title.svg';
-import rhProductTitle from '../styles/images/title-brand.svg';
+import titleImg from '../styles/images/title.svg';
+import titleImgBrand from '../styles/images/title-brand.svg';
class App extends React.Component {
- constructor() {
- super();
-
- this.menu = routes();
- this.state = {
- aboutShown: false
- };
- }
-
componentDidMount() {
- const { authorizeUser, getStatus } = this.props;
+ const { authorizeUser } = this.props;
authorizeUser();
- getStatus();
}
- componentWillReceiveProps(nextProps) {
- const { getUser } = this.props;
+ onLogout = () => {
+ const { logoutUser } = this.props;
- if (_get(nextProps, 'session.loggedIn') && !_get(this.props, 'session.loggedIn')) {
- getUser();
- }
- }
+ logoutUser().finally(() => {
+ window.location = '/logout';
+ });
+ };
onNavigateTo = path => {
const { history } = this.props;
@@ -48,19 +37,16 @@ class App extends React.Component {
};
onShowAbout = () => {
- this.setState({ aboutShown: true });
- };
-
- onCloseAbout = () => {
- this.setState({ aboutShown: false });
+ store.dispatch({
+ type: reduxTypes.aboutModal.ABOUT_MODAL_SHOW
+ });
};
renderMenuItems() {
- const { location } = this.props;
+ const { location, menu } = this.props;
+ const activeItem = menu.find(item => _startsWith(location.pathname, item.to));
- const activeItem = this.menu.find(item => _startsWith(location.pathname, item.to));
-
- return this.menu.map(item => (
+ return menu.map(item => (
- Please login to continue.
-
- );
- }
-
- return (
-
-
-
- Login error: {session.errorMessage.replace(/\.$/, '')}. {loginMessage}
-
-
-
- );
- }
+ render() {
+ const { session } = this.props;
+ const productTitleImg = helpers.RH_BRAND ? titleImgBrand : titleImg;
- if (session.pending || !session.fulfilled || (!session.loggedIn && !session.wasLoggedIn)) {
+ if (session.pending) {
return (
@@ -117,53 +81,53 @@ class App extends React.Component {
);
}
- return (
-
-
-
-
-
-
-
-
- );
- }
-
- render() {
- const { user, session, logoutUser } = this.props;
-
- if (!session.loggedIn && session.wasLoggedIn) {
- window.location = '/logout';
- }
-
- const titleImg = helpers.RH_BRAND ? rhProductTitle : productTitle;
-
- if (!session.loggedIn) {
+ if (!session.authorized || session.error) {
return (
-
+
-
{this.renderContent()}
+
+
+
+
+ Login error: {session.errorMessage.replace(/\.$/, '')}
+ {session.errorMessage && '.'}
+ {!session.authorized && (
+
+ Please login to continue.
+
+ )}
+
+
+
+
);
}
return (
-
+
-
-
+
+
{this.renderMenuItems()}
{this.renderMenuActions()}
-
{this.renderContent()}
+
);
}
@@ -171,13 +135,10 @@ class App extends React.Component {
App.propTypes = {
authorizeUser: PropTypes.func,
- getUser: PropTypes.func,
- getStatus: PropTypes.func,
logoutUser: PropTypes.func,
session: PropTypes.object,
- user: PropTypes.object,
- status: PropTypes.object,
location: PropTypes.object,
+ menu: PropTypes.array,
history: PropTypes.shape({
push: PropTypes.func.isRequired
}).isRequired
@@ -185,26 +146,19 @@ App.propTypes = {
App.defaultProps = {
authorizeUser: helpers.noop,
- getUser: helpers.noop,
- getStatus: helpers.noop,
logoutUser: helpers.noop,
+ menu: routes(),
session: {},
- user: {},
- status: {},
location: {}
};
const mapDispatchToProps = dispatch => ({
authorizeUser: () => dispatch(reduxActions.user.authorizeUser()),
- getUser: () => dispatch(reduxActions.user.getUser()),
- logoutUser: () => dispatch(reduxActions.user.logoutUser()),
- getStatus: () => dispatch(reduxActions.status.getStatus())
+ logoutUser: () => dispatch(reduxActions.user.logoutUser())
});
const mapStateToProps = state => ({
- session: state.user.session,
- user: state.user.user,
- status: state.status.currentStatus
+ session: state.user.session
});
export default withRouter(
diff --git a/src/components/confirmationModal/__tests__/__snapshots__/confirmationModal.test.js.snap b/src/components/confirmationModal/__tests__/__snapshots__/confirmationModal.test.js.snap
index dcaec075..6efa851d 100644
--- a/src/components/confirmationModal/__tests__/__snapshots__/confirmationModal.test.js.snap
+++ b/src/components/confirmationModal/__tests__/__snapshots__/confirmationModal.test.js.snap
@@ -1,110 +1,116 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`ConfirmationModal Component should shallow render a basic component 1`] = `
-
-
-
-
- Confirm
-
-
-
+
-
-
-
-
-
-
-
-
+ class="modal-header"
+ >
+
+
+ Confirm
+
+
+
+
+
+ test
+
+
+ Test body
+
+
+
+
+
-
-
-
-
-
-
+
+
+`;
+
+exports[`Confirmation Modal Component should render a connected component: connected 1`] = `
+
+ test
+
+ }
+ secondaryAction={[Function]}
+ secondaryActionButtonBsStyle="default"
+ secondaryActionButtonContent="Cancel"
+ secondaryContent={
+
+ Test body
+
+ }
+ show={true}
+ title="Confirm"
+/>
`;
diff --git a/src/components/confirmationModal/__tests__/confirmationModal.test.js b/src/components/confirmationModal/__tests__/confirmationModal.test.js
index 0219edc8..3a0757f7 100644
--- a/src/components/confirmationModal/__tests__/confirmationModal.test.js
+++ b/src/components/confirmationModal/__tests__/confirmationModal.test.js
@@ -1,16 +1,56 @@
import React from 'react';
import configureMockStore from 'redux-mock-store';
-import { shallow } from 'enzyme';
-import ConfirmationModal from '../confirmationModal';
+import { mount, shallow } from 'enzyme';
+import { ConnectedConfirmationModal, ConfirmationModal } from '../confirmationModal';
-describe('ConfirmationModal Component', () => {
- const generateEmptyStore = () => configureMockStore()({ confirmationModal: {} });
+describe('Confirmation Modal Component', () => {
+ const generateEmptyStore = (obj = {}) => configureMockStore()(obj);
- it('should shallow render a basic component', () => {
- const store = generateEmptyStore();
- const props = { show: true };
- const wrapper = shallow(, { context: { store } });
+ it('should render a connected component', () => {
+ const store = generateEmptyStore({
+ confirmationModal: {
+ show: true,
+ title: 'Confirm',
+ heading: 'test',
+ icon: null,
+ body: 'Test body',
+ confirmButtonText: 'Confirm',
+ cancelButtonText: 'Cancel'
+ }
+ });
+ const component = shallow(, { context: { store } });
- expect(wrapper.dive()).toMatchSnapshot();
+ expect(component.dive()).toMatchSnapshot('connected');
+ });
+
+ it('should display a confirmation modal', () => {
+ const onCancel = jest.fn();
+ const props = {
+ show: true,
+ title: 'Confirm',
+ heading: 'test',
+ icon: null,
+ body: 'Test body',
+ confirmButtonText: 'Confirm',
+ cancelButtonText: 'Cancel',
+ onCancel
+ };
+
+ const component = mount();
+
+ expect(component.render()).toMatchSnapshot('show');
+
+ component.find('button[className="btn btn-default"]').simulate('click');
+ expect(onCancel).toHaveBeenCalledTimes(1);
+ });
+
+ it('should NOT display a confirmation modal', () => {
+ const props = {
+ show: false
+ };
+
+ const component = mount();
+
+ expect(component.render()).toMatchSnapshot('hidden');
});
});
diff --git a/src/components/confirmationModal/confirmationModal.js b/src/components/confirmationModal/confirmationModal.js
index 4c724739..f96c996a 100644
--- a/src/components/confirmationModal/confirmationModal.js
+++ b/src/components/confirmationModal/confirmationModal.js
@@ -1,13 +1,21 @@
import React from 'react';
import PropTypes from 'prop-types';
-import { Modal, Button, Icon } from 'patternfly-react';
-import { connect, reduxTypes, store } from '../../redux';
+import { MessageDialog, Icon } from 'patternfly-react';
+import { connect, store, reduxTypes } from '../../redux';
import helpers from '../../common/helpers';
-class ConfirmationModal extends React.Component {
- onClose = () => {
- const { onCancel } = this.props;
-
+const ConfirmationModal = ({
+ show,
+ title,
+ heading,
+ body,
+ icon,
+ confirmButtonText,
+ cancelButtonText,
+ onConfirm,
+ onCancel
+}) => {
+ const cancel = () => {
if (onCancel) {
onCancel();
} else {
@@ -17,40 +25,21 @@ class ConfirmationModal extends React.Component {
}
};
- render() {
- const { show, title, heading, body, icon, confirmButtonText, cancelButtonText, onConfirm } = this.props;
-
- return (
-
-
-
- {title}
-
-
-
- {icon &&
{icon}}
-
-
- {heading}
- {body}
-
-
-
-
-
-
-
-
-
- );
- }
-}
+ return (
+ {heading}}
+ secondaryContent={{body}
}
+ />
+ );
+};
ConfirmationModal.propTypes = {
show: PropTypes.bool.isRequired,
diff --git a/src/components/formState/formState.js b/src/components/formState/formState.js
index 2aa64e04..e3e3dc71 100644
--- a/src/components/formState/formState.js
+++ b/src/components/formState/formState.js
@@ -101,7 +101,7 @@ class FormState extends React.Component {
() =>
this.validate(event).then(updatedErrors => {
const setUpdateErrors = { ...((updatedErrors && updatedErrors[0]) || updatedErrors || {}) };
- const checkIsValid = !Object.keys(setUpdateErrors).length;
+ const checkIsValid = !Object.values(setUpdateErrors).filter(val => val === true).length;
this.errors = setUpdateErrors;
@@ -158,7 +158,7 @@ class FormState extends React.Component {
() =>
this.validate(event).then(updatedErrors => {
const setUpdateErrors = { ...((updatedErrors && updatedErrors[0]) || updatedErrors || {}) };
- const checkIsValid = !Object.keys(setUpdateErrors).length;
+ const checkIsValid = !Object.values(setUpdateErrors).filter(val => val === true).length;
this.errors = setUpdateErrors;
this.touched = {};
@@ -230,7 +230,7 @@ class FormState extends React.Component {
() =>
this.validate(event).then(updatedErrors => {
const setUpdateErrors = { ...((updatedErrors && updatedErrors[0]) || updatedErrors || {}) };
- const checkIsValid = !Object.keys(setUpdateErrors).length;
+ const checkIsValid = !Object.values(setUpdateErrors).filter(val => val === true).length;
this.errors = setUpdateErrors;
diff --git a/src/components/mastheadOptions/__tests__/__snapshots__/mastheadOptions.test.js.snap b/src/components/mastheadOptions/__tests__/__snapshots__/mastheadOptions.test.js.snap
index 6ae34eed..1615d188 100644
--- a/src/components/mastheadOptions/__tests__/__snapshots__/mastheadOptions.test.js.snap
+++ b/src/components/mastheadOptions/__tests__/__snapshots__/mastheadOptions.test.js.snap
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`MastheadOptions Component should render 1`] = `
+exports[`MastheadOptions Component should render a basic component 1`] = `