1
0
mirror of https://github.com/ansible/awx.git synced 2024-11-01 08:21:15 +03:00

Add help dropdown and about modal

This commit is contained in:
Marliana Lara 2018-11-16 00:13:25 -05:00
parent 7fdf27eece
commit 6d315568d2
No known key found for this signature in database
GPG Key ID: 38C73B40DFA809EE
12 changed files with 297 additions and 20 deletions

View File

@ -1,15 +1,41 @@
import React from 'react'; import React from 'react';
import { mount } from 'enzyme'; import { mount } from 'enzyme';
import api from '../../src/api';
import { API_CONFIG } from '../../src/endpoints';
import About from '../../src/components/About'; import About from '../../src/components/About';
let aboutWrapper;
let headerElem;
describe('<About />', () => { describe('<About />', () => {
let aboutWrapper;
let closeButton;
test('initially renders without crashing', () => { test('initially renders without crashing', () => {
aboutWrapper = mount(<About />); aboutWrapper = mount(<About isOpen />);
headerElem = aboutWrapper.find('h2');
expect(aboutWrapper.length).toBe(1); expect(aboutWrapper.length).toBe(1);
expect(headerElem.length).toBe(1); aboutWrapper.unmount();
});
test('close button calls onAboutModalClose', () => {
const onAboutModalClose = jest.fn();
aboutWrapper = mount(<About isOpen onAboutModalClose={onAboutModalClose} />);
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(<About isOpen />);
await aboutWrapper.instance().componentDidMount();
expect(aboutWrapper.state('error').response.status).toBe(404);
aboutWrapper.unmount();
});
test('API Config endpoint is valid', () => {
expect(API_CONFIG).toBeDefined();
}); });
}); });

View File

