Merge pull request #162 from AlexSCorey/128-UsePFTabs

Add PF's Tabs to Orgs Details page
This commit is contained in:
Alex Corey
2019-04-22 09:26:12 -04:00
committed by GitHub
9 changed files with 208 additions and 209 deletions

View File

@@ -0,0 +1,62 @@
import React from 'react';
import { mount, shallow } from 'enzyme';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import RoutedTabs, { _RoutedTabs } from '../../src/components/Tabs/RoutedTabs';
let wrapper;
let history;
const tabs = [
{ name: 'Details', link: '/organizations/19/details', id: 1 },
{ name: 'Access', link: '/organizations/19/access', id: 2 },
{ name: 'Teams', link: '/organizations/19/teams', id: 3 },
{ name: 'Notification', link: '/organizations/19/notification', id: 4 }
];
describe('<RoutedTabs />', () => {
beforeEach(() => {
history = createMemoryHistory({
initialEntries: ['/organizations/19/teams'],
});
});
test('RoutedTabs renders successfully', () => {
wrapper = shallow(
<_RoutedTabs
tabsArray={tabs}
history={history}
/>
);
expect(wrapper.find('Tab')).toHaveLength(4);
});
test('Given a URL the correct tab is active', async () => {
wrapper = mount(
<Router history={history}>
<RoutedTabs
tabsArray={tabs}
/>
</Router>
);
expect(history.location.pathname).toEqual('/organizations/19/teams');
expect(wrapper.find('Tabs').prop('activeKey')).toBe(3);
});
test('should update history when new tab selected', async () => {
wrapper = mount(
<Router history={history}>
<RoutedTabs
tabsArray={tabs}
/>
</Router>
);
wrapper.find('Tabs').prop('onSelect')({}, 2);
wrapper.update();
expect(history.location.pathname).toEqual('/organizations/19/access');
expect(wrapper.find('Tabs').prop('activeKey')).toBe(2);
});
});

View File

@@ -1,5 +1,6 @@
import React from 'react';
import { mount } from 'enzyme';
import { MemoryRouter } from 'react-router-dom';
import { I18nProvider } from '@lingui/react';
import Organization from '../../../../../src/pages/Organizations/screens/Organization/Organization';

53
package-lock.json generated
View File

