mirror of
https://github.com/ansible/awx.git
synced 2026-05-08 09:57:35 -02:30
Update breadcrumb and fetch new hosts when url changes
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { Card, CardHeader, PageSection } from '@patternfly/react-core';
|
import { Card, CardHeader, PageSection } from '@patternfly/react-core';
|
||||||
@@ -16,186 +16,152 @@ import InventorySources from './InventorySources';
|
|||||||
import { InventoriesAPI } from '@api';
|
import { InventoriesAPI } from '@api';
|
||||||
import InventoryEdit from './InventoryEdit';
|
import InventoryEdit from './InventoryEdit';
|
||||||
|
|
||||||
class Inventory extends Component {
|
function Inventory({ history, i18n, location, match, setBreadcrumb }) {
|
||||||
constructor(props) {
|
const [contentError, setContentError] = useState(null);
|
||||||
super(props);
|
const [hasContentLoading, setHasContentLoading] = useState(true);
|
||||||
|
const [inventory, setInventory] = useState(null);
|
||||||
|
|
||||||
this.state = {
|
useEffect(() => {
|
||||||
contentError: null,
|
async function fetchData() {
|
||||||
hasContentLoading: true,
|
try {
|
||||||
inventory: null,
|
const { data } = await InventoriesAPI.readDetail(match.params.id);
|
||||||
};
|
setBreadcrumb(data);
|
||||||
this.loadInventory = this.loadInventory.bind(this);
|
setInventory(data);
|
||||||
}
|
} catch (error) {
|
||||||
|
setContentError(error);
|
||||||
async componentDidMount() {
|
} finally {
|
||||||
await this.loadInventory();
|
setHasContentLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async componentDidUpdate(prevProps) {
|
|
||||||
const { location, match } = this.props;
|
|
||||||
const url = `/inventories/inventory/${match.params.id}/`;
|
|
||||||
|
|
||||||
if (
|
|
||||||
prevProps.location.pathname.startsWith(url) &&
|
|
||||||
prevProps.location !== location &&
|
|
||||||
location.pathname === `${url}details`
|
|
||||||
) {
|
|
||||||
await this.loadInventory();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadInventory() {
|
|
||||||
const { setBreadcrumb, match } = this.props;
|
|
||||||
const { id } = match.params;
|
|
||||||
|
|
||||||
this.setState({ contentError: null, hasContentLoading: true });
|
|
||||||
try {
|
|
||||||
const { data } = await InventoriesAPI.readDetail(id);
|
|
||||||
setBreadcrumb(data);
|
|
||||||
this.setState({ inventory: data });
|
|
||||||
} catch (err) {
|
|
||||||
this.setState({ contentError: err });
|
|
||||||
} finally {
|
|
||||||
this.setState({ hasContentLoading: false });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { history, i18n, location, match } = this.props;
|
|
||||||
const { contentError, hasContentLoading, inventory } = this.state;
|
|
||||||
|
|
||||||
const tabsArray = [
|
|
||||||
{ name: i18n._(t`Details`), link: `${match.url}/details`, id: 0 },
|
|
||||||
{ name: i18n._(t`Access`), link: `${match.url}/access`, id: 1 },
|
|
||||||
{ name: i18n._(t`Groups`), link: `${match.url}/groups`, id: 2 },
|
|
||||||
{ name: i18n._(t`Hosts`), link: `${match.url}/hosts`, id: 3 },
|
|
||||||
{ name: i18n._(t`Sources`), link: `${match.url}/sources`, id: 4 },
|
|
||||||
{
|
|
||||||
name: i18n._(t`Completed Jobs`),
|
|
||||||
link: `${match.url}/completed_jobs`,
|
|
||||||
id: 5,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
let cardHeader = hasContentLoading ? null : (
|
|
||||||
<CardHeader style={{ padding: 0 }}>
|
|
||||||
<RoutedTabs history={history} tabsArray={tabsArray} />
|
|
||||||
<CardCloseButton linkTo="/inventories" />
|
|
||||||
</CardHeader>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
location.pathname.endsWith('edit') ||
|
|
||||||
location.pathname.endsWith('add')
|
|
||||||
) {
|
|
||||||
cardHeader = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasContentLoading && contentError) {
|
fetchData();
|
||||||
return (
|
}, [match.params.id, setBreadcrumb]);
|
||||||
<PageSection>
|
|
||||||
<Card className="awx-c-card">
|
|
||||||
<ContentError error={contentError}>
|
|
||||||
{contentError.response.status === 404 && (
|
|
||||||
<span>
|
|
||||||
{i18n._(`Inventory not found.`)}{' '}
|
|
||||||
<Link to="/inventories">
|
|
||||||
{i18n._(`View all Inventories.`)}
|
|
||||||
</Link>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</ContentError>
|
|
||||||
</Card>
|
|
||||||
</PageSection>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const tabsArray = [
|
||||||
|
{ name: i18n._(t`Details`), link: `${match.url}/details`, id: 0 },
|
||||||
|
{ name: i18n._(t`Access`), link: `${match.url}/access`, id: 1 },
|
||||||
|
{ name: i18n._(t`Groups`), link: `${match.url}/groups`, id: 2 },
|
||||||
|
{ name: i18n._(t`Hosts`), link: `${match.url}/hosts`, id: 3 },
|
||||||
|
{ name: i18n._(t`Sources`), link: `${match.url}/sources`, id: 4 },
|
||||||
|
{
|
||||||
|
name: i18n._(t`Completed Jobs`),
|
||||||
|
link: `${match.url}/completed_jobs`,
|
||||||
|
id: 5,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let cardHeader = hasContentLoading ? null : (
|
||||||
|
<CardHeader style={{ padding: 0 }}>
|
||||||
|
<RoutedTabs history={history} tabsArray={tabsArray} />
|
||||||
|
<CardCloseButton linkTo="/inventories" />
|
||||||
|
</CardHeader>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (location.pathname.endsWith('edit') || location.pathname.endsWith('add')) {
|
||||||
|
cardHeader = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasContentLoading && contentError) {
|
||||||
return (
|
return (
|
||||||
<PageSection>
|
<PageSection>
|
||||||
<Card className="awx-c-card">
|
<Card className="awx-c-card">
|
||||||
{cardHeader}
|
<ContentError error={contentError}>
|
||||||
<Switch>
|
{contentError.response.status === 404 && (
|
||||||
<Redirect
|
<span>
|
||||||
from="/inventories/inventory/:id"
|
{i18n._(`Inventory not found.`)}{' '}
|
||||||
to="/inventories/inventory/:id/details"
|
<Link to="/inventories">{i18n._(`View all Inventories.`)}</Link>
|
||||||
exact
|
</span>
|
||||||
/>
|
)}
|
||||||
{inventory && [
|
</ContentError>
|
||||||
<Route
|
|
||||||
key="details"
|
|
||||||
path="/inventories/inventory/:id/details"
|
|
||||||
render={() => (
|
|
||||||
<InventoryDetail
|
|
||||||
match={match}
|
|
||||||
hasInventoryLoading={hasContentLoading}
|
|
||||||
inventory={inventory}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>,
|
|
||||||
<Route
|
|
||||||
key="edit"
|
|
||||||
path="/inventories/inventory/:id/edit"
|
|
||||||
render={() => <InventoryEdit inventory={inventory} />}
|
|
||||||
/>,
|
|
||||||
<Route
|
|
||||||
key="host-add"
|
|
||||||
path="/inventories/inventory/:id/hosts/add"
|
|
||||||
render={() => <InventoryHostAdd />}
|
|
||||||
/>,
|
|
||||||
<Route
|
|
||||||
key="access"
|
|
||||||
path="/inventories/inventory/:id/access"
|
|
||||||
render={() => (
|
|
||||||
<ResourceAccessList
|
|
||||||
resource={inventory}
|
|
||||||
apiModel={InventoriesAPI}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>,
|
|
||||||
<Route
|
|
||||||
key="groups"
|
|
||||||
path="/inventories/inventory/:id/groups"
|
|
||||||
render={() => <InventoryGroups inventory={inventory} />}
|
|
||||||
/>,
|
|
||||||
<Route
|
|
||||||
key="hosts"
|
|
||||||
path="/inventories/inventory/:id/hosts"
|
|
||||||
render={() => <InventoryHosts inventory={inventory} />}
|
|
||||||
/>,
|
|
||||||
<Route
|
|
||||||
key="sources"
|
|
||||||
path="/inventories/inventory/:id/sources"
|
|
||||||
render={() => <InventorySources inventory={inventory} />}
|
|
||||||
/>,
|
|
||||||
<Route
|
|
||||||
key="completed_jobs"
|
|
||||||
path="/inventories/inventory/:id/completed_jobs"
|
|
||||||
render={() => <InventoryCompletedJobs inventory={inventory} />}
|
|
||||||
/>,
|
|
||||||
<Route
|
|
||||||
key="not-found"
|
|
||||||
path="*"
|
|
||||||
render={() =>
|
|
||||||
!hasContentLoading && (
|
|
||||||
<ContentError isNotFound>
|
|
||||||
{match.params.id && (
|
|
||||||
<Link
|
|
||||||
to={`/inventories/inventory/${match.params.id}/details`}
|
|
||||||
>
|
|
||||||
{i18n._(`View Inventory Details`)}
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</ContentError>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>,
|
|
||||||
]}
|
|
||||||
</Switch>
|
|
||||||
</Card>
|
</Card>
|
||||||
</PageSection>
|
</PageSection>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageSection>
|
||||||
|
<Card className="awx-c-card">
|
||||||
|
{cardHeader}
|
||||||
|
<Switch>
|
||||||
|
<Redirect
|
||||||
|
from="/inventories/inventory/:id"
|
||||||
|
to="/inventories/inventory/:id/details"
|
||||||
|
exact
|
||||||
|
/>
|
||||||
|
{inventory && [
|
||||||
|
<Route
|
||||||
|
key="details"
|
||||||
|
path="/inventories/inventory/:id/details"
|
||||||
|
render={() => (
|
||||||
|
<InventoryDetail
|
||||||
|
match={match}
|
||||||
|
hasInventoryLoading={hasContentLoading}
|
||||||
|
inventory={inventory}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>,
|
||||||
|
<Route
|
||||||
|
key="edit"
|
||||||
|
path="/inventories/inventory/:id/edit"
|
||||||
|
render={() => <InventoryEdit inventory={inventory} />}
|
||||||
|
/>,
|
||||||
|
<Route
|
||||||
|
key="host-add"
|
||||||
|
path="/inventories/inventory/:id/hosts/add"
|
||||||
|
render={() => <InventoryHostAdd />}
|
||||||
|
/>,
|
||||||
|
<Route
|
||||||
|
key="access"
|
||||||
|
path="/inventories/inventory/:id/access"
|
||||||
|
render={() => (
|
||||||
|
<ResourceAccessList
|
||||||
|
resource={inventory}
|
||||||
|
apiModel={InventoriesAPI}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>,
|
||||||
|
<Route
|
||||||
|
key="groups"
|
||||||
|
path="/inventories/inventory/:id/groups"
|
||||||
|
render={() => <InventoryGroups inventory={inventory} />}
|
||||||
|
/>,
|
||||||
|
<Route
|
||||||
|
key="hosts"
|
||||||
|
path="/inventories/inventory/:id/hosts"
|
||||||
|
render={() => <InventoryHosts />}
|
||||||
|
/>,
|
||||||
|
<Route
|
||||||
|
key="sources"
|
||||||
|
path="/inventories/inventory/:id/sources"
|
||||||
|
render={() => <InventorySources inventory={inventory} />}
|
||||||
|
/>,
|
||||||
|
<Route
|
||||||
|
key="completed_jobs"
|
||||||
|
path="/inventories/inventory/:id/completed_jobs"
|
||||||
|
render={() => <InventoryCompletedJobs inventory={inventory} />}
|
||||||
|
/>,
|
||||||
|
<Route
|
||||||
|
key="not-found"
|
||||||
|
path="*"
|
||||||
|
render={() =>
|
||||||
|
!hasContentLoading && (
|
||||||
|
<ContentError isNotFound>
|
||||||
|
{match.params.id && (
|
||||||
|
<Link
|
||||||
|
to={`/inventories/inventory/${match.params.id}/details`}
|
||||||
|
>
|
||||||
|
{i18n._(`View Inventory Details`)}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</ContentError>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>,
|
||||||
|
]}
|
||||||
|
</Switch>
|
||||||
|
</Card>
|
||||||
|
</PageSection>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Inventory as _Inventory };
|
export { Inventory as _Inventory };
|
||||||
|
|||||||
@@ -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 { InventoriesAPI } from '@api';
|
import { InventoriesAPI } from '@api';
|
||||||
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
|
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
|
||||||
@@ -12,41 +13,38 @@ InventoriesAPI.readDetail.mockResolvedValue({
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('<Inventory />', () => {
|
describe('<Inventory />', () => {
|
||||||
test('initially renders succesfully', async done => {
|
let wrapper;
|
||||||
const wrapper = mountWithContexts(
|
|
||||||
<Inventory setBreadcrumb={() => {}} match={{ params: { id: 1 } }} />
|
test('initially renders succesfully', async () => {
|
||||||
);
|
await act(async () => {
|
||||||
await waitForElement(
|
wrapper = mountWithContexts(
|
||||||
wrapper,
|
<Inventory setBreadcrumb={() => {}} match={{ params: { id: 1 } }} />
|
||||||
'Inventory',
|
);
|
||||||
el => el.state('hasContentLoading') === true
|
});
|
||||||
);
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
await waitForElement(
|
|
||||||
wrapper,
|
|
||||||
'Inventory',
|
|
||||||
el => el.state('hasContentLoading') === false
|
|
||||||
);
|
|
||||||
await waitForElement(wrapper, '.pf-c-tabs__item', el => el.length === 6);
|
await waitForElement(wrapper, '.pf-c-tabs__item', el => el.length === 6);
|
||||||
done();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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: ['/inventories/inventory/1/foobar'],
|
initialEntries: ['/inventories/inventory/1/foobar'],
|
||||||
});
|
});
|
||||||
const wrapper = mountWithContexts(<Inventory setBreadcrumb={() => {}} />, {
|
await act(async () => {
|
||||||
context: {
|
wrapper = mountWithContexts(<Inventory setBreadcrumb={() => {}} />, {
|
||||||
router: {
|
context: {
|
||||||
history,
|
router: {
|
||||||
route: {
|
history,
|
||||||
location: history.location,
|
route: {
|
||||||
match: {
|
location: history.location,
|
||||||
params: { id: 1 },
|
match: {
|
||||||
url: '/inventories/inventory/1/foobar',
|
params: { id: 1 },
|
||||||
path: '/inventories/inventory/1/foobar',
|
url: '/inventories/inventory/1/foobar',
|
||||||
|
path: '/inventories/inventory/1/foobar',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
});
|
||||||
});
|
});
|
||||||
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
|
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const QS_CONFIG = getQSConfig('host', {
|
|||||||
order_by: 'name',
|
order_by: 'name',
|
||||||
});
|
});
|
||||||
|
|
||||||
function InventoryHosts({ i18n, location, match, inventory }) {
|
function InventoryHosts({ i18n, location, match }) {
|
||||||
const [actions, setActions] = useState(null);
|
const [actions, setActions] = useState(null);
|
||||||
const [contentError, setContentError] = useState(null);
|
const [contentError, setContentError] = useState(null);
|
||||||
const [deletionError, setDeletionError] = useState(null);
|
const [deletionError, setDeletionError] = useState(null);
|
||||||
@@ -47,7 +47,7 @@ function InventoryHosts({ i18n, location, match, inventory }) {
|
|||||||
data: { actions: optionActions },
|
data: { actions: optionActions },
|
||||||
},
|
},
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
fetchHosts(inventory.id, location.search),
|
fetchHosts(match.params.id, location.search),
|
||||||
InventoriesAPI.readOptions(),
|
InventoriesAPI.readOptions(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ function InventoryHosts({ i18n, location, match, inventory }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fetchData();
|
fetchData();
|
||||||
}, [inventory, location]);
|
}, [match.params.id, location]);
|
||||||
|
|
||||||
const handleSelectAll = isSelected => {
|
const handleSelectAll = isSelected => {
|
||||||
setSelected(isSelected ? [...hosts] : []);
|
setSelected(isSelected ? [...hosts] : []);
|
||||||
@@ -88,7 +88,7 @@ function InventoryHosts({ i18n, location, match, inventory }) {
|
|||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
data: { count, results },
|
data: { count, results },
|
||||||
} = await fetchHosts(inventory.id, location.search);
|
} = await fetchHosts(match.params.id, location.search);
|
||||||
|
|
||||||
setHosts(results);
|
setHosts(results);
|
||||||
setHostCount(count);
|
setHostCount(count);
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ describe('<InventoryHosts />', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(<InventoryHosts inventory={mockInventory} />);
|
wrapper = mountWithContexts(<InventoryHosts />);
|
||||||
});
|
});
|
||||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user