diff --git a/__tests__/components/About.test.jsx b/__tests__/components/About.test.jsx index 336b9a7141..f790a8561a 100644 --- a/__tests__/components/About.test.jsx +++ b/__tests__/components/About.test.jsx @@ -1,15 +1,41 @@ import React from 'react'; import { mount } from 'enzyme'; +import api from '../../src/api'; +import { API_CONFIG } from '../../src/endpoints'; import About from '../../src/components/About'; -let aboutWrapper; -let headerElem; - describe('', () => { + let aboutWrapper; + let closeButton; + test('initially renders without crashing', () => { - aboutWrapper = mount(); - headerElem = aboutWrapper.find('h2'); + aboutWrapper = mount(); expect(aboutWrapper.length).toBe(1); - expect(headerElem.length).toBe(1); + aboutWrapper.unmount(); + }); + + test('close button calls onAboutModalClose', () => { + const onAboutModalClose = jest.fn(); + aboutWrapper = mount(); + closeButton = aboutWrapper.find('AboutModalBoxCloseButton Button'); + closeButton.simulate('click'); + expect(onAboutModalClose).toBeCalled(); + aboutWrapper.unmount(); + }); + + test('sets error on api request failure', async () => { + api.get = jest.fn().mockImplementation(() => { + const err = new Error('404 error'); + err.response = { status: 404, message: 'problem' }; + return Promise.reject(err); + }); + aboutWrapper = mount(); + await aboutWrapper.instance().componentDidMount(); + expect(aboutWrapper.state('error').response.status).toBe(404); + aboutWrapper.unmount(); + }); + + test('API Config endpoint is valid', () => { + expect(API_CONFIG).toBeDefined(); }); }); diff --git a/__tests__/components/HelpDropdown.test.jsx b/__tests__/components/HelpDropdown.test.jsx new file mode 100644 index 0000000000..85a15f0966 --- /dev/null +++ b/__tests__/components/HelpDropdown.test.jsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import HelpDropdown from '../../src/components/HelpDropdown'; + +let questionCircleIcon; +let dropdownWrapper; +let dropdownToggle; +let dropdownItems; +let dropdownItem; + +beforeEach(() => { + dropdownWrapper = mount(); +}); + +afterEach(() => { + dropdownWrapper.unmount(); +}); + +describe('', () => { + test('initially renders without crashing', () => { + expect(dropdownWrapper.length).toBe(1); + expect(dropdownWrapper.state('isOpen')).toEqual(false); + expect(dropdownWrapper.state('showAboutModal')).toEqual(false); + questionCircleIcon = dropdownWrapper.find('QuestionCircleIcon'); + expect(questionCircleIcon.length).toBe(1); + }); + + test('renders two dropdown items', () => { + dropdownWrapper.setState({ isOpen: true }); + dropdownItems = dropdownWrapper.find('DropdownItem'); + expect(dropdownItems.length).toBe(2); + const dropdownTexts = dropdownItems.map(item => item.text()); + expect(dropdownTexts).toEqual(['Help', 'About']); + }); + + test('onToggle sets state.isOpen to opposite', () => { + dropdownWrapper.setState({ isOpen: true }); + dropdownToggle = dropdownWrapper.find('DropdownToggle > DropdownToggle'); + dropdownToggle.simulate('click'); + expect(dropdownWrapper.state('isOpen')).toEqual(false); + }); + + test('about dropdown item sets state.showAboutModal to true', () => { + dropdownWrapper.setState({ isOpen: true }); + dropdownItem = dropdownWrapper.find('DropdownItem a').at(1); + dropdownItem.simulate('click'); + expect(dropdownWrapper.state('showAboutModal')).toEqual(true); + }); + + test('onAboutModalClose sets state.showAboutModal to false', () => { + dropdownWrapper.setState({ showAboutModal: true }); + const aboutModal = dropdownWrapper.find('AboutModal'); + aboutModal.find('AboutModalBoxCloseButton Button').simulate('click'); + expect(dropdownWrapper.state('showAboutModal')).toEqual(false); + }); +}); + diff --git a/src/components/TowerLogo/tower-logo-header-hover.svg b/images/tower-logo-header-hover.svg similarity index 100% rename from src/components/TowerLogo/tower-logo-header-hover.svg rename to images/tower-logo-header-hover.svg diff --git a/src/components/TowerLogo/tower-logo-header.svg b/images/tower-logo-header.svg similarity index 100% rename from src/components/TowerLogo/tower-logo-header.svg rename to images/tower-logo-header.svg diff --git a/images/tower-logo-login.svg b/images/tower-logo-login.svg new file mode 100644 index 0000000000..cde71349a3 --- /dev/null +++ b/images/tower-logo-login.svg @@ -0,0 +1,25 @@ + diff --git a/images/tower-logo-white.svg b/images/tower-logo-white.svg new file mode 100644 index 0000000000..4b0b17ddea --- /dev/null +++ b/images/tower-logo-white.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx index 98adc2d5c4..62edb76d3a 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -24,7 +24,7 @@ import { global_breakpoint_md as breakpointMd } from '@patternfly/react-tokens'; import api from './api'; import { API_LOGOUT } from './endpoints'; -// import About from './components/About'; +import HelpDropdown from './components/HelpDropdown'; import LogoutButton from './components/LogoutButton'; import TowerLogo from './components/TowerLogo'; import ConditionalRedirect from './components/ConditionalRedirect'; @@ -92,6 +92,9 @@ class App extends React.Component { const PageToolbar = ( + + + this.onDevLogout()} /> diff --git a/src/app.scss b/src/app.scss index 15cc031b34..3fa493cdf8 100644 --- a/src/app.scss +++ b/src/app.scss @@ -25,21 +25,21 @@ .pf-c-nav { overflow-y: auto; - + .pf-c-nav__section { --pf-c-nav__section--MarginTop: 8px; } - + .pf-c-nav__section + .pf-c-nav__section { --pf-c-nav__section--MarginTop: 8px; } - + .pf-c-nav__simple-list .pf-c-nav__link { --pf-c-nav__simple-list-link--PaddingLeft: 24px; --pf-c-nav__simple-list-link--PaddingBottom: 6px; --pf-c-nav__simple-list-link--PaddingTop: 6px; } - + .pf-c-nav__section-title { --pf-c-nav__section-title--PaddingLeft: 24px; } @@ -110,3 +110,11 @@ .pf-c-data-list__cell span { margin-right: 18px; } + +// +// about modal overrides +// +.pf-c-backdrop .pf-c-about-modal-box { + --pf-c-about-modal-box--MaxHeight: 40rem; + --pf-c-about-modal-box--MaxWidth: 63rem; +} diff --git a/src/components/About.jsx b/src/components/About.jsx index d6d5c72099..861e15165d 100644 --- a/src/components/About.jsx +++ b/src/components/About.jsx @@ -1,9 +1,105 @@ import React from 'react'; +import { + AboutModal, + TextContent, + TextList, + TextListItem } from '@patternfly/react-core'; -const About = () => ( -
-