@@ -2215,7 +2215,7 @@
},
"ansi-colors": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz",
"resolved": "http://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz",
"integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==",
"requires": {
"ansi-wrap": "^0.1.0"
@@ -3234,12 +3234,12 @@
},
"babel-plugin-syntax-class-properties": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz",
"resolved": "http://registry.npmjs.org/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz",
"integrity": "sha1-1+sjt5oxf4VDlixQW4J8fWysJ94="
},
"babel-plugin-syntax-flow": {
"version": "6.18.0",
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-flow/-/babel-plugin-syntax-flow-6.18.0.tgz",
"resolved": "http://registry.npmjs.org/babel-plugin-syntax-flow/-/babel-plugin-syntax-flow-6.18.0.tgz",
"integrity": "sha1-TDqyCiryaqIM0lmVw5jE63AxDI0="
},
"babel-plugin-syntax-jsx": {
@@ -5414,7 +5414,7 @@
},
"readable-stream": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
"resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"requires": {
"core-util-is": "~1.0.0",
@@ -6625,7 +6625,7 @@
},
"readable-stream": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
"resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"requires": {
"core-util-is": "~1.0.0",
@@ -7012,7 +7012,8 @@
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"aproba": {
"version": "1.2.0",
@@ -7033,12 +7034,14 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -7053,17 +7056,20 @@
"code-point-at": {
"version": "1.1.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"core-util-is": {
"version": "1.0.2",
@@ -7180,7 +7186,8 @@
"inherits": {
"version": "2.0.3",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"ini": {
"version": "1.3.5",
@@ -7192,6 +7199,7 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@@ -7206,6 +7214,7 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@@ -7213,12 +7222,14 @@
"minimist": {
"version": "0.0.8",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"minipass": {
"version": "2.3.5",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@@ -7237,6 +7248,7 @@
"version": "0.5.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
@@ -7317,7 +7329,8 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"object-assign": {
"version": "4.1.1",
@@ -7329,6 +7342,7 @@
"version": "1.4.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@@ -7414,7 +7428,8 @@
"safe-buffer": {
"version": "5.1.2",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"safer-buffer": {
"version": "2.1.2",
@@ -7450,6 +7465,7 @@
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@@ -7469,6 +7485,7 @@
"version": "3.0.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@@ -7512,12 +7529,14 @@
"wrappy": {
"version": "1.0.2",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"yallist": {
"version": "3.0.3",
"bundled": true,
"dev": true
"dev": true,
"optional": true
}
}
},
@@ -10583,7 +10602,7 @@
},
"kind-of": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz",
"resolved": "http://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz",
"integrity": "sha1-FAo9LUGjbS78+pN3tiwk+ElaXEQ="
},
"kleur": {

View File

@@ -283,6 +283,14 @@
margin-bottom: 10px;
}
.OrgsTab-closeButton {
color: black;
float: right;
position: relative;
top: -25px;
margin: 0 10px;
right: 10px;
}
.awx-c-form-action-group {
float: right;
display: block;

View File

@@ -0,0 +1,56 @@
import React from 'react';
import { shape, string, number, arrayOf } from 'prop-types';
import { Tab, Tabs } from '@patternfly/react-core';
import { withRouter } from 'react-router-dom';
function RoutedTabs (props) {
const { history, tabsArray } = props;
const getActiveTabId = () => {
const match = tabsArray.find(tab => tab.link === history.location.pathname);
if (match) {
return match.id;
}
return 0;
};
function handleTabSelect (event, eventKey) {
const match = tabsArray.find(tab => tab.id === eventKey);
if (match) {
history.push(match.link);
}
}
return (
<Tabs
activeKey={getActiveTabId()}
onSelect={handleTabSelect}
>
{tabsArray.map(tab => (
<Tab
className={`${tab.name}`}
aria-label={`${tab.name}`}
eventKey={tab.id}
key={tab.id}
link={tab.link}
title={tab.name}
/>
))}
</Tabs>
);
}
RoutedTabs.propTypes = {
history: shape({
location: shape({
pathname: string.isRequired
}).isRequired,
}).isRequired,
tabsArray: arrayOf(shape({
id: number.isRequired,
link: string.isRequired,
name: string.isRequired,
})).isRequired,
};
export { RoutedTabs as _RoutedTabs };
export default withRouter(RoutedTabs);

View File

@@ -1,33 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { NavLink } from 'react-router-dom';
import './tabs.scss';
const Tab = ({ children, link, replace }) => (
<li className="pf-c-tabs__item">
<NavLink
to={link}
replace={replace}
className="pf-c-tabs__button"
activeClassName="pf-m-current"
>
{children}
</NavLink>
</li>
);
Tab.propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
]).isRequired,
link: PropTypes.string,
replace: PropTypes.bool,
};
Tab.defaultProps = {
link: null,
replace: false,
};
export default Tab;

View File

@@ -1,53 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import { Button, Tooltip } from '@patternfly/react-core';
import { TimesIcon } from '@patternfly/react-icons';
import './tabs.scss';
const Tabs = ({ children, labelText, closeButton }) => (
<div
aria-label={labelText}
className="pf-c-tabs"
>
<ul className="pf-c-tabs__list">
{children}
</ul>
{closeButton
&& (
<Tooltip
content={closeButton.text}
position="top"
>
<Link to={closeButton.link}>
<Button
variant="plain"
aria-label={closeButton.text}
>
<TimesIcon />
</Button>
</Link>
</Tooltip>
)
}
</div>
);
Tabs.propTypes = {
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node
]).isRequired,
labelText: PropTypes.string,
closeButton: PropTypes.shape({
text: PropTypes.string,
link: PropTypes.string,
}),
};
Tabs.defaultProps = {
labelText: null,
closeButton: null,
};
export default Tabs;

View File

@@ -1,71 +0,0 @@
.pf-c-card__header {
--pf-c-card__header--PaddingBottom: 0;
--pf-c-card__header--PaddingX: 0;
--pf-c-card__header--PaddingRight: 0;
--pf-c-card__header--PaddingLeft: 0;
--pf-c-card__header--PaddingTop: 0;
}
.pf-c-tabs {
--pf-global--link--Color: #484848;
--pf-global--link--Color--hover: #484848;
--pf-global--link--TextDecoration--hover: none;
align-items: center;
flex-direction: row;
justify-content: space-between;
&:before {
border-bottom: 1px solid var(--pf-c-tabs__item--BorderColor);
border-top: 1px solid var(--pf-c-tabs__item--BorderColor);
bottom: 0;
content: " ";
left: 0;
position: absolute;
right: 0;
top: 0;
}
.pf-c-tabs__button {
--pf-c-tabs__button--PaddingLeft: 20px;
--pf-c-tabs__button--PaddingRight: 20px;
display: block;
font-weight: 700;
&:after {
content: '';
bottom: 0;
left: 0;
position: absolute;
right: 0;
top: 0;
}
}
.pf-c-tabs__item:first-child .pf-c-tabs__button:before {
border-left: 0;
}
.pf-c-tabs__item:not(.pf-m-current):hover
.pf-c-tabs__button::after {
border-top: none;
}
.pf-c-tabs__item:hover
.pf-c-tabs__button:not(.pf-m-current)::after {
border-bottom: 3px solid var(--pf-global--Color--dark-200);
border-top: none;
}
.pf-c-tabs__button.pf-m-current {
color: var(--pf-c-tabs__item--m-current--Color);
}
.pf-c-tabs__button.pf-m-current::after {
content: '';
border-bottom: 3px solid var(--pf-c-tabs__item--m-current--Color);
border-top: none;
margin-left: 1px;
}
}

View File

@@ -5,25 +5,25 @@ import {
Switch,
Route,
withRouter,
Redirect
Redirect,
Link
} from 'react-router-dom';
import {
Card,
CardHeader,
PageSection
PageSection,
} from '@patternfly/react-core';
import {
TimesIcon
} from '@patternfly/react-icons';
import { withNetwork } from '../../../../contexts/Network';
import Tabs from '../../../../components/Tabs/Tabs';
import Tab from '../../../../components/Tabs/Tab';
import NotifyAndRedirect from '../../../../components/NotifyAndRedirect';
import OrganizationAccess from './OrganizationAccess';
import OrganizationDetail from './OrganizationDetail';
import OrganizationEdit from './OrganizationEdit';
import OrganizationNotifications from './OrganizationNotifications';
import OrganizationTeams from './OrganizationTeams';
import RoutedTabs from '../../../../components/Tabs/RoutedTabs';
class Organization extends Component {
constructor (props) {
@@ -79,35 +79,45 @@ class Organization extends Component {
loading
} = this.state;
const tabElements = [
{ name: i18nMark('Details'), link: `${match.url}/details` },
{ name: i18nMark('Access'), link: `${match.url}/access` },
{ name: i18nMark('Teams'), link: `${match.url}/teams` },
{ name: i18nMark('Notifications'), link: `${match.url}/notifications` },
];
const tabsPaddingOverride = {
padding: '0'
};
let cardHeader = (
<CardHeader>
<I18n>
{({ i18n }) => (
<Tabs
labelText={i18n._(t`Organization detail tabs`)}
closeButton={{ link: '/organizations', text: i18nMark('Close') }}
>
{tabElements.map(tabElement => (
<Tab
key={tabElement.name}
link={tabElement.link}
replace
>
{tabElement.name}
</Tab>
))}
</Tabs>
)}
</I18n>
</CardHeader>
);
loading ? ''
: (
<CardHeader
style={tabsPaddingOverride}
>
<I18n>
{({ i18n }) => (
<React.Fragment>
<RoutedTabs
match={match}
history={history}
labeltext={i18n._(t`Organization detail tabs`)}
tabsArray={[
{ name: i18nMark('Details'), link: `${match.url}/details`, id: 0 },
{ name: i18nMark('Access'), link: `${match.url}/access`, id: 1 },
{ name: i18nMark('Teams'), link: `${match.url}/teams`, id: 2 },
{ name: i18nMark('Notifications'), link: `${match.url}/notifications`, id: 3 },
]}
/>
<Link
aria-label="Close"
title="Close"
to="/organizations"
>
<TimesIcon className="OrgsTab-closeButton" />
</Link>
</React.Fragment>
)}
</I18n>
</CardHeader>
));
if (!match) {
cardHeader = null;
}
if (location.pathname.endsWith('edit')) {
cardHeader = null;
@@ -185,5 +195,5 @@ class Organization extends Component {
);
}
}
export default withNetwork(withRouter(Organization));
export { Organization as _Organization };