@ -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(<HelpDropdown />);
});
afterEach(() => {
dropdownWrapper.unmount();
});
describe('<HelpDropdown />', () => {
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);
});
});

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,25 @@
<svg id="logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 281.5 84">
<title>Logotype_RH_AnsibleTower_RGB_RedGray (1)</title>
<path d="M114.27,22.69l-3.32-6.86h-2.27v6.86h-5.55V2.34h9.1c4.77,0,7.93,1.8,7.93,6.63a6.06,6.06,0,0,1-3.63,6l4,7.76ZM112.09,6.94h-3.4v4.45h3.38c1.83,0,2.56-.81,2.56-2.27s-.75-2.18-2.56-2.18Z" fill="#4c4c4c"/>
<path d="M124.83,22.69V2.34h15.58V7.07h-10v2.7h6.06v4.68H130.4v3.49h10.2v4.74Z" fill="#4c4c4c"/>
<path d="M151.68,22.69h-6.6V2.34h7.12c6.4,0,10.47,2.42,10.47,10.05S158.89,22.69,151.68,22.69Zm.5-15.53h-1.36v10.7H152c3.51,0,4.85-1.33,4.85-5.38,0-3.74-1.21-5.31-4.7-5.31Z" fill="#4c4c4c"/>
<path d="M189.51,22.69v-8h-6.35v8h-5.69V2.34h5.75V9.75h6.34V2.34h5.75V22.69Z" fill="#4c4c4c"/>
<path d="M213.4,22.69l-1.1-3.63h-6.06l-1.1,3.63h-6.06l7.39-20.35h5.69l7.39,20.35Zm-2.85-9.39c-.73-2.62-1-3.72-1.31-5.09-.27,1.37-.58,2.5-1.31,5.09l-.38,1.33h3.37Z" fill="#4c4c4c"/>
<path d="M231,7.26V22.69h-5.64V7.26h-5.71V2.34h17V7.26Z" fill="#4c4c4c"/>
<path d="M241.85,7.45a2.62,2.62,0,1,1,2.63-2.62A2.57,2.57,0,0,1,242,7.45Zm0-4.85a2.2,2.2,0,1,0,2.24,2.23A2.16,2.16,0,0,0,242,2.63h-.14ZM242.45,5,243,6.24h-.57l-.56-1.1h-.56v1.1h-.48V3.34h1.21a.84.84,0,0,1,.93.86.81.81,0,0,1-.58.85Zm-.34-1.21h-.75v.81h.75c.27,0,.46-.12.46-.41a.4.4,0,0,0-.39-.41h-.07Z" fill="#4c4c4c"/>
<path d="M122.81,59l-2.62-7.76H107.58L104.92,59h-3.49l10.64-30.51h3.76L126.42,59Zm-7-20.88c-.7-2-1.57-4.75-1.88-6.06-.3,1.21-1.21,4-1.91,6.06l-3.36,9.94h10.5Z" fill="#4c4c4c"/>
<path d="M151.49,59,138.17,38.14c-.65-1-1.79-3.05-2.18-3.84V59h-3.33V28.51H136L149.19,49.8c.65,1,1.79,3.05,2.18,3.84V28.51h3.27V59Z" fill="#4c4c4c"/>
<path d="M171.33,59.51a14.54,14.54,0,0,1-10.24-4.29l2.27-2.42a11.59,11.59,0,0,0,8.12,3.63c4.06,0,6.59-2,6.59-5.23,0-2.83-1.7-4.45-7.27-6.46-6.59-2.35-8.81-4.49-8.81-8.89,0-4.85,3.84-7.8,9.55-7.8a13.66,13.66,0,0,1,9.29,3.27L178.65,34a10.62,10.62,0,0,0-7.27-2.83c-4.19,0-5.94,2.1-5.94,4.49s1.14,4,7.27,6.15c6.76,2.42,8.85,4.71,8.85,9.24C181.45,55.8,177.7,59.51,171.33,59.51Z" fill="#4c4c4c"/>
<path d="M188.74,59V28.51h3.4V59Z" fill="#4c4c4c"/>
<path d="M211.33,59h-10.9V28.51h11.16c4.85,0,8.29,2.42,8.29,7.63a6.18,6.18,0,0,1-4.4,6.24A7.27,7.27,0,0,1,221.31,50C221.26,56,217.74,59,211.33,59Zm.06-27.42h-7.63v9.51h7.36c3.79,0,5.28-2.18,5.28-4.85S214.82,31.6,211.39,31.6Zm0,12.56h-7.58V55.94h7.76c4.58,0,6.32-2.27,6.32-5.75C217.86,46.34,215.34,44.16,211.39,44.16Z" fill="#4c4c4c"/>
<path d="M227.68,59V28.51h3.4V55.94h15.08V59Z" fill="#4c4c4c"/>
<path d="M252.76,59V28.51H271.2V31.6h-15V41.2h8.72v3.13h-8.72V56H272v3.1Z" fill="#4c4c4c"/>
<path d="M277.37,33.67A2.62,2.62,0,1,1,280,31a2.57,2.57,0,0,1-2.46,2.68Zm0-4.85a2.2,2.2,0,1,0,2.2,2.2v0a2.16,2.16,0,0,0-2.17-2.14h0Zm.59,2.42.59,1.21H278l-.56-1.1h-.56v1.1h-.48v-2.9h1.21a.84.84,0,0,1,.93.86.81.81,0,0,1-.55.85ZM277.62,30h-.75v.81h.75c.27,0,.46-.12.46-.41a.4.4,0,0,0-.4-.4h-.07Z" fill="#4c4c4c"/>
<path d="M108.91,67.82v13h-2.29v-13h-4.45V65.57h11.19v2.25Z" fill="#4c4c4c"/>
<path d="M117.91,81.07c-2.88,0-5-2.42-5-6s2.23-6,5.1-6,5.1,2.34,5.1,5.91C123.09,78.86,120.87,81.07,117.91,81.07Zm0-9.79c-1.7,0-2.75,1.5-2.75,3.78,0,2.51,1.21,3.89,2.86,3.89s2.81-1.72,2.81-3.82c0-2.35-1.13-3.85-2.93-3.85Z" fill="#4c4c4c"/>
<path d="M135.91,80.83H134l-1.55-5.78c-.24-.87-.48-1.9-.59-2.42-.11.55-.35,1.59-.59,2.42l-1.53,5.75h-1.91l-3.14-11.45h2.23l1.36,5.56c.22.87.46,2,.57,2.51.13-.57.39-1.61.63-2.51L131,69.36h1.8l1.55,5.58c.24.9.48,1.91.61,2.48.13-.57.35-1.64.57-2.51l1.36-5.56h2.23Z" fill="#4c4c4c"/>
<path d="M150.45,75.91h-7.57c.24,2.2,1.48,3.14,2.88,3.14a4,4,0,0,0,2.48-.92l1.37,1.44a5.32,5.32,0,0,1-3.95,1.5c-2.68,0-5-2.16-5-6s2-6,5-6c3.25,0,4.85,2.64,4.85,5.74C150.51,75.29,150.47,75.67,150.45,75.91Zm-4.91-4.74c-1.5,0-2.42,1-2.62,2.88h5.32C148.14,72.48,147.4,71.18,145.54,71.18Z" fill="#4c4c4c"/>
<path d="M159.52,71.76a3.36,3.36,0,0,0-1.72-.41c-1.42,0-2.24,1-2.24,2.94v6.54h-2.31V69.38h2.24v1.09a3.17,3.17,0,0,1,2.62-1.33,3.29,3.29,0,0,1,1.94.48Z" fill="#4c4c4c"/>
<circle cx="41.66" cy="41.7" r="40.94" fill="#c00"/>
<path d="M61.19,57.95,44.88,18.73A2.63,2.63,0,0,0,42.36,17a2.71,2.71,0,0,0-2.59,1.73l-17.9,43H28L35.08,44,56.21,61.07a3.43,3.43,0,0,0,2.27,1,2.92,2.92,0,0,0,3-2.85s0,0,0-.07A3.88,3.88,0,0,0,61.19,57.95ZM42.35,25.78,53,51.93,37,39.32Z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -0,0 +1 @@
<svg id="logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300"><title>Ansible-Mark-RGB</title><path d="M202.79511,195.28745L161.20435,95.1969a6.712,6.712,0,0,0-6.44248-4.41387,6.93571,6.93571,0,0,0-6.62067,4.41387L102.49294,204.9835h15.61529l18.07045-45.26508,53.92568,43.5655c2.169,1.75373,3.73371,2.54688,5.76785,2.54688a7.45654,7.45654,0,0,0,7.63512-7.46245A9.884,9.884,0,0,0,202.79511,195.28745Zm-48.03324-82.10658,27.03826,66.73385-40.8407-32.171Z" fill="#fff"/><path d="M153.00038,259.70772a106.78925,106.78925,0,1,1,106.78925-106.7885A106.91,106.91,0,0,1,153.00038,259.70772Zm0-208.967a102.1777,102.1777,0,1,0,102.1777,102.17845A102.29417,102.29417,0,0,0,153.00038,50.74077Z" fill="#fff"/><path d="M154.79931,112.89294l27.63843,68.20864-41.74528-32.8803Zm49.09554,83.922L161.38455,94.51424a6.85642,6.85642,0,0,0-6.58524-4.51358,7.08778,7.08778,0,0,0-6.76578,4.51358L101.37686,206.72648h15.96125l18.46744-46.268,55.11772,44.52993c2.21855,1.79319,3.81589,2.60411,5.89673,2.60411a7.62418,7.62418,0,0,0,7.80314-7.62872,10.11356,10.11356,0,0,0-.72829-3.1488" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -24,7 +24,7 @@ import { global_breakpoint_md as breakpointMd } from '@patternfly/react-tokens';
import api from './api'; import api from './api';
import { API_LOGOUT } from './endpoints'; import { API_LOGOUT } from './endpoints';
// import About from './components/About'; import HelpDropdown from './components/HelpDropdown';
import LogoutButton from './components/LogoutButton'; import LogoutButton from './components/LogoutButton';
import TowerLogo from './components/TowerLogo'; import TowerLogo from './components/TowerLogo';
import ConditionalRedirect from './components/ConditionalRedirect'; import ConditionalRedirect from './components/ConditionalRedirect';
@ -92,6 +92,9 @@ class App extends React.Component {
const PageToolbar = ( const PageToolbar = (
<Toolbar> <Toolbar>
<ToolbarGroup> <ToolbarGroup>
<ToolbarItem>
<HelpDropdown />
</ToolbarItem>
<ToolbarItem> <ToolbarItem>
<LogoutButton onDevLogout={() => this.onDevLogout()} /> <LogoutButton onDevLogout={() => this.onDevLogout()} />
</ToolbarItem> </ToolbarItem>

View File

@ -110,3 +110,11 @@
.pf-c-data-list__cell span { .pf-c-data-list__cell span {
margin-right: 18px; 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;
}

View File

@ -1,9 +1,105 @@
import React from 'react'; import React from 'react';
import {
AboutModal,
TextContent,
TextList,
TextListItem } from '@patternfly/react-core';
const About = () => ( import heroImg from '@patternfly/patternfly-next/assets/images/pfbg_992.jpg';
<div> import brandImg from '../../images/tower-logo-white.svg';
<h2>About</h2> import logoImg from '../../images/tower-logo-login.svg';
</div>
); 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 (
<AboutModal
isOpen={isOpen}
onClose={this.handleModalToggle}
productName="Ansible Tower"
trademark="Copyright 2018 Red Hat, Inc."
brandImageSrc={brandImg}
brandImageAlt="Brand Image"
logoImageSrc={logoImg}
logoImageAlt="AboutModal Logo"
heroImageSrc={heroImg}
>
<pre>
{ this.createSpeechBubble(version) }
{`
\\
\\ ^__^
(oo)\\_______
(__) A )\\
||----w |
|| ||
`}
</pre>
<TextContent>
<TextList component="dl">
<TextListItem component="dt">Ansible Version</TextListItem>
<TextListItem component="dd">{ ansible_version }</TextListItem>
</TextList>
</TextContent>
{ error ? <div>error</div> : ''}
</AboutModal>
);
}
}
export default About; export default About;

View File

@ -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 = [
<DropdownItem
href="https://docs.ansible.com/ansible-tower/latest/html/userguide/index.html"
target="_blank"
key="help"
>
Help
</DropdownItem>,
<DropdownItem
onClick={() => this.setState({ showAboutModal: true })}
key="about"
>
About
</DropdownItem>,
];
return (
<Fragment>
<Dropdown
onSelect={() => this.setState({ isOpen: !isOpen })}
toggle={(
<DropdownToggle onToggle={(isToggleOpen) => this.setState({ isOpen: isToggleOpen })}>
<QuestionCircleIcon />
</DropdownToggle>
)}
isOpen={isOpen}
dropdownItems={dropdownItems}
position={DropdownPosition.right}
/>
{showAboutModal
? (
<AboutModal
isOpen={showAboutModal}
onAboutModalClose={() => this.setState({ showAboutModal: !showAboutModal })}
/>
)
: null }
</Fragment>
);
}
}
export default HelpDropdown;

