mirror of
https://github.com/ansible/awx.git
synced 2024-10-30 22:21:13 +03:00
Add smart inv detail
This commit is contained in:
parent
8ea31d8cdd
commit
b5bbfaab11
@ -64,7 +64,7 @@ function InventoryScriptLookup({
|
||||
fieldId="inventory-script"
|
||||
helperTextInvalid={helperTextInvalid}
|
||||
isRequired={required}
|
||||
isValid={isValid}
|
||||
validated={isValid ? 'default' : 'error'}
|
||||
label={i18n._(t`Inventory script`)}
|
||||
>
|
||||
<Lookup
|
||||
|
@ -72,10 +72,12 @@ function HostDetail({ i18n, host }) {
|
||||
<HostToggle host={host} css="padding-bottom: 40px" />
|
||||
<DetailList gutter="sm">
|
||||
<Detail label={i18n._(t`Name`)} value={name} dataCy="host-name" />
|
||||
<Detail
|
||||
label={i18n._(t`Activity`)}
|
||||
value={<Sparkline jobs={recentPlaybookJobs} />}
|
||||
/>
|
||||
{recentPlaybookJobs?.length > 0 && (
|
||||
<Detail
|
||||
label={i18n._(t`Activity`)}
|
||||
value={<Sparkline jobs={recentPlaybookJobs} />}
|
||||
/>
|
||||
)}
|
||||
<Detail label={i18n._(t`Description`)} value={description} />
|
||||
<Detail
|
||||
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 { 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 Breadcrumbs from '../../components/Breadcrumbs/Breadcrumbs';
|
||||
@ -11,131 +11,116 @@ import SmartInventory from './SmartInventory';
|
||||
import InventoryAdd from './InventoryAdd';
|
||||
import SmartInventoryAdd from './SmartInventoryAdd';
|
||||
|
||||
class Inventories extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const { i18n } = this.props;
|
||||
function Inventories({ i18n }) {
|
||||
const [breadcrumbConfig, setBreadcrumbConfig] = useState({
|
||||
'/inventories': i18n._(t`Inventories`),
|
||||
'/inventories/inventory/add': i18n._(t`Create new inventory`),
|
||||
'/inventories/smart_inventory/add': i18n._(t`Create new smart inventory`),
|
||||
});
|
||||
|
||||
this.state = {
|
||||
breadcrumbConfig: {
|
||||
const buildBreadcrumbConfig = useCallback(
|
||||
(inventory, nested, schedule) => {
|
||||
if (!inventory) {
|
||||
return;
|
||||
}
|
||||
|
||||
const inventoryKind =
|
||||
inventory.kind === 'smart' ? 'smart_inventory' : 'inventory';
|
||||
|
||||
const inventoryPath = `/inventories/${inventoryKind}/${inventory.id}`;
|
||||
const inventoryHostsPath = `${inventoryPath}/hosts`;
|
||||
const inventoryGroupsPath = `${inventoryPath}/groups`;
|
||||
const inventorySourcesPath = `${inventoryPath}/sources`;
|
||||
|
||||
setBreadcrumbConfig({
|
||||
'/inventories': i18n._(t`Inventories`),
|
||||
'/inventories/inventory/add': i18n._(t`Create new inventory`),
|
||||
'/inventories/smart_inventory/add': i18n._(
|
||||
t`Create new smart inventory`
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
setBreadCrumbConfig = (inventory, nested, schedule) => {
|
||||
const { i18n } = this.props;
|
||||
if (!inventory) {
|
||||
return;
|
||||
}
|
||||
[inventoryPath]: `${inventory.name}`,
|
||||
[`${inventoryPath}/access`]: i18n._(t`Access`),
|
||||
[`${inventoryPath}/completed_jobs`]: i18n._(t`Completed jobs`),
|
||||
[`${inventoryPath}/details`]: i18n._(t`Details`),
|
||||
[`${inventoryPath}/edit`]: i18n._(t`Edit details`),
|
||||
|
||||
const inventoryKind =
|
||||
inventory.kind === 'smart' ? 'smart_inventory' : 'inventory';
|
||||
[inventoryHostsPath]: i18n._(t`Hosts`),
|
||||
[`${inventoryHostsPath}/add`]: i18n._(t`Create new host`),
|
||||
[`${inventoryHostsPath}/${nested?.id}`]: `${nested?.name}`,
|
||||
[`${inventoryHostsPath}/${nested?.id}/edit`]: i18n._(t`Edit details`),
|
||||
[`${inventoryHostsPath}/${nested?.id}/details`]: i18n._(
|
||||
t`Host Details`
|
||||
),
|
||||
[`${inventoryHostsPath}/${nested?.id}/completed_jobs`]: i18n._(
|
||||
t`Completed jobs`
|
||||
),
|
||||
[`${inventoryHostsPath}/${nested?.id}/facts`]: i18n._(t`Facts`),
|
||||
[`${inventoryHostsPath}/${nested?.id}/groups`]: i18n._(t`Groups`),
|
||||
|
||||
const inventoryPath = `/inventories/${inventoryKind}/${inventory.id}`;
|
||||
const inventoryHostsPath = `${inventoryPath}/hosts`;
|
||||
const inventoryGroupsPath = `${inventoryPath}/groups`;
|
||||
const inventorySourcesPath = `${inventoryPath}/sources`;
|
||||
[inventoryGroupsPath]: i18n._(t`Groups`),
|
||||
[`${inventoryGroupsPath}/add`]: i18n._(t`Create new group`),
|
||||
[`${inventoryGroupsPath}/${nested?.id}`]: `${nested?.name}`,
|
||||
[`${inventoryGroupsPath}/${nested?.id}/edit`]: i18n._(t`Edit details`),
|
||||
[`${inventoryGroupsPath}/${nested?.id}/details`]: i18n._(
|
||||
t`Group details`
|
||||
),
|
||||
[`${inventoryGroupsPath}/${nested?.id}/nested_hosts`]: i18n._(t`Hosts`),
|
||||
[`${inventoryGroupsPath}/${nested?.id}/nested_hosts/add`]: i18n._(
|
||||
t`Create new host`
|
||||
),
|
||||
|
||||
const breadcrumbConfig = {
|
||||
'/inventories': i18n._(t`Inventories`),
|
||||
'/inventories/inventory/add': i18n._(t`Create new inventory`),
|
||||
'/inventories/smart_inventory/add': i18n._(t`Create new smart inventory`),
|
||||
[`${inventorySourcesPath}`]: i18n._(t`Sources`),
|
||||
[`${inventorySourcesPath}/add`]: i18n._(t`Create new source`),
|
||||
[`${inventorySourcesPath}/${nested?.id}`]: `${nested?.name}`,
|
||||
[`${inventorySourcesPath}/${nested?.id}/details`]: i18n._(t`Details`),
|
||||
[`${inventorySourcesPath}/${nested?.id}/edit`]: i18n._(t`Edit details`),
|
||||
[`${inventorySourcesPath}/${nested?.id}/schedules`]: i18n._(
|
||||
t`Schedules`
|
||||
),
|
||||
[`${inventorySourcesPath}/${nested?.id}/schedules/${schedule?.id}`]: `${schedule?.name}`,
|
||||
[`${inventorySourcesPath}/${nested?.id}/schedules/${schedule?.id}/details`]: i18n._(
|
||||
t`Schedule Details`
|
||||
),
|
||||
});
|
||||
},
|
||||
[i18n]
|
||||
);
|
||||
|
||||
[inventoryPath]: `${inventory.name}`,
|
||||
[`${inventoryPath}/access`]: i18n._(t`Access`),
|
||||
[`${inventoryPath}/completed_jobs`]: i18n._(t`Completed jobs`),
|
||||
[`${inventoryPath}/details`]: i18n._(t`Details`),
|
||||
[`${inventoryPath}/edit`]: i18n._(t`Edit details`),
|
||||
|
||||
[inventoryHostsPath]: i18n._(t`Hosts`),
|
||||
[`${inventoryHostsPath}/add`]: i18n._(t`Create new host`),
|
||||
[`${inventoryHostsPath}/${nested?.id}`]: `${nested?.name}`,
|
||||
[`${inventoryHostsPath}/${nested?.id}/edit`]: i18n._(t`Edit details`),
|
||||
[`${inventoryHostsPath}/${nested?.id}/details`]: i18n._(t`Host Details`),
|
||||
[`${inventoryHostsPath}/${nested?.id}/completed_jobs`]: i18n._(
|
||||
t`Completed jobs`
|
||||
),
|
||||
[`${inventoryHostsPath}/${nested?.id}/facts`]: i18n._(t`Facts`),
|
||||
[`${inventoryHostsPath}/${nested?.id}/groups`]: i18n._(t`Groups`),
|
||||
|
||||
[inventoryGroupsPath]: i18n._(t`Groups`),
|
||||
[`${inventoryGroupsPath}/add`]: i18n._(t`Create new group`),
|
||||
[`${inventoryGroupsPath}/${nested?.id}`]: `${nested?.name}`,
|
||||
[`${inventoryGroupsPath}/${nested?.id}/edit`]: i18n._(t`Edit details`),
|
||||
[`${inventoryGroupsPath}/${nested?.id}/details`]: i18n._(
|
||||
t`Group details`
|
||||
),
|
||||
[`${inventoryGroupsPath}/${nested?.id}/nested_hosts`]: i18n._(t`Hosts`),
|
||||
[`${inventoryGroupsPath}/${nested?.id}/nested_hosts/add`]: i18n._(
|
||||
t`Create new host`
|
||||
),
|
||||
|
||||
[`${inventorySourcesPath}`]: i18n._(t`Sources`),
|
||||
[`${inventorySourcesPath}/add`]: i18n._(t`Create new source`),
|
||||
[`${inventorySourcesPath}/${nested?.id}`]: `${nested?.name}`,
|
||||
[`${inventorySourcesPath}/${nested?.id}/details`]: i18n._(t`Details`),
|
||||
[`${inventorySourcesPath}/${nested?.id}/edit`]: i18n._(t`Edit details`),
|
||||
[`${inventorySourcesPath}/${nested?.id}/schedules`]: i18n._(t`Schedules`),
|
||||
[`${inventorySourcesPath}/${nested?.id}/schedules/${schedule?.id}`]: `${schedule?.name}`,
|
||||
[`${inventorySourcesPath}/${nested?.id}/schedules/${schedule?.id}/details`]: i18n._(
|
||||
t`Schedule Details`
|
||||
),
|
||||
};
|
||||
this.setState({ breadcrumbConfig });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { match, history, location } = this.props;
|
||||
const { breadcrumbConfig } = this.state;
|
||||
return (
|
||||
<>
|
||||
<Breadcrumbs breadcrumbConfig={breadcrumbConfig} />
|
||||
<Switch>
|
||||
<Route path={`${match.path}/inventory/add`}>
|
||||
<InventoryAdd />
|
||||
</Route>
|
||||
<Route path={`${match.path}/smart_inventory/add`}>
|
||||
<SmartInventoryAdd />
|
||||
</Route>
|
||||
<Route path={`${match.path}/inventory/:id`}>
|
||||
<Config>
|
||||
{({ me }) => (
|
||||
<Inventory
|
||||
setBreadcrumb={this.setBreadCrumbConfig}
|
||||
me={me || {}}
|
||||
/>
|
||||
)}
|
||||
</Config>
|
||||
</Route>
|
||||
<Route
|
||||
path={`${match.path}/smart_inventory/:id`}
|
||||
render={({ match: newRouteMatch }) => (
|
||||
<Config>
|
||||
{({ me }) => (
|
||||
<SmartInventory
|
||||
history={history}
|
||||
location={location}
|
||||
setBreadcrumb={this.setBreadCrumbConfig}
|
||||
me={me || {}}
|
||||
match={newRouteMatch}
|
||||
/>
|
||||
)}
|
||||
</Config>
|
||||
return (
|
||||
<>
|
||||
<Breadcrumbs breadcrumbConfig={breadcrumbConfig} />
|
||||
<Switch>
|
||||
<Route path="/inventories/inventory/add">
|
||||
<InventoryAdd />
|
||||
</Route>
|
||||
<Route path="/inventories/smart_inventory/add">
|
||||
<SmartInventoryAdd />
|
||||
</Route>
|
||||
<Route path="/inventories/inventory/:id">
|
||||
<Config>
|
||||
{({ me }) => (
|
||||
<Inventory setBreadcrumb={buildBreadcrumbConfig} me={me || {}} />
|
||||
)}
|
||||
/>
|
||||
<Route path={`${match.path}`}>
|
||||
<InventoryList />
|
||||
</Route>
|
||||
</Switch>
|
||||
</>
|
||||
);
|
||||
}
|
||||
</Config>
|
||||
</Route>
|
||||
<Route path="/inventories/smart_inventory/:id">
|
||||
<Config>
|
||||
{({ me }) => (
|
||||
<SmartInventory
|
||||
setBreadcrumb={buildBreadcrumbConfig}
|
||||
me={me || {}}
|
||||
/>
|
||||
)}
|
||||
</Config>
|
||||
</Route>
|
||||
<Route path="/inventories">
|
||||
<InventoryList />
|
||||
</Route>
|
||||
</Switch>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { Inventories as _Inventories };
|
||||
export default withI18n()(withRouter(Inventories));
|
||||
export default withI18n()(Inventories);
|
||||
|
@ -108,6 +108,12 @@ function Inventory({ i18n, setBreadcrumb }) {
|
||||
showCardHeader = false;
|
||||
}
|
||||
|
||||
if (inventory?.kind === 'smart') {
|
||||
return (
|
||||
<Redirect to={`/inventories/smart_inventory/${inventory.id}/details`} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PageSection>
|
||||
<Card>
|
||||
|
@ -73,10 +73,12 @@ function InventoryHostDetail({ i18n, host }) {
|
||||
<HostToggle host={host} css="padding-bottom: 40px" />
|
||||
<DetailList gutter="sm">
|
||||
<Detail label={i18n._(t`Name`)} value={name} />
|
||||
<Detail
|
||||
label={i18n._(t`Activity`)}
|
||||
value={<Sparkline jobs={recentPlaybookJobs} />}
|
||||
/>
|
||||
{recentPlaybookJobs?.length > 0 && (
|
||||
<Detail
|
||||
label={i18n._(t`Activity`)}
|
||||
value={<Sparkline jobs={recentPlaybookJobs} />}
|
||||
/>
|
||||
)}
|
||||
<Detail label={i18n._(t`Description`)} value={description} />
|
||||
<UserDateDetail
|
||||
date={created}
|
||||
|
@ -32,6 +32,9 @@ describe('<InventoryHostDetail />', () => {
|
||||
assertDetail('Description', 'localhost description');
|
||||
assertDetail('Created', '10/28/2019, 9:26:54 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', () => {
|
||||
|
@ -1,181 +1,183 @@
|
||||
import React, { Component } from 'react';
|
||||
import { t } from '@lingui/macro';
|
||||
import React, { useCallback, useEffect } from '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 { Card, PageSection } from '@patternfly/react-core';
|
||||
import { Switch, Route, Redirect, withRouter, Link } from 'react-router-dom';
|
||||
import ContentError from '../../components/ContentError';
|
||||
import JobList from '../../components/JobList';
|
||||
import RoutedTabs from '../../components/RoutedTabs';
|
||||
import { ResourceAccessList } from '../../components/ResourceAccessList';
|
||||
import SmartInventoryDetail from './SmartInventoryDetail';
|
||||
import SmartInventoryHosts from './SmartInventoryHosts';
|
||||
|
||||
import useRequest from '../../util/useRequest';
|
||||
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 SmartInventoryHosts from './SmartInventoryHosts';
|
||||
|
||||
class SmartInventory extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
function SmartInventory({ i18n, setBreadcrumb }) {
|
||||
const location = useLocation();
|
||||
const match = useRouteMatch('/inventories/smart_inventory/:id');
|
||||
|
||||
this.state = {
|
||||
contentError: null,
|
||||
hasContentLoading: true,
|
||||
const {
|
||||
result: { inventory },
|
||||
error: contentError,
|
||||
isLoading: hasContentLoading,
|
||||
request: fetchInventory,
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
const { data } = await InventoriesAPI.readDetail(match.params.id);
|
||||
return {
|
||||
inventory: data,
|
||||
};
|
||||
}, [match.params.id]),
|
||||
{
|
||||
inventory: null,
|
||||
};
|
||||
this.loadSmartInventory = this.loadSmartInventory.bind(this);
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
await this.loadSmartInventory();
|
||||
}
|
||||
|
||||
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;
|
||||
useEffect(() => {
|
||||
fetchInventory();
|
||||
}, [fetchInventory, location.pathname]);
|
||||
|
||||
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 });
|
||||
useEffect(() => {
|
||||
if (inventory) {
|
||||
setBreadcrumb(inventory);
|
||||
}
|
||||
}
|
||||
}, [inventory, setBreadcrumb]);
|
||||
|
||||
render() {
|
||||
const { i18n, location, match } = this.props;
|
||||
const { contentError, hasContentLoading, inventory } = this.state;
|
||||
const tabsArray = [
|
||||
{
|
||||
name: (
|
||||
<>
|
||||
<CaretLeftIcon />
|
||||
{i18n._(t`Back to Inventories`)}
|
||||
</>
|
||||
),
|
||||
link: `/inventories`,
|
||||
id: 99,
|
||||
},
|
||||
{ name: i18n._(t`Details`), link: `${match.url}/details`, id: 0 },
|
||||
{ name: i18n._(t`Access`), link: `${match.url}/access`, id: 1 },
|
||||
{ name: i18n._(t`Hosts`), link: `${match.url}/hosts`, id: 2 },
|
||||
{
|
||||
name: i18n._(t`Completed jobs`),
|
||||
link: `${match.url}/completed_jobs`,
|
||||
id: 3,
|
||||
},
|
||||
];
|
||||
|
||||
const tabsArray = [
|
||||
{
|
||||
name: (
|
||||
<>
|
||||
<CaretLeftIcon />
|
||||
{i18n._(t`Back to Inventories`)}
|
||||
</>
|
||||
),
|
||||
link: `/inventories`,
|
||||
id: 99,
|
||||
},
|
||||
{ name: i18n._(t`Details`), link: `${match.url}/details`, id: 0 },
|
||||
{ name: i18n._(t`Access`), link: `${match.url}/access`, id: 1 },
|
||||
{ name: i18n._(t`Hosts`), link: `${match.url}/hosts`, id: 2 },
|
||||
{
|
||||
name: i18n._(t`Completed Jobs`),
|
||||
link: `${match.url}/completed_jobs`,
|
||||
id: 3,
|
||||
},
|
||||
];
|
||||
|
||||
let showCardHeader = true;
|
||||
|
||||
if (location.pathname.endsWith('edit')) {
|
||||
showCardHeader = false;
|
||||
}
|
||||
|
||||
if (!hasContentLoading && contentError) {
|
||||
return (
|
||||
<PageSection>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
if (hasContentLoading) {
|
||||
return (
|
||||
<PageSection>
|
||||
<Card>
|
||||
{showCardHeader && <RoutedTabs tabsArray={tabsArray} />}
|
||||
<Switch>
|
||||
<Redirect
|
||||
from="/inventories/smart_inventory/:id"
|
||||
to="/inventories/smart_inventory/:id/details"
|
||||
exact
|
||||
/>
|
||||
{inventory && [
|
||||
<Route
|
||||
key="details"
|
||||
path="/inventories/smart_inventory/:id/details"
|
||||
>
|
||||
<SmartInventoryDetail
|
||||
hasSmartInventoryLoading={hasContentLoading}
|
||||
inventory={inventory}
|
||||
/>
|
||||
</Route>,
|
||||
<Route key="edit" path="/inventories/smart_inventory/:id/edit">
|
||||
<SmartInventoryEdit inventory={inventory} />
|
||||
</Route>,
|
||||
<Route
|
||||
key="access"
|
||||
path="/inventories/smart_inventory/:id/access"
|
||||
>
|
||||
<ResourceAccessList
|
||||
resource={inventory}
|
||||
apiModel={InventoriesAPI}
|
||||
/>
|
||||
</Route>,
|
||||
<Route key="hosts" path="/inventories/smart_inventory/:id/hosts">
|
||||
<SmartInventoryHosts inventory={inventory} />
|
||||
</Route>,
|
||||
<Route
|
||||
key="completed_jobs"
|
||||
path="/inventories/smart_inventory/:id/completed_jobs"
|
||||
>
|
||||
<JobList
|
||||
defaultParams={{
|
||||
or__job__inventory: inventory.id,
|
||||
or__adhoccommand__inventory: inventory.id,
|
||||
or__inventoryupdate__inventory_source__inventory:
|
||||
inventory.id,
|
||||
or__workflowjob__inventory: inventory.id,
|
||||
}}
|
||||
/>
|
||||
</Route>,
|
||||
<Route key="not-found" path="*">
|
||||
{!hasContentLoading && (
|
||||
<ContentError isNotFound>
|
||||
{match.params.id && (
|
||||
<Link
|
||||
to={`/inventories/smart_inventory/${match.params.id}/details`}
|
||||
>
|
||||
{i18n._(`View Inventory Details`)}
|
||||
</Link>
|
||||
)}
|
||||
</ContentError>
|
||||
)}
|
||||
</Route>,
|
||||
]}
|
||||
</Switch>
|
||||
<ContentLoading />
|
||||
</Card>
|
||||
</PageSection>
|
||||
);
|
||||
}
|
||||
|
||||
if (contentError) {
|
||||
return (
|
||||
<PageSection>
|
||||
<Card>
|
||||
<ContentError error={contentError}>
|
||||
{contentError?.response?.status === 404 && (
|
||||
<span>
|
||||
{i18n._(`Smart Inventory not found.`)}{' '}
|
||||
<Link to="/inventories">{i18n._(`View all Inventories.`)}</Link>
|
||||
</span>
|
||||
)}
|
||||
</ContentError>
|
||||
</Card>
|
||||
</PageSection>
|
||||
);
|
||||
}
|
||||
|
||||
if (inventory?.kind === '') {
|
||||
return <Redirect to={`/inventories/inventory/${inventory.id}/details`} />;
|
||||
}
|
||||
|
||||
let showCardHeader = true;
|
||||
|
||||
if (location.pathname.endsWith('edit')) {
|
||||
showCardHeader = false;
|
||||
}
|
||||
|
||||
return (
|
||||
<PageSection>
|
||||
<Card>
|
||||
{showCardHeader && <RoutedTabs tabsArray={tabsArray} />}
|
||||
<Switch>
|
||||
<Redirect
|
||||
from="/inventories/smart_inventory/:id"
|
||||
to="/inventories/smart_inventory/:id/details"
|
||||
exact
|
||||
/>
|
||||
{inventory && [
|
||||
<Route
|
||||
key="details"
|
||||
path="/inventories/smart_inventory/:id/details"
|
||||
>
|
||||
<SmartInventoryDetail
|
||||
isLoading={hasContentLoading}
|
||||
inventory={inventory}
|
||||
/>
|
||||
</Route>,
|
||||
<Route key="edit" path="/inventories/smart_inventory/:id/edit">
|
||||
<SmartInventoryEdit inventory={inventory} />
|
||||
</Route>,
|
||||
<Route key="access" path="/inventories/smart_inventory/:id/access">
|
||||
<ResourceAccessList
|
||||
resource={inventory}
|
||||
apiModel={InventoriesAPI}
|
||||
/>
|
||||
</Route>,
|
||||
<Route key="hosts" path="/inventories/smart_inventory/:id/hosts">
|
||||
<SmartInventoryHosts inventory={inventory} />
|
||||
</Route>,
|
||||
<Route
|
||||
key="completed_jobs"
|
||||
path="/inventories/smart_inventory/:id/completed_jobs"
|
||||
>
|
||||
<JobList
|
||||
defaultParams={{
|
||||
or__job__inventory: inventory.id,
|
||||
or__adhoccommand__inventory: inventory.id,
|
||||
or__inventoryupdate__inventory_source__inventory:
|
||||
inventory.id,
|
||||
or__workflowjob__inventory: inventory.id,
|
||||
}}
|
||||
/>
|
||||
</Route>,
|
||||
<Route key="not-found" path="*">
|
||||
{!hasContentLoading && (
|
||||
<ContentError isNotFound>
|
||||
{match.params.id && (
|
||||
<Link
|
||||
to={`/inventories/smart_inventory/${match.params.id}/details`}
|
||||
>
|
||||
{i18n._(`View Inventory Details`)}
|
||||
</Link>
|
||||
)}
|
||||
</ContentError>
|
||||
)}
|
||||
</Route>,
|
||||
]}
|
||||
</Switch>
|
||||
</Card>
|
||||
</PageSection>
|
||||
);
|
||||
}
|
||||
|
||||
export { SmartInventory as _SmartInventory };
|
||||
export default withI18n()(withRouter(SmartInventory));
|
||||
export default withI18n()(SmartInventory);
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { InventoriesAPI } from '../../api';
|
||||
import {
|
||||
@ -9,36 +10,51 @@ import mockSmartInventory from './shared/data.smart_inventory.json';
|
||||
import SmartInventory from './SmartInventory';
|
||||
|
||||
jest.mock('../../api');
|
||||
|
||||
InventoriesAPI.readDetail.mockResolvedValue({
|
||||
data: mockSmartInventory,
|
||||
});
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useRouteMatch: () => ({
|
||||
url: '/inventories/smart_inventory/1',
|
||||
params: { id: 1 },
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('<SmartInventory />', () => {
|
||||
test('initially renders succesfully', async done => {
|
||||
const wrapper = mountWithContexts(
|
||||
<SmartInventory setBreadcrumb={() => {}} match={{ params: { id: 1 } }} />
|
||||
);
|
||||
await waitForElement(
|
||||
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();
|
||||
let wrapper;
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.unmount();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
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 () => {
|
||||
const history = createMemoryHistory({
|
||||
initialEntries: ['/inventories/smart_inventory/1/foobar'],
|
||||
});
|
||||
const wrapper = mountWithContexts(
|
||||
<SmartInventory setBreadcrumb={() => {}} />,
|
||||
{
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<SmartInventory setBreadcrumb={() => {}} />, {
|
||||
context: {
|
||||
router: {
|
||||
history,
|
||||
@ -52,8 +68,8 @@ describe('<SmartInventory />', () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
|
||||
});
|
||||
});
|
||||
|
@ -1,10 +1,193 @@
|
||||
import React, { Component } from 'react';
|
||||
import { CardBody } from '../../../components/Card';
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
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 {
|
||||
render() {
|
||||
return <CardBody>Coming soon :)</CardBody>;
|
||||
import { Inventory } from '../../../types';
|
||||
import { InventoriesAPI, UnifiedJobsAPI } from '../../../api';
|
||||
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",
|
||||
"modified": "2019-10-04T15:29:11.542924Z",
|
||||
"name": "Smart Inv",
|
||||
"description": "",
|
||||
"description": "smart inv description",
|
||||
"organization": 1,
|
||||
"kind": "smart",
|
||||
"host_filter": "search=local",
|
||||
|
Loading…
Reference in New Issue
Block a user