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

Merge pull request #28 from jlmitch5/orgUrls

add routes and breadcrumbs for org detail/edit/related routes
This commit is contained in:
John Mitchell 2018-12-05 10:01:01 -05:00 committed by GitHub
commit 8f54ec681d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 764 additions and 172 deletions

View File

@ -4,8 +4,6 @@ import DataListToolbar from '../../src/components/DataListToolbar';
describe('<DataListToolbar />', () => { describe('<DataListToolbar />', () => {
const columns = [{ name: 'Name', key: 'name', isSortable: true }]; const columns = [{ name: 'Name', key: 'name', isSortable: true }];
const noop = () => {};
let toolbar; let toolbar;
afterEach(() => { afterEach(() => {

View File

@ -0,0 +1,55 @@
import React from 'react';
import { mount } from 'enzyme';
import Tooltip from '../../src/components/Tooltip';
describe('<Tooltip />', () => {
let elem;
let content;
let mouseOverHandler;
let mouseOutHandler;
test('initially renders without crashing', () => {
elem = mount(<Tooltip />);
expect(elem.length).toBe(1);
});
test('shows/hides with mouse over and leave', () => {
elem = mount(<Tooltip />);
mouseOverHandler = elem.find('.mouseOverHandler');
mouseOutHandler = elem.find('.mouseOutHandler');
expect(elem.state().isDisplayed).toBe(false);
elem.update();
content = elem.find('.pf-c-tooltip__content');
expect(content.length).toBe(0);
mouseOverHandler.props().onMouseOver();
expect(elem.state().isDisplayed).toBe(true);
elem.update();
content = elem.find('.pf-c-tooltip__content');
expect(content.length).toBe(1);
mouseOutHandler.props().onMouseLeave();
expect(elem.state().isDisplayed).toBe(false);
elem.update();
content = elem.find('.pf-c-tooltip__content');
expect(content.length).toBe(0);
});
test('shows/hides with focus and blur', () => {
elem = mount(<Tooltip />);
mouseOverHandler = elem.find('.mouseOverHandler');
mouseOutHandler = elem.find('.mouseOutHandler');
expect(elem.state().isDisplayed).toBe(false);
elem.update();
content = elem.find('.pf-c-tooltip__content');
expect(content.length).toBe(0);
mouseOverHandler.props().onFocus();
expect(elem.state().isDisplayed).toBe(true);
elem.update();
content = elem.find('.pf-c-tooltip__content');
expect(content.length).toBe(1);
mouseOutHandler.props().onBlur();
expect(elem.state().isDisplayed).toBe(false);
elem.update();
content = elem.find('.pf-c-tooltip__content');
expect(content.length).toBe(0);
});
});

View File

@ -1,90 +0,0 @@
import React from 'react';
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';
describe('<Organizations />', () => {
let pageWrapper;
const results = [
{
id: 1,
name: 'org 1',
summary_fields: {
related_field_counts: {
users: 1,
teams: 1,
admins: 1
}
}
},
{
id: 2,
name: 'org 2',
summary_fields: {
related_field_counts: {
users: 1,
teams: 1,
admins: 1
}
}
},
{
id: 3,
name: 'org 3',
summary_fields: {
related_field_counts: {
users: 1,
teams: 1,
admins: 1
}
}
},
];
const count = results.length;
const response = { data: { count, results } };
beforeEach(() => {
api.get = jest.fn().mockImplementation(() => Promise.resolve(response));
pageWrapper = mount(<HashRouter><Organizations /></HashRouter>);
});
afterEach(() => {
pageWrapper.unmount();
});
test('it renders expected content', () => {
const pageSections = pageWrapper.find('PageSection');
const title = pageWrapper.find('Title');
expect(pageWrapper.length).toBe(1);
expect(pageSections.length).toBe(2);
expect(title.length).toBe(1);
expect(title.props().size).toBe('2xl');
pageSections.forEach(section => {
expect(section.props().variant).toBeDefined();
});
expect(pageWrapper.find('ul').length).toBe(1);
expect(pageWrapper.find('ul li').length).toBe(0);
// will render all list items on update
pageWrapper.update();
expect(pageWrapper.find('ul li').length).toBe(count);
});
test('API Organization endpoint is valid', () => {
expect(API_ORGANIZATIONS).toBeDefined();
});
test('it displays a tooltip on delete hover', () => {
const tooltip = '.pf-c-tooltip__content';
const deleteButton = 'button[aria-label="Delete"]';
expect(pageWrapper.find(tooltip).length).toBe(0);
pageWrapper.find(deleteButton).simulate('mouseover');
expect(pageWrapper.find(tooltip).length).toBe(1);
});
});

View File

@ -0,0 +1,18 @@
import React from 'react';
import { mount } from 'enzyme';
import { MemoryRouter } from 'react-router-dom';
import OrganizationBreadcrumb from '../../../../src/pages/Organizations/components/OrganizationBreadcrumb';
describe('<OrganizationBreadcrumb />', () => {
test('initially renders succesfully', () => {
mount(
<MemoryRouter initialEntries={['/organizations']} initialIndex={0}>
<OrganizationBreadcrumb
match={{ path: '/organizations', url: '/organizations' }}
location={{ search: '', pathname: '/organizations' }}
parentObj={[{ name: 'Organizations', url: '/organizations' }]}
/>
</MemoryRouter>
);
});
});

View File

@ -0,0 +1,17 @@
import React from 'react';
import { mount } from 'enzyme';
import { MemoryRouter } from 'react-router-dom';
import OrganizationDetail from '../../../../src/pages/Organizations/components/OrganizationDetail';
describe('<OrganizationDetail />', () => {
test('initially renders succesfully', () => {
mount(
<MemoryRouter initialEntries={['/organizations/1']} initialIndex={0}>
<OrganizationDetail
match={{ path: '/organizations/:id', url: '/organizations/1' }}
location={{ search: '', pathname: '/organizations/1' }}
/>
</MemoryRouter>
);
});
});

View File

@ -0,0 +1,16 @@
import React from 'react';
import { mount } from 'enzyme';
import { MemoryRouter } from 'react-router-dom';
import OrganizationEdit from '../../../../src/pages/Organizations/components/OrganizationEdit';
describe('<OrganizationEdit />', () => {
test('initially renders succesfully', () => {
mount(
<MemoryRouter initialEntries={['/organizations/1/edit']} initialIndex={0}>
<OrganizationEdit
match={{ path: '/organizations/:id/edit', url: '/organizations/1/edit', params: { id: 1 } }}
/>
</MemoryRouter>
);
});
});

View File

@ -0,0 +1,14 @@
import React from 'react';
import { mount } from 'enzyme';
import { MemoryRouter } from 'react-router-dom';
import OrganizationListItem from '../../../../src/pages/Organizations/components/OrganizationListItem';
describe('<OrganizationListItem />', () => {
test('initially renders succesfully', () => {
mount(
<MemoryRouter initialEntries={['/organizations']} initialIndex={0}>
<OrganizationListItem />
</MemoryRouter>
);
});
});

View File

@ -0,0 +1,17 @@
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { mount } from 'enzyme';
import Organizations from '../../../src/pages/Organizations/index';
describe('<Organizations />', () => {
test('initially renders succesfully', () => {
mount(
<MemoryRouter initialEntries={['/organizations']} initialIndex={0}>
<Organizations
match={{ path: '/organizations', url: '/organizations' }}
location={{ search: '', pathname: '/organizations' }}
/>
</MemoryRouter>
);
});
});

View File

@ -0,0 +1,13 @@
import getTabName from '../../../src/pages/Organizations/utils';
describe('getTabName', () => {
test('returns tab name', () => {
expect(getTabName('details')).toBe('Details');
expect(getTabName('users')).toBe('Users');
expect(getTabName('teams')).toBe('Teams');
expect(getTabName('admins')).toBe('Admins');
expect(getTabName('notifications')).toBe('Notifications');
expect(getTabName('unknown')).toBe('');
expect(getTabName()).toBe('');
});
});

View File

@ -0,0 +1,14 @@
import React from 'react';
import { mount } from 'enzyme';
import OrganizationAdd from '../../../../src/pages/Organizations/views/Organization.add';
describe('<OrganizationAdd />', () => {
test('initially renders succesfully', () => {
mount(
<OrganizationAdd
match={{ path: '/organizations/add', url: '/organizations/add' }}
location={{ search: '', pathname: '/organizations/add' }}
/>
);
});
});

View File

@ -0,0 +1,17 @@
import React from 'react';
import { mount } from 'enzyme';
import { MemoryRouter } from 'react-router-dom';
import OrganizationView from '../../../../src/pages/Organizations/views/Organization.view';
describe('<OrganizationView />', () => {
test('initially renders succesfully', () => {
mount(
<MemoryRouter initialEntries={['/organizations/1']} initialIndex={0}>
<OrganizationView
match={{ path: '/organizations/:id', url: '/organizations/1' }}
location={{ search: '', pathname: '/organizations/1' }}
/>
</MemoryRouter>
);
});
});

View File

@ -0,0 +1,17 @@
import React from 'react';
import { mount } from 'enzyme';
import { MemoryRouter } from 'react-router-dom';
import OrganizationsList from '../../../../src/pages/Organizations/views/Organizations.list';
describe('<OrganizationsList />', () => {
test('initially renders succesfully', () => {
mount(
<MemoryRouter initialEntries={['/organizations']} initialIndex={0}>
<OrganizationsList
match={{ path: '/organizations', url: '/organizations' }}
location={{ search: '', pathname: '/organizations' }}
/>
</MemoryRouter>
);
});
});

View File

@ -5,7 +5,7 @@
"main": "index.jsx", "main": "index.jsx",
"scripts": { "scripts": {
"start": "webpack-dev-server --config ./webpack.config.js --mode development", "start": "webpack-dev-server --config ./webpack.config.js --mode development",
"test": "jest --watchAll --coverage", "test": "jest --watch --coverage",
"lint": "./node_modules/eslint/bin/eslint.js src/**/*.js src/**/*.jsx" "lint": "./node_modules/eslint/bin/eslint.js src/**/*.js src/**/*.jsx"
}, },
"keywords": [], "keywords": [],

View File

@ -6,8 +6,6 @@ import {
DropdownPosition, DropdownPosition,
DropdownToggle, DropdownToggle,
DropdownItem, DropdownItem,
FormGroup,
KebabToggle,
Level, Level,
LevelItem, LevelItem,
TextInput, TextInput,
@ -24,11 +22,14 @@ import {
SortNumericUpIcon, SortNumericUpIcon,
TrashAltIcon, TrashAltIcon,
} from '@patternfly/react-icons'; } from '@patternfly/react-icons';
import {
Link
} from 'react-router-dom';
import Tooltip from '../Tooltip'; import Tooltip from '../Tooltip';
class DataListToolbar extends React.Component { class DataListToolbar extends React.Component {
constructor(props) { constructor (props) {
super(props); super(props);
const { sortedColumnKey } = this.props; const { sortedColumnKey } = this.props;
@ -72,15 +73,7 @@ class DataListToolbar extends React.Component {
this.setState({ isSearchDropdownOpen: false, searchKey: key }); this.setState({ isSearchDropdownOpen: false, searchKey: key });
}; };
onActionToggle = isActionDropdownOpen => { render () {
this.setState({ isActionDropdownOpen });
};
onActionSelect = ({ target }) => {
this.setState({ isActionDropdownOpen: false });
};
render() {
const { up } = DropdownPosition; const { up } = DropdownPosition;
const { const {
columns, columns,
@ -90,9 +83,10 @@ class DataListToolbar extends React.Component {
onSort, onSort,
sortedColumnKey, sortedColumnKey,
sortOrder, sortOrder,
addUrl
} = this.props; } = this.props;
const { const {
isActionDropdownOpen, // isActionDropdownOpen,
isSearchDropdownOpen, isSearchDropdownOpen,
isSortDropdownOpen, isSortDropdownOpen,
searchKey, searchKey,
@ -107,19 +101,29 @@ class DataListToolbar extends React.Component {
.filter(({ key }) => key === sortedColumnKey); .filter(({ key }) => key === sortedColumnKey);
const sortedColumnName = sortedColumn.name; const sortedColumnName = sortedColumn.name;
const isSortNumeric = sortedColumn.isNumeric; const isSortNumeric = sortedColumn.isNumeric;
const displayedSortIcon = () => {
let icon;
if (sortOrder === 'ascending') {
icon = isSortNumeric ? (<SortNumericUpIcon />) : (<SortAlphaUpIcon />);
} else {
icon = isSortNumeric ? (<SortNumericDownIcon />) : (<SortAlphaDownIcon />);
}
return icon;
};
return ( return (
<div className="awx-toolbar"> <div className="awx-toolbar">
<Level> <Level>
<LevelItem> <LevelItem>
<Toolbar style={{ marginLeft: "20px" }}> <Toolbar style={{ marginLeft: '20px' }}>
<ToolbarGroup> <ToolbarGroup>
<ToolbarItem> <ToolbarItem>
<Checkbox <Checkbox
checked={isAllSelected} checked={isAllSelected}
onChange={onSelectAll} onChange={onSelectAll}
aria-label="Select all" aria-label="Select all"
id="select-all"/> id="select-all"
/>
</ToolbarItem> </ToolbarItem>
</ToolbarGroup> </ToolbarGroup>
<ToolbarGroup> <ToolbarGroup>
@ -132,10 +136,12 @@ class DataListToolbar extends React.Component {
isOpen={isSearchDropdownOpen} isOpen={isSearchDropdownOpen}
toggle={( toggle={(
<DropdownToggle <DropdownToggle
onToggle={this.onSearchDropdownToggle}> onToggle={this.onSearchDropdownToggle}
>
{ searchColumnName } { searchColumnName }
</DropdownToggle> </DropdownToggle>
)}> )}
>
{columns.filter(({ key }) => key !== searchKey).map(({ key, name }) => ( {columns.filter(({ key }) => key !== searchKey).map(({ key, name }) => (
<DropdownItem key={key} component="button"> <DropdownItem key={key} component="button">
{ name } { name }
@ -146,12 +152,14 @@ class DataListToolbar extends React.Component {
type="search" type="search"
aria-label="search text input" aria-label="search text input"
value={searchValue} value={searchValue}
onChange={this.handleSearchInputChange}/> onChange={this.handleSearchInputChange}
/>
<Button <Button
variant="tertiary" variant="tertiary"
aria-label="Search" aria-label="Search"
onClick={() => onSearch(searchValue)}> onClick={() => onSearch(searchValue)}
<i className="fas fa-search" aria-hidden="true"></i> >
<i className="fas fa-search" aria-hidden="true" />
</Button> </Button>
</div> </div>
</ToolbarItem> </ToolbarItem>
@ -165,31 +173,28 @@ class DataListToolbar extends React.Component {
isOpen={isSortDropdownOpen} isOpen={isSortDropdownOpen}
toggle={( toggle={(
<DropdownToggle <DropdownToggle
onToggle={this.onSortDropdownToggle}> onToggle={this.onSortDropdownToggle}
>
{ sortedColumnName } { sortedColumnName }
</DropdownToggle> </DropdownToggle>
)}> )}
>
{columns {columns
.filter(({ key, isSortable }) => isSortable && key !== sortedColumnKey) .filter(({ key, isSortable }) => isSortable && key !== sortedColumnKey)
.map(({ key, name }) => ( .map(({ key, name }) => (
<DropdownItem key={key} component="button"> <DropdownItem key={key} component="button">
{ name } { name }
</DropdownItem> </DropdownItem>
))} ))}
</Dropdown> </Dropdown>
</ToolbarItem> </ToolbarItem>
<ToolbarItem> <ToolbarItem>
<Button <Button
onClick={() => onSort(sortedColumnKey, sortOrder === "ascending" ? "descending" : "ascending")} onClick={() => onSort(sortedColumnKey, sortOrder === 'ascending' ? 'descending' : 'ascending')}
variant="plain" variant="plain"
aria-label="Sort"> aria-label="Sort"
{ >
isSortNumeric ? ( {displayedSortIcon()}
sortOrder === "ascending" ? <SortNumericUpIcon /> : <SortNumericDownIcon />
) : (
sortOrder === "ascending" ? <SortAlphaUpIcon /> : <SortAlphaDownIcon />
)
}
</Button> </Button>
</ToolbarItem> </ToolbarItem>
</ToolbarGroup> </ToolbarGroup>
@ -210,12 +215,16 @@ class DataListToolbar extends React.Component {
<LevelItem> <LevelItem>
<Tooltip message="Delete" position="top"> <Tooltip message="Delete" position="top">
<Button variant="plain" aria-label="Delete"> <Button variant="plain" aria-label="Delete">
<TrashAltIcon/> <TrashAltIcon />
</Button> </Button>
</Tooltip> </Tooltip>
<Button variant="primary" aria-label="Add"> {addUrl && (
Add <Link to={addUrl}>
</Button> <Button variant="primary" aria-label="Add">
Add
</Button>
</Link>
)}
</LevelItem> </LevelItem>
</Level> </Level>
</div> </div>
@ -223,4 +232,4 @@ class DataListToolbar extends React.Component {
} }
} }
export default DataListToolbar; export default DataListToolbar;

View File

@ -3,36 +3,36 @@ import React from 'react';
class Tooltip extends React.Component { class Tooltip extends React.Component {
transforms = { transforms = {
top: { top: {
bottom: "100%", bottom: '100%',
left: "50%", left: '50%',
transform: "translate(-50%, -25%)" transform: 'translate(-50%, -25%)'
}, },
bottom: { bottom: {
top: "100%", top: '100%',
left: "50%", left: '50%',
transform: "translate(-50%, 25%)" transform: 'translate(-50%, 25%)'
}, },
left: { left: {
top: "50%", top: '50%',
right: "100%", right: '100%',
transform: "translate(-25%, -50%)" transform: 'translate(-25%, -50%)'
}, },
right: { right: {
bottom: "100%", bottom: '100%',
left: "50%", left: '50%',
transform: "translate(25%, 50%)" transform: 'translate(25%, 50%)'
}, },
}; };
constructor(props) { constructor (props) {
super(props) super(props);
this.state = { this.state = {
isDisplayed: false isDisplayed: false
}; };
} }
render() { render () {
const { const {
children, children,
message, message,
@ -44,24 +44,33 @@ class Tooltip extends React.Component {
return ( return (
<span <span
style={{ position: "relative"}} style={{ position: 'relative' }}
onMouseLeave={() => this.setState({ isDisplayed: false })}> className="mouseOutHandler"
{ isDisplayed && onMouseLeave={() => this.setState({ isDisplayed: false })}
<div onBlur={() => this.setState({ isDisplayed: false })}
style={{ position: "absolute", zIndex: "10", ...this.transforms[position] }} >
className={`pf-c-tooltip pf-m-${position}`}> { isDisplayed
<div className="pf-c-tooltip__arrow"></div> && (
<div className="pf-c-tooltip__content"> <div
{ message } style={{ position: 'absolute', zIndex: '10', ...this.transforms[position] }}
className={`pf-c-tooltip pf-m-${position}`}
>
<div className="pf-c-tooltip__arrow" />
<div className="pf-c-tooltip__content">
{ message }
</div>
</div> </div>
</div> )
} }
<span <span
onMouseOver={() => this.setState({ isDisplayed: true })}> className="mouseOverHandler"
onMouseOver={() => this.setState({ isDisplayed: true })}
onFocus={() => this.setState({ isDisplayed: true })}
>
{ children } { children }
</span> </span>
</span> </span>
) );
} }
} }

View File

@ -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) => (
<Fragment>
{parentObj
.map(({ url, name }, index) => {
let elem;
if (noLastLink && parentObj.length - 1 === index) {
elem = (<Fragment key={name}>{name}</Fragment>);
} else {
elem = (
<Link
key={name}
to={{ pathname: url, state: { breadcrumb: parentObj, organization } }}
>
{name}
</Link>
);
}
return elem;
})
.reduce((prev, curr) => [prev, ' > ', curr])}
</Fragment>
);
if (currentTab && currentTab !== 'details') {
breadcrumb = (
<Fragment>
{generateCrumb()}
{' > '}
{getTabName(currentTab)}
</Fragment>
);
} else if (location.pathname.indexOf('edit') > -1) {
breadcrumb = (
<Fragment>
{generateCrumb()}
{' > edit'}
</Fragment>
);
} else if (location.pathname.indexOf('add') > -1) {
breadcrumb = (
<Fragment>
{generateCrumb()}
{' > add'}
</Fragment>
);
} else {
breadcrumb = (
<Fragment>
{generateCrumb(true)}
</Fragment>
);
}
}
return (
<PageSection variant={light} className="pf-m-condensed">
<Title size="2xl">{breadcrumb}</Title>
</PageSection>
);
};
export default OrganizationBreadcrumb;

View File

@ -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 (
<ToolbarItem className={tabClasses()}>
<Link to={{ pathname: `${match.url}`, search: updateTab(), state: { breadcrumb } }} replace={tab === currentTab}>
{children}
</Link>
</ToolbarItem>
);
};
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 = () => (
<Fragment>
{`deleting ${currentTab} association with orgs `}
<Link to={{ pathname: `${match.url}`, search: `?${params.toString()}`, state: { breadcrumb: parentBreadcrumbObj, organization } }}>
{`confirm removal of ${currentTab}/cancel and go back to ${currentTab} view.`}
</Link>
</Fragment>
);
const addResourceView = () => (
<Fragment>
{`adding ${currentTab} `}
<Link to={{ pathname: `${match.url}`, search: `?${params.toString()}`, state: { breadcrumb: parentBreadcrumbObj, organization } }}>
{`save/cancel and go back to ${currentTab} view`}
</Link>
</Fragment>
);
const resourceView = () => (
<Fragment>
{`${currentTab} detail view `}
<Link to={{ pathname: `${match.url}/add-resource`, search: `?${params.toString()}`, state: { breadcrumb: parentBreadcrumbObj, organization } }}>
{`add ${currentTab}`}
</Link>
{' '}
<Link to={{ pathname: `${match.url}/delete-resources`, search: `?${params.toString()}`, state: { breadcrumb: parentBreadcrumbObj, organization } }}>
{`delete ${currentTab}`}
</Link>
</Fragment>
);
const detailTabs = (tabs) => (
<ToolbarSection aria-label="Organization detail tabs">
<ToolbarGroup className="at-c-tabs">
{tabs.map(tab => (
<DetailTab
key={tab}
tab={tab}
location={location}
match={match}
currentTab={currentTab}
breadcrumb={parentBreadcrumbObj}
>
{getTabName(tab)}
</DetailTab>
))}
</ToolbarGroup>
</ToolbarSection>
);
return (
<PageSection variant={medium}>
<Card className="at-c-orgPane">
<CardHeader>
{detailTabs(['details', 'users', 'teams', 'admins', 'notifications'])}
</CardHeader>
<CardBody>
{(currentTab && currentTab !== 'details') ? (
<Switch>
<Route path={`${match.path}/delete-resources`} component={() => deleteResourceView()} />
<Route path={`${match.path}/add-resource`} component={() => addResourceView()} />
<Route path={`${match.path}`} component={() => resourceView()} />
</Switch>
) : (
<Fragment>
{'detail view '}
<Link to={{ pathname: `${match.url}/edit`, state: { breadcrumb: parentBreadcrumbObj, organization } }}>
{'edit'}
</Link>
</Fragment>
)}
</CardBody>
</Card>
</PageSection>
);
};
export default OrganizationDetail;

View File

@ -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 (
<PageSection variant={medium}>
<Card className="at-c-orgPane">
<CardBody>
{'edit view '}
<Link to={{ pathname: `/organizations/${match.params.id}`, state: { breadcrumb: parentBreadcrumbObj, organization } }}>
{'save/cancel and go back to view'}
</Link>
</CardBody>
</Card>
</PageSection>
);
};
export default OrganizationEdit;

View File

@ -3,8 +3,21 @@ import {
Badge, Badge,
Checkbox, Checkbox,
} from '@patternfly/react-core'; } 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
}) => (
<li key={itemId} className="pf-c-data-list__item" aria-labelledby="check-action-item1"> <li key={itemId} className="pf-c-data-list__item" aria-labelledby="check-action-item1">
<div className="pf-c-data-list__check"> <div className="pf-c-data-list__check">
<Checkbox <Checkbox
@ -16,23 +29,36 @@ export default ({ itemId, name, userCount, teamCount, adminCount, isSelected, on
</div> </div>
<div className="pf-c-data-list__cell"> <div className="pf-c-data-list__cell">
<span id="check-action-item1"> <span id="check-action-item1">
<a href={`#/organizations/${itemId}`}>{ name }</a> <Link
to={{
pathname: detailUrl,
state: { breadcrumb: [parentBreadcrumb, { name, url: detailUrl }] }
}}
>
{name}
</Link>
</span> </span>
</div> </div>
<div className="pf-c-data-list__cell"> <div className="pf-c-data-list__cell">
<a href="#/dashboard"> Users </a> <Link to={`${detailUrl}?tab=users`}>
Users
</Link>
<Badge isRead> <Badge isRead>
{' '} {' '}
{userCount} {userCount}
{' '} {' '}
</Badge> </Badge>
<a href="#/dashboard"> Teams </a> <Link to={`${detailUrl}?tab=teams`}>
Teams
</Link>
<Badge isRead> <Badge isRead>
{' '} {' '}
{teamCount} {teamCount}
{' '} {' '}
</Badge> </Badge>
<a href="#/dashboard"> Admins </a> <Link to={`${detailUrl}?tab=admins`}>
Admins
</Link>
<Badge isRead> <Badge isRead>
{' '} {' '}
{adminCount} {adminCount}

View File

@ -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 }) => (
<Switch>
<Route path={`${match.path}/add`} component={OrganizationAdd} />
<Route path={`${match.path}/:id`} component={OrganizationView} />
<Route path={`${match.path}`} component={OrganizationsList} />
</Switch>
);
export default Organizations;

View File

@ -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;
}
}

View File

@ -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;

View File

@ -0,0 +1,21 @@
import React, { Fragment } from 'react';
import {
PageSection,
PageSectionVariants,
Title,
} from '@patternfly/react-core';
const { light, medium } = PageSectionVariants;
const OrganizationView = () => (
<Fragment>
<PageSection variant={light} className="pf-m-condensed">
<Title size="2xl">Organization Add</Title>
</PageSection>
<PageSection variant={medium}>
This is the add view
</PageSection>
</Fragment>
);
export default OrganizationView;

View File

@ -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 (
<Fragment>
<OrganizationBreadcrumb
parentObj={parentBreadcrumbObj}
currentTab={currentTab}
location={location}
organization={organization}
/>
<Switch>
<Route
path={`${match.path}/edit`}
component={() => (
<OrganizationEdit
location={location}
match={match}
parentBreadcrumbObj={parentBreadcrumbObj}
organization={organization}
params={params}
currentTab={currentTab}
/>
)}
/>
<Route
path={`${match.path}`}
component={() => (
<OrganizationDetail
location={location}
match={match}
parentBreadcrumbObj={parentBreadcrumbObj}
organization={organization}
params={params}
currentTab={currentTab}
/>
)}
/>
</Switch>
{error ? 'error!' : ''}
{loading ? 'loading...' : ''}
</Fragment>
);
}
}
export default OrganizationView;

View File

@ -11,17 +11,17 @@ import {
Title, Title,
} from '@patternfly/react-core'; } from '@patternfly/react-core';
import DataListToolbar from '../components/DataListToolbar'; import DataListToolbar from '../../../components/DataListToolbar';
import OrganizationListItem from '../components/OrganizationListItem'; import OrganizationListItem from '../components/OrganizationListItem';
import Pagination from '../components/Pagination'; import Pagination from '../../../components/Pagination';
import api from '../api'; import api from '../../../api';
import { API_ORGANIZATIONS } from '../endpoints'; import { API_ORGANIZATIONS } from '../../../endpoints';
import { import {
encodeQueryString, encodeQueryString,
parseQueryString, parseQueryString,
} from '../qs'; } from '../../../qs';
class Organizations extends Component { class Organizations extends Component {
columns = [ columns = [
@ -58,7 +58,6 @@ class Organizations extends Component {
componentDidMount () { componentDidMount () {
const queryParams = this.getQueryParams(); const queryParams = this.getQueryParams();
this.fetchOrganizations(queryParams); this.fetchOrganizations(queryParams);
} }
@ -122,7 +121,6 @@ class Organizations extends Component {
updateUrl (queryParams) { updateUrl (queryParams) {
const { history, location } = this.props; const { history, location } = this.props;
const pathname = '/organizations'; const pathname = '/organizations';
const search = `?${encodeQueryString(queryParams)}`; const search = `?${encodeQueryString(queryParams)}`;
@ -185,6 +183,8 @@ class Organizations extends Component {
results, results,
selected, selected,
} = this.state; } = this.state;
const { match } = this.props;
const parentBreadcrumb = { name: 'Organizations', url: match.url };
return ( return (
<Fragment> <Fragment>
@ -193,6 +193,7 @@ class Organizations extends Component {
</PageSection> </PageSection>
<PageSection variant={medium}> <PageSection variant={medium}>
<DataListToolbar <DataListToolbar
addUrl={`${match.url}/add`}
isAllSelected={selected.length === results.length} isAllSelected={selected.length === results.length}
sortedColumnKey={sortedColumnKey} sortedColumnKey={sortedColumnKey}
sortOrder={sortOrder} sortOrder={sortOrder}
@ -207,6 +208,8 @@ class Organizations extends Component {
key={o.id} key={o.id}
itemId={o.id} itemId={o.id}
name={o.name} name={o.name}
detailUrl={`${match.url}/${o.id}`}
parentBreadcrumb={parentBreadcrumb}
userCount={o.summary_fields.related_field_counts.users} userCount={o.summary_fields.related_field_counts.users}
teamCount={o.summary_fields.related_field_counts.teams} teamCount={o.summary_fields.related_field_counts.teams}
adminCount={o.summary_fields.related_field_counts.admins} adminCount={o.summary_fields.related_field_counts.admins}