diff --git a/__tests__/components/RoutedTabs.test.jsx b/__tests__/components/RoutedTabs.test.jsx new file mode 100644 index 0000000000..9fcd896ef9 --- /dev/null +++ b/__tests__/components/RoutedTabs.test.jsx @@ -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('', () => { + 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( + + + + ); + + 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( + + + + ); + + wrapper.find('Tabs').prop('onSelect')({}, 2); + wrapper.update(); + + expect(history.location.pathname).toEqual('/organizations/19/access'); + expect(wrapper.find('Tabs').prop('activeKey')).toBe(2); + }); +}); diff --git a/__tests__/pages/Organizations/screens/Organization/Organization.test.jsx b/__tests__/pages/Organizations/screens/Organization/Organization.test.jsx index ac147e2631..a3b59a9e0c 100644 --- a/__tests__/pages/Organizations/screens/Organization/Organization.test.jsx +++ b/__tests__/pages/Organizations/screens/Organization/Organization.test.jsx @@ -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'; diff --git a/package-lock.json b/package-lock.json index d48da6c8e0..ec538cf3fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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": { diff --git a/src/app.scss b/src/app.scss index 20df8aa24b..7f05497dc5 100644 --- a/src/app.scss +++ b/src/app.scss @@ -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; diff --git a/src/components/Tabs/RoutedTabs.jsx b/src/components/Tabs/RoutedTabs.jsx new file mode 100644 index 0000000000..4100e766b6 --- /dev/null +++ b/src/components/Tabs/RoutedTabs.jsx @@ -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 ( + + {tabsArray.map(tab => ( + + ))} + + ); +} +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); diff --git a/src/components/Tabs/Tab.jsx b/src/components/Tabs/Tab.jsx deleted file mode 100644 index 6dd9409391..0000000000 --- a/src/components/Tabs/Tab.jsx +++ /dev/null @@ -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 }) => ( -
  • - - {children} - -
  • -); - -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; diff --git a/src/components/Tabs/Tabs.jsx b/src/components/Tabs/Tabs.jsx deleted file mode 100644 index abea04f8b8..0000000000 --- a/src/components/Tabs/Tabs.jsx +++ /dev/null @@ -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 }) => ( -
    - - {closeButton - && ( - - - - - - ) - } -
    -); - -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; diff --git a/src/components/Tabs/tabs.scss b/src/components/Tabs/tabs.scss deleted file mode 100644 index 651be6acba..0000000000 --- a/src/components/Tabs/tabs.scss +++ /dev/null @@ -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; - } -} - diff --git a/src/pages/Organizations/screens/Organization/Organization.jsx b/src/pages/Organizations/screens/Organization/Organization.jsx index 2ba53ece98..aba494809e 100644 --- a/src/pages/Organizations/screens/Organization/Organization.jsx +++ b/src/pages/Organizations/screens/Organization/Organization.jsx @@ -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 = ( - - - {({ i18n }) => ( - - {tabElements.map(tabElement => ( - - {tabElement.name} - - ))} - - )} - - - ); + loading ? '' + : ( + + + {({ i18n }) => ( + + + + + + + )} + + + )); + 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 };