mirror of
https://github.com/ansible/awx.git
synced 2026-01-11 10:00:01 -03:30
Merge pull request #7919 from marshmalien/7806-smart-inv-detail
Add smart inventory host detail view
Reviewed-by: Daniel Sami
https://github.com/dsesami
This commit is contained in:
commit
5507f264e3
@ -111,7 +111,7 @@ function SmartInventory({ i18n, setBreadcrumb }) {
|
||||
|
||||
let showCardHeader = true;
|
||||
|
||||
if (location.pathname.endsWith('edit')) {
|
||||
if (['edit', 'hosts/'].some(name => location.pathname.includes(name))) {
|
||||
showCardHeader = false;
|
||||
}
|
||||
|
||||
@ -145,7 +145,10 @@ function SmartInventory({ i18n, setBreadcrumb }) {
|
||||
/>
|
||||
</Route>,
|
||||
<Route key="hosts" path="/inventories/smart_inventory/:id/hosts">
|
||||
<SmartInventoryHosts inventory={inventory} />
|
||||
<SmartInventoryHosts
|
||||
inventory={inventory}
|
||||
setBreadcrumb={setBreadcrumb}
|
||||
/>
|
||||
</Route>,
|
||||
<Route
|
||||
key="completed_jobs"
|
||||
|
||||
@ -0,0 +1,90 @@
|
||||
import React, { useEffect, useCallback } from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { Link, Redirect, Route, Switch, useRouteMatch } from 'react-router-dom';
|
||||
import { CaretLeftIcon } from '@patternfly/react-icons';
|
||||
import ContentError from '../../../components/ContentError';
|
||||
import ContentLoading from '../../../components/ContentLoading';
|
||||
import RoutedTabs from '../../../components/RoutedTabs';
|
||||
import SmartInventoryHostDetail from '../SmartInventoryHostDetail';
|
||||
import useRequest from '../../../util/useRequest';
|
||||
import { InventoriesAPI } from '../../../api';
|
||||
|
||||
function SmartInventoryHost({ i18n, inventory, setBreadcrumb }) {
|
||||
const { params, path, url } = useRouteMatch(
|
||||
'/inventories/smart_inventory/:id/hosts/:hostId'
|
||||
);
|
||||
|
||||
const { result: host, error, isLoading, request: fetchHost } = useRequest(
|
||||
useCallback(async () => {
|
||||
const response = await InventoriesAPI.readHostDetail(
|
||||
inventory.id,
|
||||
params.hostId
|
||||
);
|
||||
return response;
|
||||
}, [inventory.id, params.hostId]),
|
||||
null
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetchHost();
|
||||
}, [fetchHost]);
|
||||
|
||||
useEffect(() => {
|
||||
if (inventory && host) {
|
||||
setBreadcrumb(inventory, host);
|
||||
}
|
||||
}, [inventory, host, setBreadcrumb]);
|
||||
|
||||
if (error) {
|
||||
return <ContentError error={error} />;
|
||||
}
|
||||
|
||||
const tabsArray = [
|
||||
{
|
||||
name: (
|
||||
<>
|
||||
<CaretLeftIcon />
|
||||
{i18n._(t`Back to Hosts`)}
|
||||
</>
|
||||
),
|
||||
link: `/inventories/smart_inventory/${inventory.id}/hosts`,
|
||||
id: 0,
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Details`),
|
||||
link: `${url}/details`,
|
||||
id: 1,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<RoutedTabs tabsArray={tabsArray} />
|
||||
|
||||
{isLoading && <ContentLoading />}
|
||||
|
||||
{!isLoading && host && (
|
||||
<Switch>
|
||||
<Redirect
|
||||
from="/inventories/smart_inventory/:id/hosts/:hostId"
|
||||
to={`${path}/details`}
|
||||
exact
|
||||
/>
|
||||
<Route key="details" path={`${path}/details`}>
|
||||
<SmartInventoryHostDetail host={host} />
|
||||
</Route>
|
||||
<Route key="not-found" path="*">
|
||||
<ContentError isNotFound>
|
||||
<Link to={`${url}/details`}>
|
||||
{i18n._(t`View smart inventory host details`)}
|
||||
</Link>
|
||||
</ContentError>
|
||||
</Route>
|
||||
</Switch>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default withI18n()(SmartInventoryHost);
|
||||
@ -0,0 +1,91 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { InventoriesAPI } from '../../../api';
|
||||
import {
|
||||
mountWithContexts,
|
||||
waitForElement,
|
||||
} from '../../../../testUtils/enzymeHelpers';
|
||||
import mockHost from '../shared/data.host.json';
|
||||
import SmartInventoryHost from './SmartInventoryHost';
|
||||
|
||||
jest.mock('../../../api');
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useRouteMatch: () => ({
|
||||
params: { id: 1234, hostId: 2 },
|
||||
path: '/inventories/smart_inventory/:id/hosts/:hostId',
|
||||
url: '/inventories/smart_inventory/1234/hosts/2',
|
||||
}),
|
||||
}));
|
||||
|
||||
const mockSmartInventory = {
|
||||
id: 1234,
|
||||
name: 'Mock Smart Inventory',
|
||||
};
|
||||
|
||||
describe('<SmartInventoryHost />', () => {
|
||||
let wrapper;
|
||||
let history;
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.unmount();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should render expected tabs', async () => {
|
||||
InventoriesAPI.readHostDetail.mockResolvedValue({
|
||||
data: { ...mockHost },
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<SmartInventoryHost
|
||||
inventory={mockSmartInventory}
|
||||
setBreadcrumb={() => {}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const expectedTabs = ['Back to Hosts', 'Details'];
|
||||
|
||||
expect(wrapper.find('RoutedTabs li').length).toBe(2);
|
||||
wrapper.find('RoutedTabs li').forEach((tab, index) => {
|
||||
expect(tab.text()).toEqual(expectedTabs[index]);
|
||||
});
|
||||
});
|
||||
|
||||
test('should show content error when api throws error on initial render', async () => {
|
||||
InventoriesAPI.readHostDetail.mockRejectedValueOnce(new Error());
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<SmartInventoryHost
|
||||
inventory={mockSmartInventory}
|
||||
setBreadcrumb={() => {}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
|
||||
expect(wrapper.find('ContentError Title').text()).toEqual(
|
||||
'Something went wrong...'
|
||||
);
|
||||
});
|
||||
|
||||
test('should show content error when user attempts to navigate to erroneous route', async () => {
|
||||
history = createMemoryHistory({
|
||||
initialEntries: ['/inventories/smart_inventory/1/hosts/1/foobar'],
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<SmartInventoryHost
|
||||
inventory={mockSmartInventory}
|
||||
setBreadcrumb={() => {}}
|
||||
/>,
|
||||
{ context: { router: { history } } }
|
||||
);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
|
||||
expect(wrapper.find('ContentError Title').text()).toEqual('Not Found');
|
||||
});
|
||||
});
|
||||
@ -0,0 +1 @@
|
||||
export { default } from './SmartInventoryHost';
|
||||
@ -0,0 +1,75 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { Host } from '../../../types';
|
||||
import { CardBody } from '../../../components/Card';
|
||||
import {
|
||||
Detail,
|
||||
DetailList,
|
||||
UserDateDetail,
|
||||
} from '../../../components/DetailList';
|
||||
import Sparkline from '../../../components/Sparkline';
|
||||
import { VariablesDetail } from '../../../components/CodeMirrorInput';
|
||||
|
||||
function SmartInventoryHostDetail({ host, i18n }) {
|
||||
const {
|
||||
created,
|
||||
description,
|
||||
enabled,
|
||||
modified,
|
||||
name,
|
||||
variables,
|
||||
summary_fields: { inventory, recent_jobs, created_by, modified_by },
|
||||
} = host;
|
||||
|
||||
const recentPlaybookJobs = recent_jobs?.map(job => ({ ...job, type: 'job' }));
|
||||
|
||||
return (
|
||||
<CardBody>
|
||||
<DetailList gutter="sm">
|
||||
<Detail label={i18n._(t`Name`)} value={name} />
|
||||
{recentPlaybookJobs?.length > 0 && (
|
||||
<Detail
|
||||
label={i18n._(t`Activity`)}
|
||||
value={<Sparkline jobs={recentPlaybookJobs} />}
|
||||
/>
|
||||
)}
|
||||
<Detail label={i18n._(t`Description`)} value={description} />
|
||||
<Detail
|
||||
label={i18n._(t`Inventory`)}
|
||||
value={
|
||||
<Link to={`/inventories/inventory/${inventory?.id}/details`}>
|
||||
{inventory?.name}
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
<Detail
|
||||
label={i18n._(t`Enabled`)}
|
||||
value={enabled ? i18n._(t`On`) : i18n._(t`Off`)}
|
||||
/>
|
||||
<UserDateDetail
|
||||
date={created}
|
||||
label={i18n._(t`Created`)}
|
||||
user={created_by}
|
||||
/>
|
||||
<UserDateDetail
|
||||
date={modified}
|
||||
label={i18n._(t`Last modified`)}
|
||||
user={modified_by}
|
||||
/>
|
||||
<VariablesDetail
|
||||
label={i18n._(t`Variables`)}
|
||||
rows={4}
|
||||
value={variables}
|
||||
/>
|
||||
</DetailList>
|
||||
</CardBody>
|
||||
);
|
||||
}
|
||||
|
||||
SmartInventoryHostDetail.propTypes = {
|
||||
host: Host.isRequired,
|
||||
};
|
||||
|
||||
export default withI18n()(SmartInventoryHostDetail);
|
||||
@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||
import SmartInventoryHostDetail from './SmartInventoryHostDetail';
|
||||
import mockHost from '../shared/data.host.json';
|
||||
|
||||
jest.mock('../../../api');
|
||||
|
||||
describe('<SmartInventoryHostDetail />', () => {
|
||||
let wrapper;
|
||||
|
||||
beforeAll(() => {
|
||||
wrapper = mountWithContexts(<SmartInventoryHostDetail host={mockHost} />);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('should render Details', () => {
|
||||
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', 'localhost');
|
||||
assertDetail('Description', 'localhost description');
|
||||
assertDetail('Inventory', 'Mikes Inventory');
|
||||
assertDetail('Enabled', 'On');
|
||||
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);
|
||||
expect(wrapper.find('VariablesDetail')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1 @@
|
||||
export { default } from './SmartInventoryHostDetail';
|
||||
@ -2,18 +2,16 @@ import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { string, bool, func } from 'prop-types';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t, Trans } from '@lingui/macro';
|
||||
import { t } from '@lingui/macro';
|
||||
import 'styled-components/macro';
|
||||
|
||||
import {
|
||||
DataListAction,
|
||||
DataListCheck,
|
||||
DataListItem,
|
||||
DataListItemCells,
|
||||
DataListItemRow,
|
||||
} from '@patternfly/react-core';
|
||||
import DataListCell from '../../../components/DataListCell';
|
||||
import HostToggle from '../../../components/HostToggle';
|
||||
import Sparkline from '../../../components/Sparkline';
|
||||
import { Host } from '../../../types';
|
||||
|
||||
@ -62,25 +60,6 @@ function SmartInventoryHostListItem({
|
||||
</DataListCell>,
|
||||
]}
|
||||
/>
|
||||
<DataListAction
|
||||
aria-label="actions"
|
||||
aria-labelledby={labelId}
|
||||
id={labelId}
|
||||
>
|
||||
<HostToggle
|
||||
isDisabled
|
||||
host={host}
|
||||
tooltip={
|
||||
<Trans>
|
||||
<b>Smart inventory hosts are read-only.</b>
|
||||
<br />
|
||||
Toggle indicates if a host is available and should be included
|
||||
in running jobs. For hosts that are part of an external
|
||||
inventory, this may be reset by the inventory sync process.
|
||||
</Trans>
|
||||
}
|
||||
/>
|
||||
</DataListAction>
|
||||
</DataListItemRow>
|
||||
</DataListItem>
|
||||
);
|
||||
|
||||
@ -44,9 +44,4 @@ describe('<SmartInventoryHostListItem />', () => {
|
||||
expect(cells.at(1).find('Sparkline').length).toEqual(1);
|
||||
expect(cells.at(2).text()).toContain('Inv 1');
|
||||
});
|
||||
|
||||
test('should display disabled host toggle', () => {
|
||||
expect(wrapper.find('HostToggle').length).toBe(1);
|
||||
expect(wrapper.find('HostToggle Switch').prop('isDisabled')).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,13 +1,22 @@
|
||||
import React from 'react';
|
||||
import { Route } from 'react-router-dom';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
import SmartInventoryHostList from './SmartInventoryHostList';
|
||||
import SmartInventoryHost from '../SmartInventoryHost';
|
||||
import { Inventory } from '../../../types';
|
||||
|
||||
function SmartInventoryHosts({ inventory }) {
|
||||
function SmartInventoryHosts({ inventory, setBreadcrumb }) {
|
||||
return (
|
||||
<Route key="host-list" path="/inventories/smart_inventory/:id/hosts">
|
||||
<SmartInventoryHostList inventory={inventory} />
|
||||
</Route>
|
||||
<Switch>
|
||||
<Route key="host" path="/inventories/smart_inventory/:id/hosts/:hostId">
|
||||
<SmartInventoryHost
|
||||
setBreadcrumb={setBreadcrumb}
|
||||
inventory={inventory}
|
||||
/>
|
||||
</Route>
|
||||
<Route key="host-list" path="/inventories/smart_inventory/:id/hosts">
|
||||
<SmartInventoryHostList inventory={inventory} />
|
||||
</Route>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||
import {
|
||||
mountWithContexts,
|
||||
waitForElement,
|
||||
} from '../../../../testUtils/enzymeHelpers';
|
||||
import SmartInventoryHosts from './SmartInventoryHosts';
|
||||
|
||||
jest.mock('../../../api');
|
||||
@ -25,4 +29,28 @@ describe('<SmartInventoryHosts />', () => {
|
||||
jest.clearAllMocks();
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('should render smart inventory host details', async () => {
|
||||
let wrapper;
|
||||
const history = createMemoryHistory({
|
||||
initialEntries: ['/inventories/smart_inventory/1/hosts/2'],
|
||||
});
|
||||
const match = {
|
||||
path: '/inventories/smart_inventory/:id/hosts/:hostId',
|
||||
url: '/inventories/smart_inventory/1/hosts/2',
|
||||
isExact: true,
|
||||
};
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<SmartInventoryHosts inventory={{ id: 1 }} setBreadcrumb={() => {}} />,
|
||||
{
|
||||
context: { router: { history, route: { match } } },
|
||||
}
|
||||
);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
expect(wrapper.find('SmartInventoryHost').length).toBe(1);
|
||||
jest.clearAllMocks();
|
||||
wrapper.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user