mirror of
https://github.com/ansible/awx.git
synced 2026-02-15 02:00:01 -03:30
Merge pull request #5786 from marshmalien/4951-org-team-links
Fix organization team links Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
@@ -30,7 +30,9 @@ function OrganizationTeams({ id, i18n }) {
|
|||||||
data: { count = 0, results = [] },
|
data: { count = 0, results = [] },
|
||||||
} = await OrganizationsAPI.readTeams(id, params);
|
} = await OrganizationsAPI.readTeams(id, params);
|
||||||
setItemCount(count);
|
setItemCount(count);
|
||||||
setTeams(results);
|
setTeams(
|
||||||
|
results.map(team => ({ ...team, url: `/teams/${team.id}/details` }))
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setContentError(error);
|
setContentError(error);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import { act } from 'react-dom/test-utils';
|
import { act } from 'react-dom/test-utils';
|
||||||
|
|
||||||
import { OrganizationsAPI } from '@api';
|
import { OrganizationsAPI } from '@api';
|
||||||
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
|
||||||
import { sleep } from '@testUtils/testUtils';
|
import { sleep } from '@testUtils/testUtils';
|
||||||
|
|
||||||
import OrganizationTeams from './OrganizationTeams';
|
import OrganizationTeams from './OrganizationTeams';
|
||||||
@@ -65,7 +65,9 @@ describe('<OrganizationTeams />', () => {
|
|||||||
wrapper.update();
|
wrapper.update();
|
||||||
|
|
||||||
const list = wrapper.find('PaginatedDataList');
|
const list = wrapper.find('PaginatedDataList');
|
||||||
expect(list.prop('items')).toEqual(listData.data.results);
|
list.find('DataListCell').forEach((el, index) => {
|
||||||
|
expect(el.text()).toBe(listData.data.results[index].name);
|
||||||
|
});
|
||||||
expect(list.prop('itemCount')).toEqual(listData.data.count);
|
expect(list.prop('itemCount')).toEqual(listData.data.count);
|
||||||
expect(list.prop('qsConfig')).toEqual({
|
expect(list.prop('qsConfig')).toEqual({
|
||||||
namespace: 'team',
|
namespace: 'team',
|
||||||
@@ -78,4 +80,15 @@ describe('<OrganizationTeams />', () => {
|
|||||||
integerFields: ['page', 'page_size'],
|
integerFields: ['page', 'page_size'],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should show content error for failed instance group fetch', async () => {
|
||||||
|
OrganizationsAPI.readTeams.mockImplementationOnce(() =>
|
||||||
|
Promise.reject(new Error())
|
||||||
|
);
|
||||||
|
let wrapper;
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<OrganizationTeams id={1} />);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Switch, Route, withRouter, Redirect, Link } from 'react-router-dom';
|
import {
|
||||||
|
Link,
|
||||||
|
Redirect,
|
||||||
|
Route,
|
||||||
|
Switch,
|
||||||
|
useLocation,
|
||||||
|
useParams,
|
||||||
|
} from 'react-router-dom';
|
||||||
import { Card, PageSection } from '@patternfly/react-core';
|
import { Card, PageSection } from '@patternfly/react-core';
|
||||||
import CardCloseButton from '@components/CardCloseButton';
|
import CardCloseButton from '@components/CardCloseButton';
|
||||||
import { TabbedCardHeader } from '@components/Card';
|
import { TabbedCardHeader } from '@components/Card';
|
||||||
@@ -11,148 +18,110 @@ import TeamDetail from './TeamDetail';
|
|||||||
import TeamEdit from './TeamEdit';
|
import TeamEdit from './TeamEdit';
|
||||||
import { TeamsAPI } from '@api';
|
import { TeamsAPI } from '@api';
|
||||||
|
|
||||||
class Team extends Component {
|
function Team({ i18n, setBreadcrumb }) {
|
||||||
constructor(props) {
|
const [team, setTeam] = useState(null);
|
||||||
super(props);
|
const [contentError, setContentError] = useState(null);
|
||||||
|
const [hasContentLoading, setHasContentLoading] = useState(true);
|
||||||
|
const location = useLocation();
|
||||||
|
const { id } = useParams();
|
||||||
|
|
||||||
this.state = {
|
useEffect(() => {
|
||||||
team: null,
|
(async () => {
|
||||||
hasContentLoading: true,
|
try {
|
||||||
contentError: null,
|
const { data } = await TeamsAPI.readDetail(id);
|
||||||
isInitialized: false,
|
setBreadcrumb(data);
|
||||||
};
|
setTeam(data);
|
||||||
this.loadTeam = this.loadTeam.bind(this);
|
} catch (error) {
|
||||||
|
setContentError(error);
|
||||||
|
} finally {
|
||||||
|
setHasContentLoading(false);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [id, setBreadcrumb]);
|
||||||
|
|
||||||
|
const tabsArray = [
|
||||||
|
{ name: i18n._(t`Details`), link: `/teams/${id}/details`, id: 0 },
|
||||||
|
{ name: i18n._(t`Users`), link: `/teams/${id}/users`, id: 1 },
|
||||||
|
{ name: i18n._(t`Access`), link: `/teams/${id}/access`, id: 2 },
|
||||||
|
];
|
||||||
|
|
||||||
|
let cardHeader = (
|
||||||
|
<TabbedCardHeader>
|
||||||
|
<RoutedTabs tabsArray={tabsArray} />
|
||||||
|
<CardCloseButton linkTo="/teams" />
|
||||||
|
</TabbedCardHeader>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (location.pathname.endsWith('edit')) {
|
||||||
|
cardHeader = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async componentDidMount() {
|
if (!hasContentLoading && contentError) {
|
||||||
await this.loadTeam();
|
|
||||||
this.setState({ isInitialized: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
async componentDidUpdate(prevProps) {
|
|
||||||
const { location, match } = this.props;
|
|
||||||
const url = `/teams/${match.params.id}/`;
|
|
||||||
|
|
||||||
if (
|
|
||||||
prevProps.location.pathname.startsWith(url) &&
|
|
||||||
prevProps.location !== location &&
|
|
||||||
location.pathname === `${url}details`
|
|
||||||
) {
|
|
||||||
await this.loadTeam();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadTeam() {
|
|
||||||
const { match, setBreadcrumb } = this.props;
|
|
||||||
const id = parseInt(match.params.id, 10);
|
|
||||||
|
|
||||||
this.setState({ contentError: null, hasContentLoading: true });
|
|
||||||
try {
|
|
||||||
const { data } = await TeamsAPI.readDetail(id);
|
|
||||||
setBreadcrumb(data);
|
|
||||||
this.setState({ team: data });
|
|
||||||
} catch (err) {
|
|
||||||
this.setState({ contentError: err });
|
|
||||||
} finally {
|
|
||||||
this.setState({ hasContentLoading: false });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { location, match, i18n } = this.props;
|
|
||||||
|
|
||||||
const { team, contentError, hasContentLoading, isInitialized } = this.state;
|
|
||||||
|
|
||||||
const tabsArray = [
|
|
||||||
{ name: i18n._(t`Details`), link: `${match.url}/details`, id: 0 },
|
|
||||||
{ name: i18n._(t`Users`), link: `${match.url}/users`, id: 1 },
|
|
||||||
{ name: i18n._(t`Access`), link: `${match.url}/access`, id: 2 },
|
|
||||||
];
|
|
||||||
|
|
||||||
let cardHeader = (
|
|
||||||
<TabbedCardHeader>
|
|
||||||
<RoutedTabs tabsArray={tabsArray} />
|
|
||||||
<CardCloseButton linkTo="/teams" />
|
|
||||||
</TabbedCardHeader>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!isInitialized) {
|
|
||||||
cardHeader = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (location.pathname.endsWith('edit')) {
|
|
||||||
cardHeader = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasContentLoading && contentError) {
|
|
||||||
return (
|
|
||||||
<PageSection>
|
|
||||||
<Card className="awx-c-card">
|
|
||||||
<ContentError error={contentError}>
|
|
||||||
{contentError.response.status === 404 && (
|
|
||||||
<span>
|
|
||||||
{i18n._(`Team not found.`)}{' '}
|
|
||||||
<Link to="/teams">{i18n._(`View all Teams.`)}</Link>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</ContentError>
|
|
||||||
</Card>
|
|
||||||
</PageSection>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageSection>
|
<PageSection>
|
||||||
<Card className="awx-c-card">
|
<Card className="awx-c-card">
|
||||||
{cardHeader}
|
<ContentError error={contentError}>
|
||||||
<Switch>
|
{contentError.response.status === 404 && (
|
||||||
<Redirect from="/teams/:id" to="/teams/:id/details" exact />
|
<span>
|
||||||
{team && (
|
{i18n._(`Team not found.`)}{' '}
|
||||||
<Route
|
<Link to="/teams">{i18n._(`View all Teams.`)}</Link>
|
||||||
path="/teams/:id/edit"
|
</span>
|
||||||
render={() => <TeamEdit team={team} />}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
{team && (
|
</ContentError>
|
||||||
<Route
|
|
||||||
path="/teams/:id/details"
|
|
||||||
render={() => <TeamDetail team={team} />}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{team && (
|
|
||||||
<Route
|
|
||||||
path="/teams/:id/users"
|
|
||||||
render={() => <span>Coming soon :)</span>}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{team && (
|
|
||||||
<Route
|
|
||||||
path="/teams/:id/access"
|
|
||||||
render={() => <span>Coming soon :)</span>}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Route
|
|
||||||
key="not-found"
|
|
||||||
path="*"
|
|
||||||
render={() =>
|
|
||||||
!hasContentLoading && (
|
|
||||||
<ContentError isNotFound>
|
|
||||||
{match.params.id && (
|
|
||||||
<Link to={`/teams/${match.params.id}/details`}>
|
|
||||||
{i18n._(`View Team Details`)}
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</ContentError>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
,
|
|
||||||
</Switch>
|
|
||||||
</Card>
|
</Card>
|
||||||
</PageSection>
|
</PageSection>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageSection>
|
||||||
|
<Card className="awx-c-card">
|
||||||
|
{cardHeader}
|
||||||
|
<Switch>
|
||||||
|
<Redirect from="/teams/:id" to="/teams/:id/details" exact />
|
||||||
|
{team && (
|
||||||
|
<Route path="/teams/:id/details">
|
||||||
|
<TeamDetail team={team} />
|
||||||
|
</Route>
|
||||||
|
)}
|
||||||
|
{team && (
|
||||||
|
<Route
|
||||||
|
path="/teams/:id/edit"
|
||||||
|
render={() => <TeamEdit team={team} />}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{team && (
|
||||||
|
<Route
|
||||||
|
path="/teams/:id/users"
|
||||||
|
render={() => <span>Coming soon :)</span>}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{team && (
|
||||||
|
<Route
|
||||||
|
path="/teams/:id/access"
|
||||||
|
render={() => <span>Coming soon :)</span>}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Route
|
||||||
|
key="not-found"
|
||||||
|
path="*"
|
||||||
|
render={() =>
|
||||||
|
!hasContentLoading && (
|
||||||
|
<ContentError isNotFound>
|
||||||
|
{id && (
|
||||||
|
<Link to={`/teams/${id}/details`}>
|
||||||
|
{i18n._(`View Team Details`)}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</ContentError>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Switch>
|
||||||
|
</Card>
|
||||||
|
</PageSection>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withI18n()(withRouter(Team));
|
export default withI18n()(Team);
|
||||||
export { Team as _Team };
|
export { Team as _Team };
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
import { createMemoryHistory } from 'history';
|
import { createMemoryHistory } from 'history';
|
||||||
import { TeamsAPI } from '@api';
|
import { TeamsAPI } from '@api';
|
||||||
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
|
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
|
||||||
@@ -34,34 +35,46 @@ async function getTeams() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('<Team />', () => {
|
describe('<Team />', () => {
|
||||||
test('initially renders succesfully', () => {
|
let wrapper;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
TeamsAPI.readDetail.mockResolvedValue({ data: mockTeam });
|
TeamsAPI.readDetail.mockResolvedValue({ data: mockTeam });
|
||||||
TeamsAPI.read.mockImplementation(getTeams);
|
TeamsAPI.read.mockImplementation(getTeams);
|
||||||
mountWithContexts(<Team setBreadcrumb={() => {}} me={mockMe} />);
|
});
|
||||||
|
|
||||||
|
test('initially renders succesfully', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<Team setBreadcrumb={() => {}} me={mockMe} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
expect(wrapper.find('Team').length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should show content error when user attempts to navigate to erroneous route', async () => {
|
test('should show content error when user attempts to navigate to erroneous route', async () => {
|
||||||
const history = createMemoryHistory({
|
const history = createMemoryHistory({
|
||||||
initialEntries: ['/teams/1/foobar'],
|
initialEntries: ['/teams/1/foobar'],
|
||||||
});
|
});
|
||||||
const wrapper = mountWithContexts(
|
await act(async () => {
|
||||||
<Team setBreadcrumb={() => {}} me={mockMe} />,
|
wrapper = mountWithContexts(
|
||||||
{
|
<Team setBreadcrumb={() => {}} me={mockMe} />,
|
||||||
context: {
|
{
|
||||||
router: {
|
context: {
|
||||||
history,
|
router: {
|
||||||
route: {
|
history,
|
||||||
location: history.location,
|
route: {
|
||||||
match: {
|
location: history.location,
|
||||||
params: { id: 1 },
|
match: {
|
||||||
url: '/teams/1/foobar',
|
params: { id: 1 },
|
||||||
path: '/teams/1/foobar',
|
url: '/teams/1/foobar',
|
||||||
|
path: '/teams/1/foobar',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
}
|
);
|
||||||
);
|
});
|
||||||
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
|
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
export { default as TeamList } from './TeamList';
|
export { default } from './TeamList';
|
||||||
export { default as TeamListItem } from './TeamListItem';
|
export { default as TeamListItem } from './TeamListItem';
|
||||||
|
|||||||
@@ -1,79 +1,58 @@
|
|||||||
import React, { Component, Fragment } from 'react';
|
import React, { useState, useCallback } from 'react';
|
||||||
import { Route, withRouter, Switch } from 'react-router-dom';
|
import { Route, Switch } from 'react-router-dom';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
|
|
||||||
import { Config } from '@contexts/Config';
|
import { Config } from '@contexts/Config';
|
||||||
import Breadcrumbs from '@components/Breadcrumbs/Breadcrumbs';
|
import Breadcrumbs from '@components/Breadcrumbs';
|
||||||
|
import TeamList from './TeamList';
|
||||||
import TeamsList from './TeamList/TeamList';
|
import TeamAdd from './TeamAdd';
|
||||||
import TeamAdd from './TeamAdd/TeamAdd';
|
|
||||||
import Team from './Team';
|
import Team from './Team';
|
||||||
|
|
||||||
class Teams extends Component {
|
function Teams({ i18n }) {
|
||||||
constructor(props) {
|
const [breadcrumbConfig, setBreadcrumbConfig] = useState({
|
||||||
super(props);
|
'/teams': i18n._(t`Teams`),
|
||||||
|
'/teams/add': i18n._(t`Create New Team`),
|
||||||
|
});
|
||||||
|
|
||||||
const { i18n } = props;
|
const buildBreadcrumbConfig = useCallback(
|
||||||
|
team => {
|
||||||
|
if (!team) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.state = {
|
setBreadcrumbConfig({
|
||||||
breadcrumbConfig: {
|
|
||||||
'/teams': i18n._(t`Teams`),
|
'/teams': i18n._(t`Teams`),
|
||||||
'/teams/add': i18n._(t`Create New Team`),
|
'/teams/add': i18n._(t`Create New Team`),
|
||||||
},
|
[`/teams/${team.id}`]: `${team.name}`,
|
||||||
};
|
[`/teams/${team.id}/edit`]: i18n._(t`Edit Details`),
|
||||||
}
|
[`/teams/${team.id}/details`]: i18n._(t`Details`),
|
||||||
|
[`/teams/${team.id}/users`]: i18n._(t`Users`),
|
||||||
|
[`/teams/${team.id}/access`]: i18n._(t`Access`),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[i18n]
|
||||||
|
);
|
||||||
|
|
||||||
setBreadcrumbConfig = team => {
|
return (
|
||||||
const { i18n } = this.props;
|
<>
|
||||||
|
<Breadcrumbs breadcrumbConfig={breadcrumbConfig} />
|
||||||
if (!team) {
|
<Switch>
|
||||||
return;
|
<Route path="/teams/add">
|
||||||
}
|
<TeamAdd />
|
||||||
|
</Route>
|
||||||
const breadcrumbConfig = {
|
<Route path="/teams/:id">
|
||||||
'/teams': i18n._(t`Teams`),
|
<Team setBreadcrumb={buildBreadcrumbConfig} />
|
||||||
'/teams/add': i18n._(t`Create New Team`),
|
</Route>
|
||||||
[`/teams/${team.id}`]: `${team.name}`,
|
<Route path="/teams">
|
||||||
[`/teams/${team.id}/edit`]: i18n._(t`Edit Details`),
|
<Config>
|
||||||
[`/teams/${team.id}/details`]: i18n._(t`Details`),
|
{({ me }) => <TeamList path="/teams" me={me || {}} />}
|
||||||
[`/teams/${team.id}/users`]: i18n._(t`Users`),
|
</Config>
|
||||||
[`/teams/${team.id}/access`]: i18n._(t`Access`),
|
</Route>
|
||||||
};
|
</Switch>
|
||||||
|
</>
|
||||||
this.setState({ breadcrumbConfig });
|
);
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { match, history, location } = this.props;
|
|
||||||
const { breadcrumbConfig } = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<Breadcrumbs breadcrumbConfig={breadcrumbConfig} />
|
|
||||||
<Switch>
|
|
||||||
<Route path={`${match.path}/add`} render={() => <TeamAdd />} />
|
|
||||||
<Route
|
|
||||||
path={`${match.path}/:id`}
|
|
||||||
render={() => (
|
|
||||||
<Config>
|
|
||||||
{({ me }) => (
|
|
||||||
<Team
|
|
||||||
history={history}
|
|
||||||
location={location}
|
|
||||||
setBreadcrumb={this.setBreadcrumbConfig}
|
|
||||||
me={me || {}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Config>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Route path={`${match.path}`} render={() => <TeamsList />} />
|
|
||||||
</Switch>
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Teams as _Teams };
|
export { Teams as _Teams };
|
||||||
export default withI18n()(withRouter(Teams));
|
export default withI18n()(Teams);
|
||||||
|
|||||||
Reference in New Issue
Block a user