View File

@ -2,8 +2,8 @@ import React, { Component } from 'react';
import { withRouter } from 'react-router-dom'; import { withRouter } from 'react-router-dom';
import { Brand } from '@patternfly/react-core'; import { Brand } from '@patternfly/react-core';
import TowerLogoHeader from './tower-logo-header.svg'; import TowerLogoHeader from '../../../images/tower-logo-header.svg';
import TowerLogoHeaderHover from './tower-logo-header-hover.svg'; import TowerLogoHeaderHover from '../../../images/tower-logo-header-hover.svg';
class TowerLogo extends Component { class TowerLogo extends Component {
constructor (props) { constructor (props) {

View File

@ -28,7 +28,7 @@ module.exports = {
options: { options: {
name: '[name].[ext]', name: '[name].[ext]',
outputPath: 'assets/fonts/', outputPath: 'assets/fonts/',
publicPatH: '../', publicPath: 'assets/fonts',
includePaths: [ includePaths: [
'node_modules/@patternfly/patternfly-next/assets/fonts', 'node_modules/@patternfly/patternfly-next/assets/fonts',
] ]
@ -42,7 +42,7 @@ module.exports = {
options: { options: {
name: '[name].[ext]', name: '[name].[ext]',
outputPath: 'assets/images/', outputPath: 'assets/images/',
publicPatH: '../', publicPath: 'assets/images',
includePaths: [ includePaths: [
'node_modules/@patternfly/patternfly-next/assets/images', 'node_modules/@patternfly/patternfly-next/assets/images',
] ]