mirror of
https://github.com/ansible/awx.git
synced 2024-10-31 06:51:10 +03:00
Add smart inv detail
This commit is contained in:
parent
8ea31d8cdd
commit
b5bbfaab11
@ -64,7 +64,7 @@ function InventoryScriptLookup({
|
|||||||
fieldId="inventory-script"
|
fieldId="inventory-script"
|
||||||
helperTextInvalid={helperTextInvalid}
|
helperTextInvalid={helperTextInvalid}
|
||||||
isRequired={required}
|
isRequired={required}
|
||||||
isValid={isValid}
|
validated={isValid ? 'default' : 'error'}
|
||||||
label={i18n._(t`Inventory script`)}
|
label={i18n._(t`Inventory script`)}
|
||||||
>
|
>
|
||||||
<Lookup
|
<Lookup
|
||||||
|
@ -72,10 +72,12 @@ function HostDetail({ i18n, host }) {
|
|||||||
<HostToggle host={host} css="padding-bottom: 40px" />
|
<HostToggle host={host} css="padding-bottom: 40px" />
|
||||||
<DetailList gutter="sm">
|
<DetailList gutter="sm">
|
||||||
<Detail label={i18n._(t`Name`)} value={name} dataCy="host-name" />
|
<Detail label={i18n._(t`Name`)} value={name} dataCy="host-name" />
|
||||||
|
{recentPlaybookJobs?.length > 0 && (
|
||||||
<Detail
|
<Detail
|
||||||
label={i18n._(t`Activity`)}
|
label={i18n._(t`Activity`)}
|
||||||
value={<Sparkline jobs={recentPlaybookJobs} />}
|
value={<Sparkline jobs={recentPlaybookJobs} />}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
<Detail label={i18n._(t`Description`)} value={description} />
|
<Detail label={i18n._(t`Description`)} value={description} />
|
||||||
<Detail
|
<Detail
|
||||||
label={i18n._(t`Inventory`)}
|
label={i18n._(t`Inventory`)}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { useState, useCallback } from 'react';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Route, withRouter, Switch } from 'react-router-dom';
|
import { Route, Switch } from 'react-router-dom';
|
||||||
|
|
||||||
import { Config } from '../../contexts/Config';
|
import { Config } from '../../contexts/Config';
|
||||||
import Breadcrumbs from '../../components/Breadcrumbs/Breadcrumbs';
|
import Breadcrumbs from '../../components/Breadcrumbs/Breadcrumbs';
|
||||||
@ -11,24 +11,15 @@ import SmartInventory from './SmartInventory';
|
|||||||
import InventoryAdd from './InventoryAdd';
|
import InventoryAdd from './InventoryAdd';
|
||||||
import SmartInventoryAdd from './SmartInventoryAdd';
|
import SmartInventoryAdd from './SmartInventoryAdd';
|
||||||
|
|
||||||
class Inventories extends Component {
|
function Inventories({ i18n }) {
|
||||||
constructor(props) {
|
const [breadcrumbConfig, setBreadcrumbConfig] = useState({
|
||||||
super(props);
|
|
||||||
const { i18n } = this.props;
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
breadcrumbConfig: {
|
|
||||||
'/inventories': i18n._(t`Inventories`),
|
'/inventories': i18n._(t`Inventories`),
|
||||||
'/inventories/inventory/add': i18n._(t`Create new inventory`),
|
'/inventories/inventory/add': i18n._(t`Create new inventory`),
|
||||||
'/inventories/smart_inventory/add': i18n._(
|
'/inventories/smart_inventory/add': i18n._(t`Create new smart inventory`),
|
||||||
t`Create new smart inventory`
|
});
|
||||||
),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
setBreadCrumbConfig = (inventory, nested, schedule) => {
|
const buildBreadcrumbConfig = useCallback(
|
||||||
const { i18n } = this.props;
|
(inventory, nested, schedule) => {
|
||||||
if (!inventory) {
|
if (!inventory) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -41,10 +32,12 @@ class Inventories extends Component {
|
|||||||
const inventoryGroupsPath = `${inventoryPath}/groups`;
|
const inventoryGroupsPath = `${inventoryPath}/groups`;
|
||||||
const inventorySourcesPath = `${inventoryPath}/sources`;
|
const inventorySourcesPath = `${inventoryPath}/sources`;
|
||||||
|
|
||||||
const breadcrumbConfig = {
|
setBreadcrumbConfig({
|
||||||
'/inventories': i18n._(t`Inventories`),
|
'/inventories': i18n._(t`Inventories`),
|
||||||
'/inventories/inventory/add': i18n._(t`Create new inventory`),
|
'/inventories/inventory/add': i18n._(t`Create new inventory`),
|
||||||
'/inventories/smart_inventory/add': i18n._(t`Create new smart inventory`),
|
'/inventories/smart_inventory/add': i18n._(
|
||||||
|
t`Create new smart inventory`
|
||||||
|
),
|
||||||
|
|
||||||
[inventoryPath]: `${inventory.name}`,
|
[inventoryPath]: `${inventory.name}`,
|
||||||
[`${inventoryPath}/access`]: i18n._(t`Access`),
|
[`${inventoryPath}/access`]: i18n._(t`Access`),
|
||||||
@ -56,7 +49,9 @@ class Inventories extends Component {
|
|||||||
[`${inventoryHostsPath}/add`]: i18n._(t`Create new host`),
|
[`${inventoryHostsPath}/add`]: i18n._(t`Create new host`),
|
||||||
[`${inventoryHostsPath}/${nested?.id}`]: `${nested?.name}`,
|
[`${inventoryHostsPath}/${nested?.id}`]: `${nested?.name}`,
|
||||||
[`${inventoryHostsPath}/${nested?.id}/edit`]: i18n._(t`Edit details`),
|
[`${inventoryHostsPath}/${nested?.id}/edit`]: i18n._(t`Edit details`),
|
||||||
[`${inventoryHostsPath}/${nested?.id}/details`]: i18n._(t`Host Details`),
|
[`${inventoryHostsPath}/${nested?.id}/details`]: i18n._(
|
||||||
|
t`Host Details`
|
||||||
|
),
|
||||||
[`${inventoryHostsPath}/${nested?.id}/completed_jobs`]: i18n._(
|
[`${inventoryHostsPath}/${nested?.id}/completed_jobs`]: i18n._(
|
||||||
t`Completed jobs`
|
t`Completed jobs`
|
||||||
),
|
),
|
||||||
@ -80,62 +75,52 @@ class Inventories extends Component {
|
|||||||
[`${inventorySourcesPath}/${nested?.id}`]: `${nested?.name}`,
|
[`${inventorySourcesPath}/${nested?.id}`]: `${nested?.name}`,
|
||||||
[`${inventorySourcesPath}/${nested?.id}/details`]: i18n._(t`Details`),
|
[`${inventorySourcesPath}/${nested?.id}/details`]: i18n._(t`Details`),
|
||||||
[`${inventorySourcesPath}/${nested?.id}/edit`]: i18n._(t`Edit details`),
|
[`${inventorySourcesPath}/${nested?.id}/edit`]: i18n._(t`Edit details`),
|
||||||
[`${inventorySourcesPath}/${nested?.id}/schedules`]: i18n._(t`Schedules`),
|
[`${inventorySourcesPath}/${nested?.id}/schedules`]: i18n._(
|
||||||
|
t`Schedules`
|
||||||
|
),
|
||||||
[`${inventorySourcesPath}/${nested?.id}/schedules/${schedule?.id}`]: `${schedule?.name}`,
|
[`${inventorySourcesPath}/${nested?.id}/schedules/${schedule?.id}`]: `${schedule?.name}`,
|
||||||
[`${inventorySourcesPath}/${nested?.id}/schedules/${schedule?.id}/details`]: i18n._(
|
[`${inventorySourcesPath}/${nested?.id}/schedules/${schedule?.id}/details`]: i18n._(
|
||||||
t`Schedule Details`
|
t`Schedule Details`
|
||||||
),
|
),
|
||||||
};
|
});
|
||||||
this.setState({ breadcrumbConfig });
|
},
|
||||||
};
|
[i18n]
|
||||||
|
);
|
||||||
|
|
||||||
render() {
|
|
||||||
const { match, history, location } = this.props;
|
|
||||||
const { breadcrumbConfig } = this.state;
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Breadcrumbs breadcrumbConfig={breadcrumbConfig} />
|
<Breadcrumbs breadcrumbConfig={breadcrumbConfig} />
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path={`${match.path}/inventory/add`}>
|
<Route path="/inventories/inventory/add">
|
||||||
<InventoryAdd />
|
<InventoryAdd />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path={`${match.path}/smart_inventory/add`}>
|
<Route path="/inventories/smart_inventory/add">
|
||||||
<SmartInventoryAdd />
|
<SmartInventoryAdd />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path={`${match.path}/inventory/:id`}>
|
<Route path="/inventories/inventory/:id">
|
||||||
<Config>
|
<Config>
|
||||||
{({ me }) => (
|
{({ me }) => (
|
||||||
<Inventory
|
<Inventory setBreadcrumb={buildBreadcrumbConfig} me={me || {}} />
|
||||||
setBreadcrumb={this.setBreadCrumbConfig}
|
|
||||||
me={me || {}}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</Config>
|
</Config>
|
||||||
</Route>
|
</Route>
|
||||||
<Route
|
<Route path="/inventories/smart_inventory/:id">
|
||||||
path={`${match.path}/smart_inventory/:id`}
|
|
||||||
render={({ match: newRouteMatch }) => (
|
|
||||||
<Config>
|
<Config>
|
||||||
{({ me }) => (
|
{({ me }) => (
|
||||||
<SmartInventory
|
<SmartInventory
|
||||||
history={history}
|
setBreadcrumb={buildBreadcrumbConfig}
|
||||||
location={location}
|
|
||||||
setBreadcrumb={this.setBreadCrumbConfig}
|
|
||||||
me={me || {}}
|
me={me || {}}
|
||||||
match={newRouteMatch}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Config>
|
</Config>
|
||||||
)}
|
</Route>
|
||||||
/>
|
<Route path="/inventories">
|
||||||
<Route path={`${match.path}`}>
|
|
||||||
<InventoryList />
|
<InventoryList />
|
||||||
</Route>
|
</Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Inventories as _Inventories };
|
export { Inventories as _Inventories };
|
||||||
export default withI18n()(withRouter(Inventories));
|
export default withI18n()(Inventories);
|
||||||
|
@ -108,6 +108,12 @@ function Inventory({ i18n, setBreadcrumb }) {
|
|||||||
showCardHeader = false;
|
showCardHeader = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (inventory?.kind === 'smart') {
|
||||||
|
return (
|
||||||
|
<Redirect to={`/inventories/smart_inventory/${inventory.id}/details`} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageSection>
|
<PageSection>
|
||||||
<Card>
|
<Card>
|
||||||
|
@ -73,10 +73,12 @@ function InventoryHostDetail({ i18n, host }) {
|
|||||||
<HostToggle host={host} css="padding-bottom: 40px" />
|
<HostToggle host={host} css="padding-bottom: 40px" />
|
||||||
<DetailList gutter="sm">
|
<DetailList gutter="sm">
|
||||||
<Detail label={i18n._(t`Name`)} value={name} />
|
<Detail label={i18n._(t`Name`)} value={name} />
|
||||||
|
{recentPlaybookJobs?.length > 0 && (
|
||||||
<Detail
|
<Detail
|
||||||
label={i18n._(t`Activity`)}
|
label={i18n._(t`Activity`)}
|
||||||
value={<Sparkline jobs={recentPlaybookJobs} />}
|
value={<Sparkline jobs={recentPlaybookJobs} />}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
<Detail label={i18n._(t`Description`)} value={description} />
|
<Detail label={i18n._(t`Description`)} value={description} />
|
||||||
<UserDateDetail
|
<UserDateDetail
|
||||||
date={created}
|
date={created}
|
||||||
|
@ -32,6 +32,9 @@ describe('<InventoryHostDetail />', () => {
|
|||||||
assertDetail('Description', 'localhost description');
|
assertDetail('Description', 'localhost description');
|
||||||
assertDetail('Created', '10/28/2019, 9:26:54 PM');
|
assertDetail('Created', '10/28/2019, 9:26:54 PM');
|
||||||
assertDetail('Last Modified', '10/29/2019, 8:18:41 PM');
|
assertDetail('Last Modified', '10/29/2019, 8:18:41 PM');
|
||||||
|
expect(wrapper.find(`Detail[label="Activity"] Sparkline`)).toHaveLength(
|
||||||
|
1
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should show edit button for users with edit permission', () => {
|
test('should show edit button for users with edit permission', () => {
|
||||||
|
@ -1,66 +1,59 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { useCallback, useEffect } from 'react';
|
||||||
import { t } from '@lingui/macro';
|
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import {
|
||||||
|
Link,
|
||||||
|
Switch,
|
||||||
|
Route,
|
||||||
|
Redirect,
|
||||||
|
useRouteMatch,
|
||||||
|
useLocation,
|
||||||
|
} from 'react-router-dom';
|
||||||
import { CaretLeftIcon } from '@patternfly/react-icons';
|
import { CaretLeftIcon } from '@patternfly/react-icons';
|
||||||
import { Card, PageSection } from '@patternfly/react-core';
|
import { Card, PageSection } from '@patternfly/react-core';
|
||||||
import { Switch, Route, Redirect, withRouter, Link } from 'react-router-dom';
|
|
||||||
import ContentError from '../../components/ContentError';
|
import useRequest from '../../util/useRequest';
|
||||||
import JobList from '../../components/JobList';
|
|
||||||
import RoutedTabs from '../../components/RoutedTabs';
|
|
||||||
import { ResourceAccessList } from '../../components/ResourceAccessList';
|
|
||||||
import SmartInventoryDetail from './SmartInventoryDetail';
|
|
||||||
import SmartInventoryHosts from './SmartInventoryHosts';
|
|
||||||
import { InventoriesAPI } from '../../api';
|
import { InventoriesAPI } from '../../api';
|
||||||
|
|
||||||
|
import ContentError from '../../components/ContentError';
|
||||||
|
import ContentLoading from '../../components/ContentLoading';
|
||||||
|
import JobList from '../../components/JobList';
|
||||||
|
import { ResourceAccessList } from '../../components/ResourceAccessList';
|
||||||
|
import RoutedTabs from '../../components/RoutedTabs';
|
||||||
|
import SmartInventoryDetail from './SmartInventoryDetail';
|
||||||
import SmartInventoryEdit from './SmartInventoryEdit';
|
import SmartInventoryEdit from './SmartInventoryEdit';
|
||||||
|
import SmartInventoryHosts from './SmartInventoryHosts';
|
||||||
|
|
||||||
class SmartInventory extends Component {
|
function SmartInventory({ i18n, setBreadcrumb }) {
|
||||||
constructor(props) {
|
const location = useLocation();
|
||||||
super(props);
|
const match = useRouteMatch('/inventories/smart_inventory/:id');
|
||||||
|
|
||||||
this.state = {
|
const {
|
||||||
contentError: null,
|
result: { inventory },
|
||||||
hasContentLoading: true,
|
error: contentError,
|
||||||
inventory: null,
|
isLoading: hasContentLoading,
|
||||||
|
request: fetchInventory,
|
||||||
|
} = useRequest(
|
||||||
|
useCallback(async () => {
|
||||||
|
const { data } = await InventoriesAPI.readDetail(match.params.id);
|
||||||
|
return {
|
||||||
|
inventory: data,
|
||||||
};
|
};
|
||||||
this.loadSmartInventory = this.loadSmartInventory.bind(this);
|
}, [match.params.id]),
|
||||||
|
{
|
||||||
|
inventory: null,
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
async componentDidMount() {
|
useEffect(() => {
|
||||||
await this.loadSmartInventory();
|
fetchInventory();
|
||||||
|
}, [fetchInventory, location.pathname]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (inventory) {
|
||||||
|
setBreadcrumb(inventory);
|
||||||
}
|
}
|
||||||
|
}, [inventory, setBreadcrumb]);
|
||||||
async componentDidUpdate(prevProps) {
|
|
||||||
const { location, match } = this.props;
|
|
||||||
const url = `/inventories/smart_inventory/${match.params.id}/`;
|
|
||||||
|
|
||||||
if (
|
|
||||||
prevProps.location.pathname.startsWith(url) &&
|
|
||||||
prevProps.location !== location &&
|
|
||||||
location.pathname === `${url}details`
|
|
||||||
) {
|
|
||||||
await this.loadSmartInventory();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadSmartInventory() {
|
|
||||||
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 { i18n, location, match } = this.props;
|
|
||||||
const { contentError, hasContentLoading, inventory } = this.state;
|
|
||||||
|
|
||||||
const tabsArray = [
|
const tabsArray = [
|
||||||
{
|
{
|
||||||
@ -77,29 +70,31 @@ class SmartInventory extends Component {
|
|||||||
{ name: i18n._(t`Access`), link: `${match.url}/access`, id: 1 },
|
{ name: i18n._(t`Access`), link: `${match.url}/access`, id: 1 },
|
||||||
{ name: i18n._(t`Hosts`), link: `${match.url}/hosts`, id: 2 },
|
{ name: i18n._(t`Hosts`), link: `${match.url}/hosts`, id: 2 },
|
||||||
{
|
{
|
||||||
name: i18n._(t`Completed Jobs`),
|
name: i18n._(t`Completed jobs`),
|
||||||
link: `${match.url}/completed_jobs`,
|
link: `${match.url}/completed_jobs`,
|
||||||
id: 3,
|
id: 3,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
let showCardHeader = true;
|
if (hasContentLoading) {
|
||||||
|
return (
|
||||||
if (location.pathname.endsWith('edit')) {
|
<PageSection>
|
||||||
showCardHeader = false;
|
<Card>
|
||||||
|
<ContentLoading />
|
||||||
|
</Card>
|
||||||
|
</PageSection>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasContentLoading && contentError) {
|
if (contentError) {
|
||||||
return (
|
return (
|
||||||
<PageSection>
|
<PageSection>
|
||||||
<Card>
|
<Card>
|
||||||
<ContentError error={contentError}>
|
<ContentError error={contentError}>
|
||||||
{contentError.response.status === 404 && (
|
{contentError?.response?.status === 404 && (
|
||||||
<span>
|
<span>
|
||||||
{i18n._(`Inventory not found.`)}{' '}
|
{i18n._(`Smart Inventory not found.`)}{' '}
|
||||||
<Link to="/inventories">
|
<Link to="/inventories">{i18n._(`View all Inventories.`)}</Link>
|
||||||
{i18n._(`View all Inventories.`)}
|
|
||||||
</Link>
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</ContentError>
|
</ContentError>
|
||||||
@ -107,6 +102,17 @@ class SmartInventory extends Component {
|
|||||||
</PageSection>
|
</PageSection>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (inventory?.kind === '') {
|
||||||
|
return <Redirect to={`/inventories/inventory/${inventory.id}/details`} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
let showCardHeader = true;
|
||||||
|
|
||||||
|
if (location.pathname.endsWith('edit')) {
|
||||||
|
showCardHeader = false;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageSection>
|
<PageSection>
|
||||||
<Card>
|
<Card>
|
||||||
@ -123,17 +129,14 @@ class SmartInventory extends Component {
|
|||||||
path="/inventories/smart_inventory/:id/details"
|
path="/inventories/smart_inventory/:id/details"
|
||||||
>
|
>
|
||||||
<SmartInventoryDetail
|
<SmartInventoryDetail
|
||||||
hasSmartInventoryLoading={hasContentLoading}
|
isLoading={hasContentLoading}
|
||||||
inventory={inventory}
|
inventory={inventory}
|
||||||
/>
|
/>
|
||||||
</Route>,
|
</Route>,
|
||||||
<Route key="edit" path="/inventories/smart_inventory/:id/edit">
|
<Route key="edit" path="/inventories/smart_inventory/:id/edit">
|
||||||
<SmartInventoryEdit inventory={inventory} />
|
<SmartInventoryEdit inventory={inventory} />
|
||||||
</Route>,
|
</Route>,
|
||||||
<Route
|
<Route key="access" path="/inventories/smart_inventory/:id/access">
|
||||||
key="access"
|
|
||||||
path="/inventories/smart_inventory/:id/access"
|
|
||||||
>
|
|
||||||
<ResourceAccessList
|
<ResourceAccessList
|
||||||
resource={inventory}
|
resource={inventory}
|
||||||
apiModel={InventoriesAPI}
|
apiModel={InventoriesAPI}
|
||||||
@ -174,8 +177,7 @@ class SmartInventory extends Component {
|
|||||||
</Card>
|
</Card>
|
||||||
</PageSection>
|
</PageSection>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export { SmartInventory as _SmartInventory };
|
export { SmartInventory as _SmartInventory };
|
||||||
export default withI18n()(withRouter(SmartInventory));
|
export default withI18n()(SmartInventory);
|
||||||
|
@ -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 {
|
import {
|
||||||
@ -9,36 +10,51 @@ import mockSmartInventory from './shared/data.smart_inventory.json';
|
|||||||
import SmartInventory from './SmartInventory';
|
import SmartInventory from './SmartInventory';
|
||||||
|
|
||||||
jest.mock('../../api');
|
jest.mock('../../api');
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
InventoriesAPI.readDetail.mockResolvedValue({
|
...jest.requireActual('react-router-dom'),
|
||||||
data: mockSmartInventory,
|
useRouteMatch: () => ({
|
||||||
});
|
url: '/inventories/smart_inventory/1',
|
||||||
|
params: { id: 1 },
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('<SmartInventory />', () => {
|
describe('<SmartInventory />', () => {
|
||||||
test('initially renders succesfully', async done => {
|
let wrapper;
|
||||||
const wrapper = mountWithContexts(
|
|
||||||
<SmartInventory setBreadcrumb={() => {}} match={{ params: { id: 1 } }} />
|
afterEach(() => {
|
||||||
);
|
wrapper.unmount();
|
||||||
await waitForElement(
|
jest.clearAllMocks();
|
||||||
wrapper,
|
|
||||||
'SmartInventory',
|
|
||||||
el => el.state('hasContentLoading') === true
|
|
||||||
);
|
|
||||||
await waitForElement(
|
|
||||||
wrapper,
|
|
||||||
'SmartInventory',
|
|
||||||
el => el.state('hasContentLoading') === false
|
|
||||||
);
|
|
||||||
await waitForElement(wrapper, '.pf-c-tabs__item', el => el.length === 5);
|
|
||||||
done();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('initially renders succesfully', async () => {
|
||||||
|
InventoriesAPI.readDetail.mockResolvedValue({
|
||||||
|
data: mockSmartInventory,
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<SmartInventory setBreadcrumb={() => {}} />);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'SmartInventory');
|
||||||
|
await waitForElement(wrapper, '.pf-c-tabs__item', el => el.length === 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show content error when api throws an error', async () => {
|
||||||
|
const error = new Error();
|
||||||
|
error.response = { status: 404 };
|
||||||
|
InventoriesAPI.readDetail.mockRejectedValueOnce(error);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<SmartInventory setBreadcrumb={() => {}} />);
|
||||||
|
});
|
||||||
|
expect(InventoriesAPI.readDetail).toHaveBeenCalledTimes(1);
|
||||||
|
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
|
||||||
|
expect(wrapper.find('ContentError Title').text()).toEqual('Not Found');
|
||||||
|
});
|
||||||
|
|
||||||
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/smart_inventory/1/foobar'],
|
initialEntries: ['/inventories/smart_inventory/1/foobar'],
|
||||||
});
|
});
|
||||||
const wrapper = mountWithContexts(
|
await act(async () => {
|
||||||
<SmartInventory setBreadcrumb={() => {}} />,
|
wrapper = mountWithContexts(<SmartInventory setBreadcrumb={() => {}} />, {
|
||||||
{
|
|
||||||
context: {
|
context: {
|
||||||
router: {
|
router: {
|
||||||
history,
|
history,
|
||||||
@ -52,8 +68,8 @@ describe('<SmartInventory />', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
});
|
||||||
);
|
});
|
||||||
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
|
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,10 +1,193 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { useCallback, useEffect } from 'react';
|
||||||
import { CardBody } from '../../../components/Card';
|
import { Link, useHistory } from 'react-router-dom';
|
||||||
|
import { withI18n } from '@lingui/react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { shape } from 'prop-types';
|
||||||
|
import { Button, Chip, Label } from '@patternfly/react-core';
|
||||||
|
|
||||||
class SmartInventoryDetail extends Component {
|
import { Inventory } from '../../../types';
|
||||||
render() {
|
import { InventoriesAPI, UnifiedJobsAPI } from '../../../api';
|
||||||
return <CardBody>Coming soon :)</CardBody>;
|
import useRequest, { useDismissableError } from '../../../util/useRequest';
|
||||||
|
|
||||||
|
import AlertModal from '../../../components/AlertModal';
|
||||||
|
import { CardBody, CardActionsRow } from '../../../components/Card';
|
||||||
|
import ChipGroup from '../../../components/ChipGroup';
|
||||||
|
import { VariablesDetail } from '../../../components/CodeMirrorInput';
|
||||||
|
import ContentError from '../../../components/ContentError';
|
||||||
|
import ContentLoading from '../../../components/ContentLoading';
|
||||||
|
import DeleteButton from '../../../components/DeleteButton';
|
||||||
|
import {
|
||||||
|
DetailList,
|
||||||
|
Detail,
|
||||||
|
UserDateDetail,
|
||||||
|
} from '../../../components/DetailList';
|
||||||
|
import ErrorDetail from '../../../components/ErrorDetail';
|
||||||
|
import Sparkline from '../../../components/Sparkline';
|
||||||
|
|
||||||
|
function SmartInventoryDetail({ inventory, i18n }) {
|
||||||
|
const history = useHistory();
|
||||||
|
const {
|
||||||
|
created,
|
||||||
|
description,
|
||||||
|
host_filter,
|
||||||
|
id,
|
||||||
|
modified,
|
||||||
|
name,
|
||||||
|
variables,
|
||||||
|
summary_fields: {
|
||||||
|
created_by,
|
||||||
|
modified_by,
|
||||||
|
organization,
|
||||||
|
user_capabilities,
|
||||||
|
},
|
||||||
|
} = inventory;
|
||||||
|
|
||||||
|
const {
|
||||||
|
error: contentError,
|
||||||
|
isLoading: hasContentLoading,
|
||||||
|
request: fetchData,
|
||||||
|
result: { recentJobs, instanceGroups },
|
||||||
|
} = useRequest(
|
||||||
|
useCallback(async () => {
|
||||||
|
const params = {
|
||||||
|
or__job__inventory: id,
|
||||||
|
or__workflowjob__inventory: id,
|
||||||
|
order_by: '-finished',
|
||||||
|
page_size: 10,
|
||||||
|
};
|
||||||
|
const [{ data: jobData }, { data: igData }] = await Promise.all([
|
||||||
|
UnifiedJobsAPI.read(params),
|
||||||
|
InventoriesAPI.readInstanceGroups(id),
|
||||||
|
]);
|
||||||
|
return {
|
||||||
|
recentJobs: jobData.results,
|
||||||
|
instanceGroups: igData.results,
|
||||||
|
};
|
||||||
|
}, [id]),
|
||||||
|
{
|
||||||
|
recentJobs: [],
|
||||||
|
instanceGroups: [],
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData();
|
||||||
|
}, [fetchData]);
|
||||||
|
|
||||||
|
const { error: deleteError, isLoading, request: handleDelete } = useRequest(
|
||||||
|
useCallback(async () => {
|
||||||
|
await InventoriesAPI.destroy(id);
|
||||||
|
history.push(`/inventories`);
|
||||||
|
}, [id, history])
|
||||||
|
);
|
||||||
|
|
||||||
|
const { error, dismissError } = useDismissableError(deleteError);
|
||||||
|
|
||||||
|
if (hasContentLoading) {
|
||||||
|
return <ContentLoading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contentError) {
|
||||||
|
return <ContentError error={contentError} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CardBody>
|
||||||
|
<DetailList>
|
||||||
|
<Detail label={i18n._(t`Name`)} value={name} />
|
||||||
|
{recentJobs.length > 0 && (
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Activity`)}
|
||||||
|
value={<Sparkline jobs={recentJobs} />}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Detail label={i18n._(t`Description`)} value={description} />
|
||||||
|
<Detail label={i18n._(t`Type`)} value={i18n._(t`Smart inventory`)} />
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Organization`)}
|
||||||
|
value={
|
||||||
|
<Link to={`/organizations/${organization.id}/details`}>
|
||||||
|
{organization.name}
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Detail
|
||||||
|
fullWidth
|
||||||
|
label={i18n._(t`Smart host filter`)}
|
||||||
|
value={<Label variant="outline">{host_filter}</Label>}
|
||||||
|
/>
|
||||||
|
{instanceGroups.length > 0 && (
|
||||||
|
<Detail
|
||||||
|
fullWidth
|
||||||
|
label={i18n._(t`Instance groups`)}
|
||||||
|
value={
|
||||||
|
<ChipGroup numChips={5} totalChips={instanceGroups.length}>
|
||||||
|
{instanceGroups.map(ig => (
|
||||||
|
<Chip key={ig.id} isReadOnly>
|
||||||
|
{ig.name}
|
||||||
|
</Chip>
|
||||||
|
))}
|
||||||
|
</ChipGroup>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<VariablesDetail
|
||||||
|
label={i18n._(t`Variables`)}
|
||||||
|
value={variables}
|
||||||
|
rows={4}
|
||||||
|
/>
|
||||||
|
<UserDateDetail
|
||||||
|
label={i18n._(t`Created`)}
|
||||||
|
date={created}
|
||||||
|
user={created_by}
|
||||||
|
/>
|
||||||
|
<UserDateDetail
|
||||||
|
label={i18n._(t`Last modified`)}
|
||||||
|
date={modified}
|
||||||
|
user={modified_by}
|
||||||
|
/>
|
||||||
|
</DetailList>
|
||||||
|
<CardActionsRow>
|
||||||
|
{user_capabilities?.edit && (
|
||||||
|
<Button
|
||||||
|
component={Link}
|
||||||
|
aria-label="edit"
|
||||||
|
to={`/inventories/smart_inventory/${id}/edit`}
|
||||||
|
>
|
||||||
|
{i18n._(t`Edit`)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{user_capabilities?.delete && (
|
||||||
|
<DeleteButton
|
||||||
|
name={name}
|
||||||
|
modalTitle={i18n._(t`Delete smart inventory`)}
|
||||||
|
onConfirm={handleDelete}
|
||||||
|
isDisabled={isLoading}
|
||||||
|
>
|
||||||
|
{i18n._(t`Delete`)}
|
||||||
|
</DeleteButton>
|
||||||
|
)}
|
||||||
|
</CardActionsRow>
|
||||||
|
</CardBody>
|
||||||
|
{error && (
|
||||||
|
<AlertModal
|
||||||
|
isOpen={error}
|
||||||
|
variant="error"
|
||||||
|
title={i18n._(t`Error!`)}
|
||||||
|
onClose={dismissError}
|
||||||
|
>
|
||||||
|
{i18n._(t`Failed to delete smart inventory.`)}
|
||||||
|
<ErrorDetail error={error} />
|
||||||
|
</AlertModal>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SmartInventoryDetail;
|
SmartInventoryDetail.propTypes = {
|
||||||
|
inventory: Inventory.isRequired,
|
||||||
|
i18n: shape({}).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withI18n()(SmartInventoryDetail);
|
||||||
|
@ -0,0 +1,155 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import {
|
||||||
|
mountWithContexts,
|
||||||
|
waitForElement,
|
||||||
|
} from '../../../../testUtils/enzymeHelpers';
|
||||||
|
import SmartInventoryDetail from './SmartInventoryDetail';
|
||||||
|
import { InventoriesAPI, UnifiedJobsAPI } from '../../../api';
|
||||||
|
|
||||||
|
import mockSmartInventory from '../shared/data.smart_inventory.json';
|
||||||
|
|
||||||
|
jest.mock('../../../api/models/UnifiedJobs');
|
||||||
|
jest.mock('../../../api/models/Inventories');
|
||||||
|
|
||||||
|
UnifiedJobsAPI.read.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
results: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'job 1',
|
||||||
|
type: 'job',
|
||||||
|
status: 'successful',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
InventoriesAPI.readInstanceGroups.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
results: [{ id: 1, name: 'mock instance group' }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('<SmartInventoryDetail />', () => {
|
||||||
|
let wrapper;
|
||||||
|
|
||||||
|
describe('User has edit permissions', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SmartInventoryDetail inventory={mockSmartInventory} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
wrapper.unmount();
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render Details', async () => {
|
||||||
|
function assertDetail(label, value) {
|
||||||
|
expect(wrapper.find(`Detail[label="${label}"] dt`).text()).toBe(label);
|
||||||
|
expect(wrapper.find(`Detail[label="${label}"] dd`).text()).toBe(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertDetail('Name', 'Smart Inv');
|
||||||
|
assertDetail('Description', 'smart inv description');
|
||||||
|
assertDetail('Type', 'Smart inventory');
|
||||||
|
assertDetail('Organization', 'Default');
|
||||||
|
assertDetail('Smart host filter', 'search=local');
|
||||||
|
assertDetail('Instance groups', 'mock instance group');
|
||||||
|
expect(wrapper.find(`Detail[label="Activity"] Sparkline`)).toHaveLength(
|
||||||
|
1
|
||||||
|
);
|
||||||
|
const vars = wrapper.find('VariablesDetail');
|
||||||
|
expect(vars).toHaveLength(1);
|
||||||
|
expect(vars.prop('value')).toEqual(mockSmartInventory.variables);
|
||||||
|
const dates = wrapper.find('UserDateDetail');
|
||||||
|
expect(dates).toHaveLength(2);
|
||||||
|
expect(dates.at(0).prop('date')).toEqual(mockSmartInventory.created);
|
||||||
|
expect(dates.at(1).prop('date')).toEqual(mockSmartInventory.modified);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show edit button for users with edit permission', () => {
|
||||||
|
const editButton = wrapper.find('Button[aria-label="edit"]');
|
||||||
|
expect(editButton.text()).toEqual('Edit');
|
||||||
|
expect(editButton.prop('to')).toBe(
|
||||||
|
`/inventories/smart_inventory/${mockSmartInventory.id}/edit`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('expected api calls are made on initial render', () => {
|
||||||
|
expect(InventoriesAPI.readInstanceGroups).toHaveBeenCalledTimes(1);
|
||||||
|
expect(UnifiedJobsAPI.read).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('expected api call is made for delete', async () => {
|
||||||
|
expect(InventoriesAPI.destroy).toHaveBeenCalledTimes(0);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('DeleteButton').invoke('onConfirm')();
|
||||||
|
});
|
||||||
|
expect(InventoriesAPI.destroy).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Error dialog shown for failed deletion', async () => {
|
||||||
|
InventoriesAPI.destroy.mockImplementationOnce(() =>
|
||||||
|
Promise.reject(new Error())
|
||||||
|
);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('DeleteButton').invoke('onConfirm')();
|
||||||
|
});
|
||||||
|
await waitForElement(
|
||||||
|
wrapper,
|
||||||
|
'Modal[title="Error!"]',
|
||||||
|
el => el.length === 1
|
||||||
|
);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('Modal[title="Error!"]').invoke('onClose')();
|
||||||
|
});
|
||||||
|
await waitForElement(
|
||||||
|
wrapper,
|
||||||
|
'Modal[title="Error!"]',
|
||||||
|
el => el.length === 0
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('User has read-only permissions', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
wrapper.unmount();
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should hide edit button for users without edit permission', async () => {
|
||||||
|
const readOnlySmartInv = { ...mockSmartInventory };
|
||||||
|
readOnlySmartInv.summary_fields.user_capabilities.edit = false;
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SmartInventoryDetail inventory={readOnlySmartInv} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
expect(wrapper.find('Button[aria-label="edit"]').length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show content error when jobs request fails', async () => {
|
||||||
|
UnifiedJobsAPI.read.mockImplementationOnce(() =>
|
||||||
|
Promise.reject(new Error())
|
||||||
|
);
|
||||||
|
expect(UnifiedJobsAPI.read).toHaveBeenCalledTimes(0);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<SmartInventoryDetail inventory={mockSmartInventory} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
expect(UnifiedJobsAPI.read).toHaveBeenCalledTimes(1);
|
||||||
|
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
|
||||||
|
expect(wrapper.find('ContentError Title').text()).toEqual(
|
||||||
|
'Something went wrong...'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -77,7 +77,7 @@
|
|||||||
"created": "2019-10-04T15:29:11.542911Z",
|
"created": "2019-10-04T15:29:11.542911Z",
|
||||||
"modified": "2019-10-04T15:29:11.542924Z",
|
"modified": "2019-10-04T15:29:11.542924Z",
|
||||||
"name": "Smart Inv",
|
"name": "Smart Inv",
|
||||||
"description": "",
|
"description": "smart inv description",
|
||||||
"organization": 1,
|
"organization": 1,
|
||||||
"kind": "smart",
|
"kind": "smart",
|
||||||
"host_filter": "search=local",
|
"host_filter": "search=local",
|
||||||
|
Loading…
Reference in New Issue
Block a user