mirror of
https://github.com/ansible/awx.git
synced 2026-03-11 14:39:30 -02:30
update organizations structure and add unstyled sub routes and breadcrumbs
This commit is contained in:
@@ -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;
|
||||
140
src/pages/Organizations/components/OrganizationDetail.jsx
Normal file
140
src/pages/Organizations/components/OrganizationDetail.jsx
Normal 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 } }}>
|
||||
{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;
|
||||
29
src/pages/Organizations/components/OrganizationEdit.jsx
Normal file
29
src/pages/Organizations/components/OrganizationEdit.jsx
Normal 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;
|
||||
70
src/pages/Organizations/components/OrganizationListItem.jsx
Normal file
70
src/pages/Organizations/components/OrganizationListItem.jsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Badge,
|
||||
Checkbox,
|
||||
} from '@patternfly/react-core';
|
||||
import {
|
||||
Link
|
||||
} from 'react-router-dom';
|
||||
|
||||
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">
|
||||
<div className="pf-c-data-list__check">
|
||||
<Checkbox
|
||||
checked={isSelected}
|
||||
onChange={onSelect}
|
||||
aria-label={`select organization ${itemId}`}
|
||||
id={`select-organization-${itemId}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="pf-c-data-list__cell">
|
||||
<span id="check-action-item1">
|
||||
<Link
|
||||
to={{
|
||||
pathname: detailUrl,
|
||||
state: { breadcrumb: [parentBreadcrumb, { name, url: detailUrl }] }
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</Link>
|
||||
</span>
|
||||
</div>
|
||||
<div className="pf-c-data-list__cell">
|
||||
<Link to={`${detailUrl}?tab=users`}>
|
||||
Users
|
||||
</Link>
|
||||
<Badge isRead>
|
||||
{' '}
|
||||
{userCount}
|
||||
{' '}
|
||||
</Badge>
|
||||
<Link to={`${detailUrl}?tab=teams`}>
|
||||
Teams
|
||||
</Link>
|
||||
<Badge isRead>
|
||||
{' '}
|
||||
{teamCount}
|
||||
{' '}
|
||||
</Badge>
|
||||
<Link to={`${detailUrl}?tab=admins`}>
|
||||
Admins
|
||||
</Link>
|
||||
<Badge isRead>
|
||||
{' '}
|
||||
{adminCount}
|
||||
{' '}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="pf-c-data-list__cell" />
|
||||
</li>
|
||||
);
|
||||
16
src/pages/Organizations/index.jsx
Normal file
16
src/pages/Organizations/index.jsx
Normal 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;
|
||||
18
src/pages/Organizations/tabs.scss
Normal file
18
src/pages/Organizations/tabs.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
17
src/pages/Organizations/utils.jsx
Normal file
17
src/pages/Organizations/utils.jsx
Normal 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;
|
||||
21
src/pages/Organizations/views/Organization.add.jsx
Normal file
21
src/pages/Organizations/views/Organization.add.jsx
Normal 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;
|
||||
120
src/pages/Organizations/views/Organization.view.jsx
Normal file
120
src/pages/Organizations/views/Organization.view.jsx
Normal 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;
|
||||
@@ -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 (
|
||||
<Fragment>
|
||||
@@ -193,6 +193,7 @@ class Organizations extends Component {
|
||||
</PageSection>
|
||||
<PageSection variant={medium}>
|
||||
<DataListToolbar
|
||||
addUrl={`${match.url}/add`}
|
||||
isAllSelected={selected.length === results.length}
|
||||
sortedColumnKey={sortedColumnKey}
|
||||
sortOrder={sortOrder}
|
||||
@@ -207,6 +208,8 @@ class Organizations extends Component {
|
||||
key={o.id}
|
||||
itemId={o.id}
|
||||
name={o.name}
|
||||
detailUrl={`${match.url}/${o.id}`}
|
||||
parentBreadcrumb={parentBreadcrumb}
|
||||
userCount={o.summary_fields.related_field_counts.users}
|
||||
teamCount={o.summary_fields.related_field_counts.teams}
|
||||
adminCount={o.summary_fields.related_field_counts.admins}
|
||||
Reference in New Issue
Block a user