About

-
-); +import heroImg from '@patternfly/patternfly-next/assets/images/pfbg_992.jpg'; +import brandImg from '../../images/tower-logo-white.svg'; +import logoImg from '../../images/tower-logo-login.svg'; + +import api from '../api'; +import { API_CONFIG } from '../endpoints'; + +class About extends React.Component { + unmounting = false; + + constructor (props) { + super(props); + + this.state = { + config: {}, + error: false + }; + } + + async componentDidMount () { + try { + const { data } = await api.get(API_CONFIG); + this.safeSetState({ config: data }); + } catch (error) { + this.safeSetState({ error }); + } + } + + componentWillUnmount () { + this.unmounting = true; + } + + safeSetState = obj => !this.unmounting && this.setState(obj); + + createSpeechBubble = (version) => { + let text = `Tower ${version}`; + let top = ''; + let bottom = ''; + + for (let i = 0; i < text.length; i++) { + top += '_'; + bottom += '-'; + } + + top = ` __${top}__ \n`; + text = `< ${text} >\n`; + bottom = ` --${bottom}-- `; + + return top + text + bottom; + } + + handleModalToggle = () => { + const { onAboutModalClose } = this.props; + onAboutModalClose(); + }; + + render () { + const { isOpen } = this.props; + const { config = {}, error } = this.state; + const { ansible_version = 'loading', version = 'loading' } = config; + + return ( + +
+          { this.createSpeechBubble(version) }
+          {`
+          \\
+           \\  ^__^
+              (oo)\\_______
+              (__)      A )\\
+                  ||----w |
+                  ||     ||
+                    `}
+        
+ + + + Ansible Version + { ansible_version } + + + { error ?
error
: ''} +
+ ); + } +} export default About; diff --git a/src/components/HelpDropdown.jsx b/src/components/HelpDropdown.jsx new file mode 100644 index 0000000000..9107dee0b4 --- /dev/null +++ b/src/components/HelpDropdown.jsx @@ -0,0 +1,61 @@ +import React, { Component, Fragment } from 'react'; +import { + Dropdown, + DropdownItem, + DropdownToggle, + DropdownPosition, +} from '@patternfly/react-core'; +import { QuestionCircleIcon } from '@patternfly/react-icons'; +import AboutModal from './About'; + +class HelpDropdown extends Component { + state = { + isOpen: false, + showAboutModal: false + }; + + render () { + const { isOpen, showAboutModal } = this.state; + const dropdownItems = [ + + Help + , + this.setState({ showAboutModal: true })} + key="about" + > + About + , + ]; + + return ( + + this.setState({ isOpen: !isOpen })} + toggle={( + this.setState({ isOpen: isToggleOpen })}> + + + )} + isOpen={isOpen} + dropdownItems={dropdownItems} + position={DropdownPosition.right} + /> + {showAboutModal + ? ( + this.setState({ showAboutModal: !showAboutModal })} + /> + ) + : null } + + ); + } +} + +export default HelpDropdown; diff --git a/src/components/TowerLogo/TowerLogo.jsx b/src/components/TowerLogo/TowerLogo.jsx index 113e405453..09bdd1013c 100644 --- a/src/components/TowerLogo/TowerLogo.jsx +++ b/src/components/TowerLogo/TowerLogo.jsx @@ -2,8 +2,8 @@ import React, { Component } from 'react'; import { withRouter } from 'react-router-dom'; import { Brand } from '@patternfly/react-core'; -import TowerLogoHeader from './tower-logo-header.svg'; -import TowerLogoHeaderHover from './tower-logo-header-hover.svg'; +import TowerLogoHeader from '../../../images/tower-logo-header.svg'; +import TowerLogoHeaderHover from '../../../images/tower-logo-header-hover.svg'; class TowerLogo extends Component { constructor (props) { diff --git a/webpack.config.js b/webpack.config.js index 2f86ad4340..f87f9b53f3 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -28,7 +28,7 @@ module.exports = { options: { name: '[name].[ext]', outputPath: 'assets/fonts/', - publicPatH: '../', + publicPath: 'assets/fonts', includePaths: [ 'node_modules/@patternfly/patternfly-next/assets/fonts', ] @@ -42,7 +42,7 @@ module.exports = { options: { name: '[name].[ext]', outputPath: 'assets/images/', - publicPatH: '../', + publicPath: 'assets/images', includePaths: [ 'node_modules/@patternfly/patternfly-next/assets/images', ]