diff --git a/__tests__/pages/Organizations/Organization.add.test.jsx b/__tests__/pages/Organizations/Organization.add.test.jsx
new file mode 100644
index 0000000000..e45b381a7b
--- /dev/null
+++ b/__tests__/pages/Organizations/Organization.add.test.jsx
@@ -0,0 +1,24 @@
+import React from 'react';
+import { mount } from 'enzyme';
+import { API_ORGANIZATIONS } from '../../../src/endpoints';
+import OrganizationAdd from '../../../src/pages/Organizations/Organization.add';
+
+describe('', () => {
+ let pageWrapper;
+
+ beforeEach(() => {
+ pageWrapper = mount();
+ });
+
+ afterEach(() => {
+ pageWrapper.unmount();
+ });
+
+ test('initially renders without crashing', () => {
+ expect(pageWrapper.length).toBe(1);
+ });
+
+ test('API Organization endpoint is valid', () => {
+ expect(API_ORGANIZATIONS).toBeDefined();
+ });
+});
diff --git a/__tests__/pages/Organizations/Organization.view.test.jsx b/__tests__/pages/Organizations/Organization.view.test.jsx
new file mode 100644
index 0000000000..a6fccea001
--- /dev/null
+++ b/__tests__/pages/Organizations/Organization.view.test.jsx
@@ -0,0 +1,32 @@
+import React from 'react';
+import { MemoryRouter } from 'react-router-dom';
+import { mount } from 'enzyme';
+import { API_ORGANIZATIONS } from '../../../src/endpoints';
+import OrganizationView from '../../../src/pages/Organizations/Organization.view';
+
+describe('', () => {
+ let pageWrapper;
+
+ beforeEach(() => {
+ pageWrapper = mount(
+
+
+
+ );
+ });
+
+ afterEach(() => {
+ pageWrapper.unmount();
+ });
+
+ test('initially renders without crashing', () => {
+ expect(pageWrapper.length).toBe(1);
+ });
+
+ test('API Organization endpoint is valid', () => {
+ expect(API_ORGANIZATIONS).toBeDefined();
+ });
+});
diff --git a/__tests__/pages/Organizations.jsx b/__tests__/pages/Organizations/Organizations.list.test.jsx
similarity index 93%
rename from __tests__/pages/Organizations.jsx
rename to __tests__/pages/Organizations/Organizations.list.test.jsx
index fdbc219cc8..4162bae9f4 100644
--- a/__tests__/pages/Organizations.jsx
+++ b/__tests__/pages/Organizations/Organizations.list.test.jsx
@@ -3,9 +3,9 @@ import { HashRouter } from 'react-router-dom';
import { mount } from 'enzyme';
-import api from '../../src/api';
-import { API_ORGANIZATIONS } from '../../src/endpoints';
-import Organizations from '../../src/pages/Organizations';
+import api from '../../../src/api';
+import { API_ORGANIZATIONS } from '../../../src/endpoints';
+import Organizations from '../../../src/pages/Organizations';
describe('', () => {
let pageWrapper;
diff --git a/src/components/DataListToolbar/DataListToolbar.jsx b/src/components/DataListToolbar/DataListToolbar.jsx
index a3fbb80d92..7707dd5575 100644
--- a/src/components/DataListToolbar/DataListToolbar.jsx
+++ b/src/components/DataListToolbar/DataListToolbar.jsx
@@ -6,8 +6,6 @@ import {
DropdownPosition,
DropdownToggle,
DropdownItem,
- FormGroup,
- KebabToggle,
Level,
LevelItem,
TextInput,
@@ -24,11 +22,14 @@ import {
SortNumericUpIcon,
TrashAltIcon,
} from '@patternfly/react-icons';
+import {
+ Link
+} from 'react-router-dom';
import Tooltip from '../Tooltip';
class DataListToolbar extends React.Component {
- constructor(props) {
+ constructor (props) {
super(props);
const { sortedColumnKey } = this.props;
@@ -72,15 +73,15 @@ class DataListToolbar extends React.Component {
this.setState({ isSearchDropdownOpen: false, searchKey: key });
};
- onActionToggle = isActionDropdownOpen => {
- this.setState({ isActionDropdownOpen });
- };
+ // onActionToggle = isActionDropdownOpen => {
+ // this.setState({ isActionDropdownOpen });
+ // };
- onActionSelect = ({ target }) => {
- this.setState({ isActionDropdownOpen: false });
- };
+ // onActionSelect = () => {
+ // this.setState({ isActionDropdownOpen: false });
+ // };
- render() {
+ render () {
const { up } = DropdownPosition;
const {
columns,
@@ -90,9 +91,10 @@ class DataListToolbar extends React.Component {
onSort,
sortedColumnKey,
sortOrder,
+ addUrl
} = this.props;
const {
- isActionDropdownOpen,
+ // isActionDropdownOpen,
isSearchDropdownOpen,
isSortDropdownOpen,
searchKey,
@@ -107,19 +109,29 @@ class DataListToolbar extends React.Component {
.filter(({ key }) => key === sortedColumnKey);
const sortedColumnName = sortedColumn.name;
const isSortNumeric = sortedColumn.isNumeric;
+ const displayedSortIcon = () => {
+ let icon;
+ if (sortOrder === 'ascending') {
+ icon = isSortNumeric ? () : ();
+ } else {
+ icon = isSortNumeric ? () : ();
+ }
+ return icon;
+ };
return (
-
+
+ id="select-all"
+ />
@@ -132,10 +144,12 @@ class DataListToolbar extends React.Component {
isOpen={isSearchDropdownOpen}
toggle={(
+ onToggle={this.onSearchDropdownToggle}
+ >
{ searchColumnName }
- )}>
+ )}
+ >
{columns.filter(({ key }) => key !== searchKey).map(({ key, name }) => (
{ name }
@@ -146,12 +160,14 @@ class DataListToolbar extends React.Component {
type="search"
aria-label="search text input"
value={searchValue}
- onChange={this.handleSearchInputChange}/>
+ onChange={this.handleSearchInputChange}
+ />
@@ -165,31 +181,28 @@ class DataListToolbar extends React.Component {
isOpen={isSortDropdownOpen}
toggle={(
+ onToggle={this.onSortDropdownToggle}
+ >
{ sortedColumnName }
- )}>
+ )}
+ >
{columns
.filter(({ key, isSortable }) => isSortable && key !== sortedColumnKey)
.map(({ key, name }) => (
-
- { name }
-
- ))}
+
+ { name }
+
+ ))}
@@ -210,12 +223,16 @@ class DataListToolbar extends React.Component {
-
+ {addUrl && (
+
+
+
+ )}
@@ -223,4 +240,4 @@ class DataListToolbar extends React.Component {
}
}
-export default DataListToolbar;
\ No newline at end of file
+export default DataListToolbar;
diff --git a/src/pages/Organizations/components/OrganizationBreadcrumb.jsx b/src/pages/Organizations/components/OrganizationBreadcrumb.jsx
new file mode 100644
index 0000000000..bbac1b3d5c
--- /dev/null
+++ b/src/pages/Organizations/components/OrganizationBreadcrumb.jsx
@@ -0,0 +1,78 @@
+import React, { Fragment } from 'react';
+import {
+ PageSection,
+ PageSectionVariants,
+ Title,
+} from '@patternfly/react-core';
+import {
+ Link
+} from 'react-router-dom';
+
+import getTabName from '../utils';
+
+const OrganizationBreadcrumb = ({ parentObj, organization, currentTab, location }) => {
+ const { light } = PageSectionVariants;
+ let breadcrumb = '';
+ if (parentObj !== 'loading') {
+ const generateCrumb = (noLastLink = false) => (
+
+ {parentObj
+ .map(({ url, name }, index) => {
+ let elem;
+ if (noLastLink && parentObj.length - 1 === index) {
+ elem = ({name});
+ } else {
+ elem = (
+
+ {name}
+
+ );
+ }
+ return elem;
+ })
+ .reduce((prev, curr) => [prev, ' > ', curr])}
+
+ );
+
+ if (currentTab && currentTab !== 'details') {
+ breadcrumb = (
+
+ {generateCrumb()}
+ {' > '}
+ {getTabName(currentTab)}
+
+ );
+ } else if (location.pathname.indexOf('edit') > -1) {
+ breadcrumb = (
+
+ {generateCrumb()}
+ {' > edit'}
+
+ );
+ } else if (location.pathname.indexOf('add') > -1) {
+ breadcrumb = (
+
+ {generateCrumb()}
+ {' > add'}
+
+ );
+ } else {
+ breadcrumb = (
+
+ {generateCrumb(true)}
+
+ );
+ }
+ }
+
+ return (
+
+ {breadcrumb}
+
+ );
+};
+
+export default OrganizationBreadcrumb;
diff --git a/src/pages/Organizations/components/OrganizationDetail.jsx b/src/pages/Organizations/components/OrganizationDetail.jsx
new file mode 100644
index 0000000000..4c1ea3bb31
--- /dev/null
+++ b/src/pages/Organizations/components/OrganizationDetail.jsx
@@ -0,0 +1,140 @@
+import React, { Fragment } from 'react';
+import {
+ Card,
+ CardHeader,
+ CardBody,
+ PageSection,
+ PageSectionVariants,
+ ToolbarGroup,
+ ToolbarItem,
+ ToolbarSection,
+} from '@patternfly/react-core';
+import {
+ Switch,
+ Link,
+ Route
+} from 'react-router-dom';
+
+import getTabName from '../utils';
+
+import '../tabs.scss';
+
+const DetailTab = ({ location, match, tab, currentTab, children, breadcrumb }) => {
+ const tabClasses = () => {
+ let classes = 'at-c-tabs__tab';
+ if (tab === currentTab) {
+ classes += ' at-m-selected';
+ }
+
+ return classes;
+ };
+
+ const updateTab = () => {
+ const params = new URLSearchParams(location.search);
+ if (params.get('tab') !== undefined) {
+ params.set('tab', tab);
+ } else {
+ params.append('tab', tab);
+ }
+
+ return `?${params.toString()}`;
+ };
+
+ return (
+
+
+ {children}
+
+
+ );
+};
+
+const OrganizationDetail = ({
+ location,
+ match,
+ parentBreadcrumbObj,
+ organization,
+ params,
+ currentTab
+}) => {
+ // TODO: set objectName by param or through grabbing org detail get from api
+ const { medium } = PageSectionVariants;
+
+ const deleteResourceView = () => (
+
+ {`deleting ${currentTab} association with orgs `}
+
+ {`confirm removal of ${currentTab}/cancel and go back to ${currentTab} view.`}
+
+
+ );
+
+ const addResourceView = () => (
+
+ {`adding ${currentTab} `}
+
+ {`save/cancel and go back to ${currentTab} view`}
+
+
+ );
+
+ const resourceView = () => (
+
+ {`${currentTab} detail view `}
+
+ {`add ${currentTab}`}
+
+ {' '}
+
+ {`delete ${currentTab}`}
+
+
+ );
+
+ const detailTabs = (tabs) => (
+
+
+ {tabs.map(tab => (
+
+ {getTabName(tab)}
+
+ ))}
+
+
+ );
+
+ return (
+
+
+
+ {detailTabs(['details', 'users', 'teams', 'admins', 'notifications'])}
+
+
+ {(currentTab && currentTab !== 'details') ? (
+
+ deleteResourceView()} />
+ addResourceView()} />
+ resourceView()} />
+
+ ) : (
+
+ {'detail view '}
+
+ {'edit'}
+
+
+ )}
+
+
+
+ );
+};
+
+export default OrganizationDetail;
diff --git a/src/pages/Organizations/components/OrganizationEdit.jsx b/src/pages/Organizations/components/OrganizationEdit.jsx
new file mode 100644
index 0000000000..04d49ff6d5
--- /dev/null
+++ b/src/pages/Organizations/components/OrganizationEdit.jsx
@@ -0,0 +1,29 @@
+import React from 'react';
+import {
+ Card,
+ CardBody,
+ PageSection,
+ PageSectionVariants
+} from '@patternfly/react-core';
+import {
+ Link
+} from 'react-router-dom';
+
+const OrganizationEdit = ({ match, parentBreadcrumbObj, organization }) => {
+ const { medium } = PageSectionVariants;
+
+ return (
+
+
+
+ {'edit view '}
+
+ {'save/cancel and go back to view'}
+
+
+
+
+ );
+};
+
+export default OrganizationEdit;
diff --git a/src/components/OrganizationListItem.jsx b/src/pages/Organizations/components/OrganizationListItem.jsx
similarity index 59%
rename from src/components/OrganizationListItem.jsx
rename to src/pages/Organizations/components/OrganizationListItem.jsx
index f64beb3984..d199b47e96 100644
--- a/src/components/OrganizationListItem.jsx
+++ b/src/pages/Organizations/components/OrganizationListItem.jsx
@@ -3,8 +3,21 @@ import {
Badge,
Checkbox,
} from '@patternfly/react-core';
+import {
+ Link
+} from 'react-router-dom';
-export default ({ itemId, name, userCount, teamCount, adminCount, isSelected, onSelect }) => (
+export default ({
+ itemId,
+ name,
+ userCount,
+ teamCount,
+ adminCount,
+ isSelected,
+ onSelect,
+ detailUrl,
+ parentBreadcrumb
+}) => (
-
Users
+
+ Users
+
{' '}
{userCount}
{' '}
-
Teams
+
+ Teams
+
{' '}
{teamCount}
{' '}
-
Admins
+
+ Admins
+
{' '}
{adminCount}
diff --git a/src/pages/Organizations/index.jsx b/src/pages/Organizations/index.jsx
new file mode 100644
index 0000000000..09299873df
--- /dev/null
+++ b/src/pages/Organizations/index.jsx
@@ -0,0 +1,16 @@
+import React from 'react';
+import { Route, Switch } from 'react-router-dom';
+
+import OrganizationAdd from './views/Organization.add';
+import OrganizationView from './views/Organization.view';
+import OrganizationsList from './views/Organizations.list';
+
+const Organizations = ({ match }) => (
+
+
+
+
+
+);
+
+export default Organizations;
diff --git a/src/pages/Organizations/tabs.scss b/src/pages/Organizations/tabs.scss
new file mode 100644
index 0000000000..2afa2f5ee7
--- /dev/null
+++ b/src/pages/Organizations/tabs.scss
@@ -0,0 +1,18 @@
+.at-c-tabs {
+ padding: 0 5px !important;
+ margin: 0 -10px !important;
+
+ .at-c-tabs__tab {
+ margin: 0 5px;
+ }
+
+ .at-c-tabs__tab.at-m-selected {
+ text-decoration: underline;
+ }
+}
+
+.at-c-orgPane {
+ a {
+ display: block;
+ }
+}
\ No newline at end of file
diff --git a/src/pages/Organizations/utils.jsx b/src/pages/Organizations/utils.jsx
new file mode 100644
index 0000000000..4db8f0627d
--- /dev/null
+++ b/src/pages/Organizations/utils.jsx
@@ -0,0 +1,17 @@
+const getTabName = (tab) => {
+ let tabName = '';
+ if (tab === 'details') {
+ tabName = 'Details';
+ } else if (tab === 'users') {
+ tabName = 'Users';
+ } else if (tab === 'teams') {
+ tabName = 'Teams';
+ } else if (tab === 'admins') {
+ tabName = 'Admins';
+ } else if (tab === 'notifications') {
+ tabName = 'Notifications';
+ }
+ return tabName;
+};
+
+export default getTabName;
diff --git a/src/pages/Organizations/views/Organization.add.jsx b/src/pages/Organizations/views/Organization.add.jsx
new file mode 100644
index 0000000000..831cc0c243
--- /dev/null
+++ b/src/pages/Organizations/views/Organization.add.jsx
@@ -0,0 +1,21 @@
+import React, { Fragment } from 'react';
+import {
+ PageSection,
+ PageSectionVariants,
+ Title,
+} from '@patternfly/react-core';
+
+const { light, medium } = PageSectionVariants;
+
+const OrganizationView = () => (
+
+
+ Organization Add
+
+
+ This is the add view
+
+
+);
+
+export default OrganizationView;
diff --git a/src/pages/Organizations/views/Organization.view.jsx b/src/pages/Organizations/views/Organization.view.jsx
new file mode 100644
index 0000000000..f714eda0a5
--- /dev/null
+++ b/src/pages/Organizations/views/Organization.view.jsx
@@ -0,0 +1,120 @@
+import React, { Component, Fragment } from 'react';
+import {
+ Switch,
+ Route
+} from 'react-router-dom';
+
+import OrganizationBreadcrumb from '../components/OrganizationBreadcrumb';
+import OrganizationDetail from '../components/OrganizationDetail';
+import OrganizationEdit from '../components/OrganizationEdit';
+
+import api from '../../../api';
+import { API_ORGANIZATIONS } from '../../../endpoints';
+
+class OrganizationView extends Component {
+ constructor (props) {
+ super(props);
+
+ let { breadcrumb: parentBreadcrumbObj, organization } = props.location.state || {};
+ if (!parentBreadcrumbObj) {
+ parentBreadcrumbObj = 'loading';
+ }
+ if (!organization) {
+ organization = 'loading';
+ }
+ this.state = {
+ parentBreadcrumbObj,
+ organization,
+ error: false,
+ loading: false,
+ mounted: false
+ };
+ }
+
+ componentDidMount () {
+ this.setState({ mounted: true }, () => {
+ const { organization } = this.state;
+ if (organization === 'loading') {
+ this.fetchOrganization();
+ }
+ });
+ }
+
+ componentWillUnmount () {
+ this.setState({ mounted: false });
+ }
+
+ async fetchOrganization () {
+ const { mounted } = this.state;
+ if (mounted) {
+ this.setState({ error: false, loading: true });
+
+ const { match } = this.props;
+ const { parentBreadcrumbObj, organization } = this.state;
+ try {
+ const { data } = await api.get(`${API_ORGANIZATIONS}${match.params.id}/`);
+ if (organization === 'loading') {
+ this.setState({ organization: data });
+ }
+ const { name } = data;
+ if (parentBreadcrumbObj === 'loading') {
+ this.setState({ parentBreadcrumbObj: [{ name: 'Organizations', url: '/organizations' }, { name, url: match.url }] });
+ }
+ } catch (err) {
+ this.setState({ error: true });
+ } finally {
+ this.setState({ loading: false });
+ }
+ }
+ }
+
+ render () {
+ const { location, match } = this.props;
+ const { parentBreadcrumbObj, organization, error, loading } = this.state;
+ const params = new URLSearchParams(location.search);
+ const currentTab = params.get('tab') || 'details';
+
+ return (
+
+
+
+ (
+
+ )}
+ />
+ (
+
+ )}
+ />
+
+ {error ? 'error!' : ''}
+ {loading ? 'loading...' : ''}
+
+ );
+ }
+}
+
+export default OrganizationView;
diff --git a/src/pages/Organizations.jsx b/src/pages/Organizations/views/Organizations.list.jsx
similarity index 91%
rename from src/pages/Organizations.jsx
rename to src/pages/Organizations/views/Organizations.list.jsx
index 050031afeb..741eeb30f0 100644
--- a/src/pages/Organizations.jsx
+++ b/src/pages/Organizations/views/Organizations.list.jsx
@@ -11,17 +11,17 @@ import {
Title,
} from '@patternfly/react-core';
-import DataListToolbar from '../components/DataListToolbar';
+import DataListToolbar from '../../../components/DataListToolbar';
import OrganizationListItem from '../components/OrganizationListItem';
-import Pagination from '../components/Pagination';
+import Pagination from '../../../components/Pagination';
-import api from '../api';
-import { API_ORGANIZATIONS } from '../endpoints';
+import api from '../../../api';
+import { API_ORGANIZATIONS } from '../../../endpoints';
import {
encodeQueryString,
parseQueryString,
-} from '../qs';
+} from '../../../qs';
class Organizations extends Component {
columns = [
@@ -58,7 +58,6 @@ class Organizations extends Component {
componentDidMount () {
const queryParams = this.getQueryParams();
-
this.fetchOrganizations(queryParams);
}
@@ -122,7 +121,6 @@ class Organizations extends Component {
updateUrl (queryParams) {
const { history, location } = this.props;
-
const pathname = '/organizations';
const search = `?${encodeQueryString(queryParams)}`;
@@ -185,6 +183,8 @@ class Organizations extends Component {
results,
selected,
} = this.state;
+ const { match } = this.props;
+ const parentBreadcrumb = { name: 'Organizations', url: match.url };
return (
@@ -193,6 +193,7 @@ class Organizations extends Component {