mirror of
https://github.com/ansible/awx.git
synced 2026-03-05 02:31:03 -03:30
Add constructed inventory CRUD and subtab routes
* Add constructed inventory API model * Add constructed inventory detail view * Add util to switch inventory url based on "kind"
This commit is contained in:
@@ -6,6 +6,7 @@ import Config from './models/Config';
|
|||||||
import CredentialInputSources from './models/CredentialInputSources';
|
import CredentialInputSources from './models/CredentialInputSources';
|
||||||
import CredentialTypes from './models/CredentialTypes';
|
import CredentialTypes from './models/CredentialTypes';
|
||||||
import Credentials from './models/Credentials';
|
import Credentials from './models/Credentials';
|
||||||
|
import ConstructedInventories from './models/ConstructedInventories';
|
||||||
import Dashboard from './models/Dashboard';
|
import Dashboard from './models/Dashboard';
|
||||||
import ExecutionEnvironments from './models/ExecutionEnvironments';
|
import ExecutionEnvironments from './models/ExecutionEnvironments';
|
||||||
import Groups from './models/Groups';
|
import Groups from './models/Groups';
|
||||||
@@ -53,6 +54,7 @@ const ConfigAPI = new Config();
|
|||||||
const CredentialInputSourcesAPI = new CredentialInputSources();
|
const CredentialInputSourcesAPI = new CredentialInputSources();
|
||||||
const CredentialTypesAPI = new CredentialTypes();
|
const CredentialTypesAPI = new CredentialTypes();
|
||||||
const CredentialsAPI = new Credentials();
|
const CredentialsAPI = new Credentials();
|
||||||
|
const ConstructedInventoriesAPI = new ConstructedInventories();
|
||||||
const DashboardAPI = new Dashboard();
|
const DashboardAPI = new Dashboard();
|
||||||
const ExecutionEnvironmentsAPI = new ExecutionEnvironments();
|
const ExecutionEnvironmentsAPI = new ExecutionEnvironments();
|
||||||
const GroupsAPI = new Groups();
|
const GroupsAPI = new Groups();
|
||||||
@@ -101,6 +103,7 @@ export {
|
|||||||
CredentialInputSourcesAPI,
|
CredentialInputSourcesAPI,
|
||||||
CredentialTypesAPI,
|
CredentialTypesAPI,
|
||||||
CredentialsAPI,
|
CredentialsAPI,
|
||||||
|
ConstructedInventoriesAPI,
|
||||||
DashboardAPI,
|
DashboardAPI,
|
||||||
ExecutionEnvironmentsAPI,
|
ExecutionEnvironmentsAPI,
|
||||||
GroupsAPI,
|
GroupsAPI,
|
||||||
|
|||||||
11
awx/ui/src/api/models/ConstructedInventories.js
Normal file
11
awx/ui/src/api/models/ConstructedInventories.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import Base from '../Base';
|
||||||
|
import InstanceGroupsMixin from '../mixins/InstanceGroups.mixin';
|
||||||
|
|
||||||
|
class ConstructedInventories extends InstanceGroupsMixin(Base) {
|
||||||
|
constructor(http) {
|
||||||
|
super(http);
|
||||||
|
this.baseUrl = 'api/v2/constructed_inventories/';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ConstructedInventories;
|
||||||
@@ -13,6 +13,7 @@ class Inventories extends InstanceGroupsMixin(Base) {
|
|||||||
this.readGroups = this.readGroups.bind(this);
|
this.readGroups = this.readGroups.bind(this);
|
||||||
this.readGroupsOptions = this.readGroupsOptions.bind(this);
|
this.readGroupsOptions = this.readGroupsOptions.bind(this);
|
||||||
this.promoteGroup = this.promoteGroup.bind(this);
|
this.promoteGroup = this.promoteGroup.bind(this);
|
||||||
|
this.readSourceInventories = this.readSourceInventories.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
readAccessList(id, params) {
|
readAccessList(id, params) {
|
||||||
@@ -72,6 +73,12 @@ class Inventories extends InstanceGroupsMixin(Base) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readSourceInventories(inventoryId, params) {
|
||||||
|
return this.http.get(`${this.baseUrl}${inventoryId}/source_inventories/`, {
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
readSources(inventoryId, params) {
|
readSources(inventoryId, params) {
|
||||||
return this.http.get(`${this.baseUrl}${inventoryId}/inventory_sources/`, {
|
return this.http.get(`${this.baseUrl}${inventoryId}/inventory_sources/`, {
|
||||||
params,
|
params,
|
||||||
|
|||||||
206
awx/ui/src/screens/Inventory/ConstructedInventory.js
Normal file
206
awx/ui/src/screens/Inventory/ConstructedInventory.js
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
import React, { useCallback, useEffect } from '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 useRequest from 'hooks/useRequest';
|
||||||
|
import { ConstructedInventoriesAPI, InventoriesAPI } from 'api';
|
||||||
|
|
||||||
|
import ContentError from 'components/ContentError';
|
||||||
|
import ContentLoading from 'components/ContentLoading';
|
||||||
|
import JobList from 'components/JobList';
|
||||||
|
import RelatedTemplateList from 'components/RelatedTemplateList';
|
||||||
|
import { ResourceAccessList } from 'components/ResourceAccessList';
|
||||||
|
import RoutedTabs from 'components/RoutedTabs';
|
||||||
|
import ConstructedInventoryDetail from './ConstructedInventoryDetail';
|
||||||
|
import ConstructedInventoryEdit from './ConstructedInventoryEdit';
|
||||||
|
import ConstructedInventoryGroups from './ConstructedInventoryGroups';
|
||||||
|
import ConstructedInventoryHosts from './ConstructedInventoryHosts';
|
||||||
|
import { getInventoryPath } from './shared/utils';
|
||||||
|
|
||||||
|
function ConstructedInventory({ setBreadcrumb }) {
|
||||||
|
const location = useLocation();
|
||||||
|
const match = useRouteMatch('/inventories/constructed_inventory/:id');
|
||||||
|
|
||||||
|
const {
|
||||||
|
result: inventory,
|
||||||
|
error: contentError,
|
||||||
|
isLoading: hasContentLoading,
|
||||||
|
request: fetchInventory,
|
||||||
|
} = useRequest(
|
||||||
|
useCallback(async () => {
|
||||||
|
const { data } = await ConstructedInventoriesAPI.readDetail(
|
||||||
|
match.params.id
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
}, [match.params.id]),
|
||||||
|
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchInventory();
|
||||||
|
}, [fetchInventory, location.pathname]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (inventory) {
|
||||||
|
setBreadcrumb(inventory);
|
||||||
|
}
|
||||||
|
}, [inventory, setBreadcrumb]);
|
||||||
|
|
||||||
|
const tabsArray = [
|
||||||
|
{
|
||||||
|
name: (
|
||||||
|
<>
|
||||||
|
<CaretLeftIcon />
|
||||||
|
{t`Back to Inventories`}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
link: `/inventories`,
|
||||||
|
id: 99,
|
||||||
|
},
|
||||||
|
{ name: t`Details`, link: `${match.url}/details`, id: 0 },
|
||||||
|
{ name: t`Access`, link: `${match.url}/access`, id: 1 },
|
||||||
|
{ name: t`Hosts`, link: `${match.url}/hosts`, id: 2 },
|
||||||
|
{ name: t`Groups`, link: `${match.url}/groups`, id: 3 },
|
||||||
|
{
|
||||||
|
name: t`Jobs`,
|
||||||
|
link: `${match.url}/jobs`,
|
||||||
|
id: 4,
|
||||||
|
},
|
||||||
|
{ name: t`Job Templates`, link: `${match.url}/job_templates`, id: 5 },
|
||||||
|
];
|
||||||
|
|
||||||
|
if (hasContentLoading) {
|
||||||
|
return (
|
||||||
|
<PageSection>
|
||||||
|
<Card>
|
||||||
|
<ContentLoading />
|
||||||
|
</Card>
|
||||||
|
</PageSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contentError) {
|
||||||
|
return (
|
||||||
|
<PageSection>
|
||||||
|
<Card>
|
||||||
|
<ContentError error={contentError}>
|
||||||
|
{contentError?.response?.status === 404 && (
|
||||||
|
<span>
|
||||||
|
{t`Constructed Inventory not found.`}{' '}
|
||||||
|
<Link to="/inventories">{t`View all Inventories.`}</Link>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</ContentError>
|
||||||
|
</Card>
|
||||||
|
</PageSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inventory && inventory?.kind !== 'constructed') {
|
||||||
|
return <Redirect to={`${getInventoryPath(inventory)}/details`} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
let showCardHeader = true;
|
||||||
|
if (['edit'].some((name) => location.pathname.includes(name))) {
|
||||||
|
showCardHeader = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageSection>
|
||||||
|
<Card>
|
||||||
|
{showCardHeader && <RoutedTabs tabsArray={tabsArray} />}
|
||||||
|
<Switch>
|
||||||
|
<Redirect
|
||||||
|
from="/inventories/constructed_inventory/:id"
|
||||||
|
to="/inventories/constructed_inventory/:id/details"
|
||||||
|
exact
|
||||||
|
/>
|
||||||
|
{inventory && [
|
||||||
|
<Route
|
||||||
|
path="/inventories/constructed_inventory/:id/details"
|
||||||
|
key="details"
|
||||||
|
>
|
||||||
|
<ConstructedInventoryDetail
|
||||||
|
inventory={inventory}
|
||||||
|
hasInventoryLoading={hasContentLoading}
|
||||||
|
/>
|
||||||
|
</Route>,
|
||||||
|
<Route
|
||||||
|
key="edit"
|
||||||
|
path="/inventories/constructed_inventory/:id/edit"
|
||||||
|
>
|
||||||
|
<ConstructedInventoryEdit />
|
||||||
|
</Route>,
|
||||||
|
<Route
|
||||||
|
path="/inventories/constructed_inventory/:id/access"
|
||||||
|
key="access"
|
||||||
|
>
|
||||||
|
<ResourceAccessList
|
||||||
|
resource={inventory}
|
||||||
|
apiModel={InventoriesAPI}
|
||||||
|
/>
|
||||||
|
</Route>,
|
||||||
|
<Route
|
||||||
|
path="/inventories/constructed_inventory/:id/hosts"
|
||||||
|
key="hosts"
|
||||||
|
>
|
||||||
|
<ConstructedInventoryHosts />
|
||||||
|
</Route>,
|
||||||
|
<Route
|
||||||
|
path="/inventories/constructed_inventory/:id/groups"
|
||||||
|
key="groups"
|
||||||
|
>
|
||||||
|
<ConstructedInventoryGroups />
|
||||||
|
</Route>,
|
||||||
|
<Route
|
||||||
|
key="jobs"
|
||||||
|
path="/inventories/constructed_inventory/:id/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="job_templates"
|
||||||
|
path="/inventories/constructed_inventory/:id/job_templates"
|
||||||
|
>
|
||||||
|
<RelatedTemplateList
|
||||||
|
searchParams={{ inventory__id: inventory.id }}
|
||||||
|
/>
|
||||||
|
</Route>,
|
||||||
|
]}
|
||||||
|
<Route path="*" key="not-found">
|
||||||
|
<ContentError isNotFound>
|
||||||
|
{match.params.id && (
|
||||||
|
<Link
|
||||||
|
to={`/inventories/constructed_inventory/${match.params.id}/details`}
|
||||||
|
>
|
||||||
|
{t`View Constructed Inventory Details`}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</ContentError>
|
||||||
|
</Route>
|
||||||
|
</Switch>
|
||||||
|
</Card>
|
||||||
|
</PageSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { ConstructedInventory as _ConstructedInventory };
|
||||||
|
export default ConstructedInventory;
|
||||||
73
awx/ui/src/screens/Inventory/ConstructedInventory.test.js
Normal file
73
awx/ui/src/screens/Inventory/ConstructedInventory.test.js
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { createMemoryHistory } from 'history';
|
||||||
|
import { ConstructedInventoriesAPI } from 'api';
|
||||||
|
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
||||||
|
import mockInventory from './shared/data.inventory.json';
|
||||||
|
import ConstructedInventory from './ConstructedInventory';
|
||||||
|
|
||||||
|
jest.mock('../../api');
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
...jest.requireActual('react-router-dom'),
|
||||||
|
useRouteMatch: () => ({
|
||||||
|
url: '/constructed_inventories/1',
|
||||||
|
params: { id: 1 },
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('<ConstructedInventory />', () => {
|
||||||
|
let wrapper;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
ConstructedInventoriesAPI.readDetail.mockResolvedValue({
|
||||||
|
data: mockInventory,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render expected tabs', async () => {
|
||||||
|
const expectedTabs = [
|
||||||
|
'Back to Inventories',
|
||||||
|
'Details',
|
||||||
|
'Access',
|
||||||
|
'Hosts',
|
||||||
|
'Groups',
|
||||||
|
'Jobs',
|
||||||
|
'Job Templates',
|
||||||
|
];
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<ConstructedInventory setBreadcrumb={() => {}} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
wrapper.find('RoutedTabs li').forEach((tab, index) => {
|
||||||
|
expect(tab.text()).toEqual(expectedTabs[index]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show content error when user attempts to navigate to erroneous route', async () => {
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/inventories/constructed_inventory/1/foobar'],
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<ConstructedInventory setBreadcrumb={() => {}} />,
|
||||||
|
{
|
||||||
|
context: {
|
||||||
|
router: {
|
||||||
|
history,
|
||||||
|
route: {
|
||||||
|
location: history.location,
|
||||||
|
match: {
|
||||||
|
params: { id: 1 },
|
||||||
|
url: '/inventories/constructed_inventory/1/foobar',
|
||||||
|
path: '/inventories/constructed_inventory/1/foobar',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
expect(wrapper.find('ContentError').length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
/* eslint i18next/no-literal-string: "off" */
|
||||||
|
import React from 'react';
|
||||||
|
import { Card, PageSection } from '@patternfly/react-core';
|
||||||
|
import { CardBody } from 'components/Card';
|
||||||
|
|
||||||
|
function ConstructedInventoryAdd() {
|
||||||
|
return (
|
||||||
|
<PageSection>
|
||||||
|
<Card>
|
||||||
|
<CardBody>
|
||||||
|
<div>Coming Soon!</div>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</PageSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ConstructedInventoryAdd;
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||||
|
import ConstructedInventoryAdd from './ConstructedInventoryAdd';
|
||||||
|
|
||||||
|
describe('<ConstructedInventoryAdd />', () => {
|
||||||
|
test('initially renders successfully', async () => {
|
||||||
|
let wrapper;
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<ConstructedInventoryAdd />);
|
||||||
|
});
|
||||||
|
expect(wrapper.length).toBe(1);
|
||||||
|
expect(wrapper.find('ConstructedInventoryAdd').length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './ConstructedInventoryAdd';
|
||||||
@@ -0,0 +1,288 @@
|
|||||||
|
import React, { useCallback, useEffect } from 'react';
|
||||||
|
import { Link, useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Chip,
|
||||||
|
TextList,
|
||||||
|
TextListItem,
|
||||||
|
TextListItemVariants,
|
||||||
|
TextListVariants,
|
||||||
|
} from '@patternfly/react-core';
|
||||||
|
import AlertModal from 'components/AlertModal';
|
||||||
|
import { CardBody, CardActionsRow } from 'components/Card';
|
||||||
|
import { DetailList, Detail, UserDateDetail } from 'components/DetailList';
|
||||||
|
import { VariablesDetail } from 'components/CodeEditor';
|
||||||
|
import DeleteButton from 'components/DeleteButton';
|
||||||
|
import ErrorDetail from 'components/ErrorDetail';
|
||||||
|
import ContentError from 'components/ContentError';
|
||||||
|
import ContentLoading from 'components/ContentLoading';
|
||||||
|
import ChipGroup from 'components/ChipGroup';
|
||||||
|
import Popover from 'components/Popover';
|
||||||
|
import { InventoriesAPI, ConstructedInventoriesAPI } from 'api';
|
||||||
|
import useRequest, { useDismissableError } from 'hooks/useRequest';
|
||||||
|
import { Inventory } from 'types';
|
||||||
|
import { relatedResourceDeleteRequests } from 'util/getRelatedResourceDeleteDetails';
|
||||||
|
import InstanceGroupLabels from 'components/InstanceGroupLabels';
|
||||||
|
import getHelpText from '../shared/Inventory.helptext';
|
||||||
|
|
||||||
|
function ConstructedInventoryDetail({ inventory }) {
|
||||||
|
const history = useHistory();
|
||||||
|
const helpText = getHelpText();
|
||||||
|
|
||||||
|
const {
|
||||||
|
result: { instanceGroups, sourceInventories, actions },
|
||||||
|
request: fetchRelatedDetails,
|
||||||
|
error: contentError,
|
||||||
|
isLoading,
|
||||||
|
} = useRequest(
|
||||||
|
useCallback(async () => {
|
||||||
|
const [response, sourceInvResponse, options] = await Promise.all([
|
||||||
|
InventoriesAPI.readInstanceGroups(inventory.id),
|
||||||
|
InventoriesAPI.readSourceInventories(inventory.id),
|
||||||
|
ConstructedInventoriesAPI.readOptions(inventory.id),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
instanceGroups: response.data.results,
|
||||||
|
sourceInventories: sourceInvResponse.data.results,
|
||||||
|
actions: options.data.actions.GET,
|
||||||
|
};
|
||||||
|
}, [inventory.id]),
|
||||||
|
{
|
||||||
|
instanceGroups: [],
|
||||||
|
sourceInventories: [],
|
||||||
|
actions: {},
|
||||||
|
isLoading: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchRelatedDetails();
|
||||||
|
}, [fetchRelatedDetails]);
|
||||||
|
|
||||||
|
const { request: deleteInventory, error: deleteError } = useRequest(
|
||||||
|
useCallback(async () => {
|
||||||
|
await InventoriesAPI.destroy(inventory.id);
|
||||||
|
history.push(`/inventories`);
|
||||||
|
}, [inventory.id, history])
|
||||||
|
);
|
||||||
|
|
||||||
|
const { error, dismissError } = useDismissableError(deleteError);
|
||||||
|
|
||||||
|
const { organization, user_capabilities: userCapabilities } =
|
||||||
|
inventory.summary_fields;
|
||||||
|
|
||||||
|
const deleteDetailsRequests =
|
||||||
|
relatedResourceDeleteRequests.inventory(inventory);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <ContentLoading />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contentError) {
|
||||||
|
return <ContentError error={contentError} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CardBody>
|
||||||
|
<DetailList>
|
||||||
|
<Detail
|
||||||
|
label={t`Name`}
|
||||||
|
value={inventory.name}
|
||||||
|
dataCy="constructed-inventory-name"
|
||||||
|
/>
|
||||||
|
<Detail
|
||||||
|
label={t`Description`}
|
||||||
|
value={inventory.description}
|
||||||
|
dataCy="constructed-inventory-description"
|
||||||
|
/>
|
||||||
|
<Detail
|
||||||
|
label={t`Type`}
|
||||||
|
value={t`Constructed Inventory`}
|
||||||
|
dataCy="constructed-inventory-type"
|
||||||
|
/>
|
||||||
|
<Detail
|
||||||
|
label={actions.limit.label}
|
||||||
|
value={inventory.limit}
|
||||||
|
helpText={actions.limit.help_text}
|
||||||
|
dataCy="constructed-inventory-limit"
|
||||||
|
/>
|
||||||
|
<Detail
|
||||||
|
label={t`Organization`}
|
||||||
|
dataCy="constructed-inventory-organization"
|
||||||
|
value={
|
||||||
|
<Link to={`/organizations/${organization.id}/details`}>
|
||||||
|
{organization.name}
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Detail
|
||||||
|
label={actions.total_groups.label}
|
||||||
|
value={inventory.total_groups}
|
||||||
|
helpText={actions.total_groups.help_text}
|
||||||
|
dataCy="constructed-inventory-total-groups"
|
||||||
|
/>
|
||||||
|
<Detail
|
||||||
|
label={actions.total_hosts.label}
|
||||||
|
value={inventory.total_hosts}
|
||||||
|
helpText={actions.total_hosts.help_text}
|
||||||
|
dataCy="constructed-inventory-total-hosts"
|
||||||
|
/>
|
||||||
|
<Detail
|
||||||
|
label={actions.total_inventory_sources.label}
|
||||||
|
value={inventory.total_inventory_sources}
|
||||||
|
helpText={actions.total_inventory_sources.help_text}
|
||||||
|
dataCy="constructed-inventory-sources"
|
||||||
|
/>
|
||||||
|
<Detail
|
||||||
|
label={actions.update_cache_timeout.label}
|
||||||
|
value={inventory.update_cache_timeout}
|
||||||
|
helpText={actions.update_cache_timeout.help_text}
|
||||||
|
dataCy="constructed-inventory-cache-timeout"
|
||||||
|
/>
|
||||||
|
<Detail
|
||||||
|
label={actions.inventory_sources_with_failures.label}
|
||||||
|
value={inventory.inventory_sources_with_failures}
|
||||||
|
helpText={actions.inventory_sources_with_failures.help_text}
|
||||||
|
dataCy="constructed-inventory-sources-with-failures"
|
||||||
|
/>
|
||||||
|
<Detail
|
||||||
|
label={actions.verbosity.label}
|
||||||
|
value={inventory.verbosity}
|
||||||
|
helpText={actions.verbosity.help_text}
|
||||||
|
dataCy="constructed-inventory-verbosity"
|
||||||
|
/>
|
||||||
|
{instanceGroups && (
|
||||||
|
<Detail
|
||||||
|
fullWidth
|
||||||
|
label={t`Instance Groups`}
|
||||||
|
value={<InstanceGroupLabels labels={instanceGroups} isLinkable />}
|
||||||
|
isEmpty={instanceGroups.length === 0}
|
||||||
|
dataCy="constructed-inventory-instance-groups"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{inventory.prevent_instance_group_fallback && (
|
||||||
|
<Detail
|
||||||
|
fullWidth
|
||||||
|
label={t`Enabled Options`}
|
||||||
|
dataCy="constructed-inventory-instance-group-fallback"
|
||||||
|
value={
|
||||||
|
<TextList component={TextListVariants.ul}>
|
||||||
|
{inventory.prevent_instance_group_fallback && (
|
||||||
|
<TextListItem component={TextListItemVariants.li}>
|
||||||
|
{t`Prevent Instance Group Fallback`}
|
||||||
|
<Popover
|
||||||
|
header={t`Prevent Instance Group Fallback`}
|
||||||
|
content={helpText.preventInstanceGroupFallback}
|
||||||
|
/>
|
||||||
|
</TextListItem>
|
||||||
|
)}
|
||||||
|
</TextList>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Detail
|
||||||
|
fullWidth
|
||||||
|
helpText={helpText.labels}
|
||||||
|
dataCy="constructed-inventory-labels"
|
||||||
|
label={t`Labels`}
|
||||||
|
value={
|
||||||
|
<ChipGroup
|
||||||
|
numChips={5}
|
||||||
|
totalChips={inventory.summary_fields.labels?.results?.length}
|
||||||
|
>
|
||||||
|
{inventory.summary_fields.labels?.results?.map((l) => (
|
||||||
|
<Chip key={l.id} isReadOnly>
|
||||||
|
{l.name}
|
||||||
|
</Chip>
|
||||||
|
))}
|
||||||
|
</ChipGroup>
|
||||||
|
}
|
||||||
|
isEmpty={inventory.summary_fields.labels?.results?.length === 0}
|
||||||
|
/>
|
||||||
|
<Detail
|
||||||
|
fullWidth
|
||||||
|
label={t`Source Inventories`}
|
||||||
|
value={
|
||||||
|
<ChipGroup
|
||||||
|
numChips={5}
|
||||||
|
totalChips={sourceInventories?.length}
|
||||||
|
ouiaId="source-inventory-chips"
|
||||||
|
>
|
||||||
|
{sourceInventories?.map((sourceInventory) => (
|
||||||
|
<Link
|
||||||
|
key={sourceInventory.id}
|
||||||
|
to={`/inventories/inventory/${sourceInventory.id}/details`}
|
||||||
|
>
|
||||||
|
<Chip key={sourceInventory.id} isReadOnly>
|
||||||
|
{sourceInventory.name}
|
||||||
|
</Chip>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</ChipGroup>
|
||||||
|
}
|
||||||
|
isEmpty={sourceInventories?.length === 0}
|
||||||
|
/>
|
||||||
|
<VariablesDetail
|
||||||
|
label={actions.source_vars.label}
|
||||||
|
helpText={helpText.variables()}
|
||||||
|
value={inventory.source_vars}
|
||||||
|
rows={4}
|
||||||
|
name="variables"
|
||||||
|
dataCy="inventory-detail-variables"
|
||||||
|
/>
|
||||||
|
<UserDateDetail
|
||||||
|
label={actions.created.label}
|
||||||
|
date={inventory.created}
|
||||||
|
user={inventory.summary_fields.created_by}
|
||||||
|
/>
|
||||||
|
<UserDateDetail
|
||||||
|
label={actions.modified.label}
|
||||||
|
date={inventory.modified}
|
||||||
|
user={inventory.summary_fields.modified_by}
|
||||||
|
/>
|
||||||
|
</DetailList>
|
||||||
|
<CardActionsRow>
|
||||||
|
{userCapabilities.edit && (
|
||||||
|
<Button
|
||||||
|
ouiaId="inventory-detail-edit-button"
|
||||||
|
component={Link}
|
||||||
|
to={`/inventories/constructed_inventory/${inventory.id}/edit`}
|
||||||
|
>
|
||||||
|
{t`Edit`}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{userCapabilities.delete && (
|
||||||
|
<DeleteButton
|
||||||
|
name={inventory.name}
|
||||||
|
modalTitle={t`Delete Inventory`}
|
||||||
|
onConfirm={deleteInventory}
|
||||||
|
deleteDetailsRequests={deleteDetailsRequests}
|
||||||
|
deleteMessage={t`This inventory is currently being used by other resources. Are you sure you want to delete it?`}
|
||||||
|
>
|
||||||
|
{t`Delete`}
|
||||||
|
</DeleteButton>
|
||||||
|
)}
|
||||||
|
</CardActionsRow>
|
||||||
|
{error && (
|
||||||
|
<AlertModal
|
||||||
|
isOpen={error}
|
||||||
|
variant="error"
|
||||||
|
title={t`Error!`}
|
||||||
|
onClose={dismissError}
|
||||||
|
>
|
||||||
|
{t`Failed to delete inventory.`}
|
||||||
|
<ErrorDetail error={error} />
|
||||||
|
</AlertModal>
|
||||||
|
)}
|
||||||
|
</CardBody>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ConstructedInventoryDetail.propTypes = {
|
||||||
|
inventory: Inventory.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConstructedInventoryDetail;
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { InventoriesAPI, CredentialTypesAPI } from 'api';
|
||||||
|
|
||||||
|
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||||
|
import ConstructedInventoryDetail from './ConstructedInventoryDetail';
|
||||||
|
|
||||||
|
jest.mock('../../../api');
|
||||||
|
|
||||||
|
const mockInventory = {
|
||||||
|
id: 1,
|
||||||
|
type: 'inventory',
|
||||||
|
summary_fields: {
|
||||||
|
organization: {
|
||||||
|
id: 1,
|
||||||
|
name: 'The Organization',
|
||||||
|
description: '',
|
||||||
|
},
|
||||||
|
created_by: {
|
||||||
|
username: 'the_creator',
|
||||||
|
id: 2,
|
||||||
|
},
|
||||||
|
modified_by: {
|
||||||
|
username: 'the_modifier',
|
||||||
|
id: 3,
|
||||||
|
},
|
||||||
|
user_capabilities: {
|
||||||
|
edit: true,
|
||||||
|
delete: true,
|
||||||
|
copy: true,
|
||||||
|
adhoc: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created: '2019-10-04T16:56:48.025455Z',
|
||||||
|
modified: '2019-10-04T16:56:48.025468Z',
|
||||||
|
name: 'Constructed Inv',
|
||||||
|
description: '',
|
||||||
|
organization: 1,
|
||||||
|
kind: 'constructed',
|
||||||
|
has_active_failures: false,
|
||||||
|
total_hosts: 0,
|
||||||
|
hosts_with_active_failures: 0,
|
||||||
|
total_groups: 0,
|
||||||
|
groups_with_active_failures: 0,
|
||||||
|
has_inventory_sources: false,
|
||||||
|
total_inventory_sources: 0,
|
||||||
|
inventory_sources_with_failures: 0,
|
||||||
|
pending_deletion: false,
|
||||||
|
prevent_instance_group_fallback: false,
|
||||||
|
update_cache_timeout: 0,
|
||||||
|
limit: '',
|
||||||
|
verbosity: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('<ConstructedInventoryDetail />', () => {
|
||||||
|
test('initially renders successfully', async () => {
|
||||||
|
let wrapper;
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<ConstructedInventoryDetail inventory={mockInventory} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
expect(wrapper.length).toBe(1);
|
||||||
|
expect(wrapper.find('ConstructedInventoryDetail').length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './ConstructedInventoryDetail';
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
/* eslint i18next/no-literal-string: "off" */
|
||||||
|
import React from 'react';
|
||||||
|
import { CardBody } from 'components/Card';
|
||||||
|
|
||||||
|
function ConstructedInventoryEdit() {
|
||||||
|
return (
|
||||||
|
<CardBody>
|
||||||
|
<div>Coming Soon!</div>
|
||||||
|
</CardBody>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ConstructedInventoryEdit;
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||||
|
import ConstructedInventoryEdit from './ConstructedInventoryEdit';
|
||||||
|
|
||||||
|
describe('<ConstructedInventoryEdit />', () => {
|
||||||
|
test('initially renders successfully', async () => {
|
||||||
|
let wrapper;
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<ConstructedInventoryEdit />);
|
||||||
|
});
|
||||||
|
expect(wrapper.length).toBe(1);
|
||||||
|
expect(wrapper.find('ConstructedInventoryEdit').length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './ConstructedInventoryEdit';
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
/* eslint i18next/no-literal-string: "off" */
|
||||||
|
import React from 'react';
|
||||||
|
import { CardBody } from 'components/Card';
|
||||||
|
|
||||||
|
function ConstructedInventoryGroups() {
|
||||||
|
return (
|
||||||
|
<CardBody>
|
||||||
|
<div>Coming Soon!</div>
|
||||||
|
</CardBody>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ConstructedInventoryGroups;
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||||
|
import ConstructedInventoryGroups from './ConstructedInventoryGroups';
|
||||||
|
|
||||||
|
describe('<ConstructedInventoryGroups />', () => {
|
||||||
|
test('initially renders successfully', async () => {
|
||||||
|
let wrapper;
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<ConstructedInventoryGroups />);
|
||||||
|
});
|
||||||
|
expect(wrapper.length).toBe(1);
|
||||||
|
expect(wrapper.find('ConstructedInventoryGroups').length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './ConstructedInventoryGroups';
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
/* eslint i18next/no-literal-string: "off" */
|
||||||
|
import React from 'react';
|
||||||
|
import { CardBody } from 'components/Card';
|
||||||
|
|
||||||
|
function ConstructedInventoryHosts() {
|
||||||
|
return (
|
||||||
|
<CardBody>
|
||||||
|
<div>Coming Soon!</div>
|
||||||
|
</CardBody>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ConstructedInventoryHosts;
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
||||||
|
import ConstructedInventoryHosts from './ConstructedInventoryHosts';
|
||||||
|
|
||||||
|
describe('<ConstructedInventoryHosts />', () => {
|
||||||
|
test('initially renders successfully', async () => {
|
||||||
|
let wrapper;
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<ConstructedInventoryHosts />);
|
||||||
|
});
|
||||||
|
expect(wrapper.length).toBe(1);
|
||||||
|
expect(wrapper.find('ConstructedInventoryHosts').length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './ConstructedInventoryHosts';
|
||||||
@@ -9,14 +9,18 @@ import PersistentFilters from 'components/PersistentFilters';
|
|||||||
import { InventoryList } from './InventoryList';
|
import { InventoryList } from './InventoryList';
|
||||||
import Inventory from './Inventory';
|
import Inventory from './Inventory';
|
||||||
import SmartInventory from './SmartInventory';
|
import SmartInventory from './SmartInventory';
|
||||||
|
import ConstructedInventory from './ConstructedInventory';
|
||||||
import InventoryAdd from './InventoryAdd';
|
import InventoryAdd from './InventoryAdd';
|
||||||
import SmartInventoryAdd from './SmartInventoryAdd';
|
import SmartInventoryAdd from './SmartInventoryAdd';
|
||||||
|
import ConstructedInventoryAdd from './ConstructedInventoryAdd';
|
||||||
|
import { getInventoryPath } from './shared/utils';
|
||||||
|
|
||||||
function Inventories() {
|
function Inventories() {
|
||||||
const initScreenHeader = useRef({
|
const initScreenHeader = useRef({
|
||||||
'/inventories': t`Inventories`,
|
'/inventories': t`Inventories`,
|
||||||
'/inventories/inventory/add': t`Create new inventory`,
|
'/inventories/inventory/add': t`Create new inventory`,
|
||||||
'/inventories/smart_inventory/add': t`Create new smart inventory`,
|
'/inventories/smart_inventory/add': t`Create new smart inventory`,
|
||||||
|
'/inventories/constructed_inventory/add': t`Create new constructed inventory`,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [breadcrumbConfig, setScreenHeader] = useState(
|
const [breadcrumbConfig, setScreenHeader] = useState(
|
||||||
@@ -45,10 +49,7 @@ function Inventories() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const inventoryKind =
|
const inventoryPath = getInventoryPath(inventory);
|
||||||
inventory.kind === 'smart' ? 'smart_inventory' : 'inventory';
|
|
||||||
|
|
||||||
const inventoryPath = `/inventories/${inventoryKind}/${inventory.id}`;
|
|
||||||
const inventoryHostsPath = `${inventoryPath}/hosts`;
|
const inventoryHostsPath = `${inventoryPath}/hosts`;
|
||||||
const inventoryGroupsPath = `${inventoryPath}/groups`;
|
const inventoryGroupsPath = `${inventoryPath}/groups`;
|
||||||
const inventorySourcesPath = `${inventoryPath}/sources`;
|
const inventorySourcesPath = `${inventoryPath}/sources`;
|
||||||
@@ -109,6 +110,9 @@ function Inventories() {
|
|||||||
<Route path="/inventories/smart_inventory/add">
|
<Route path="/inventories/smart_inventory/add">
|
||||||
<SmartInventoryAdd />
|
<SmartInventoryAdd />
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route path="/inventories/constructed_inventory/add">
|
||||||
|
<ConstructedInventoryAdd />
|
||||||
|
</Route>
|
||||||
<Route path="/inventories/inventory/:id">
|
<Route path="/inventories/inventory/:id">
|
||||||
<Config>
|
<Config>
|
||||||
{({ me }) => (
|
{({ me }) => (
|
||||||
@@ -119,6 +123,9 @@ function Inventories() {
|
|||||||
<Route path="/inventories/smart_inventory/:id">
|
<Route path="/inventories/smart_inventory/:id">
|
||||||
<SmartInventory setBreadcrumb={setBreadcrumbConfig} />
|
<SmartInventory setBreadcrumb={setBreadcrumbConfig} />
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route path="/inventories/constructed_inventory/:id">
|
||||||
|
<ConstructedInventory setBreadcrumb={setBreadcrumbConfig} />
|
||||||
|
</Route>
|
||||||
<Route path="/inventories">
|
<Route path="/inventories">
|
||||||
<PersistentFilters pageKey="inventories">
|
<PersistentFilters pageKey="inventories">
|
||||||
<InventoryList />
|
<InventoryList />
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import InventoryEdit from './InventoryEdit';
|
|||||||
import InventoryGroups from './InventoryGroups';
|
import InventoryGroups from './InventoryGroups';
|
||||||
import InventoryHosts from './InventoryHosts/InventoryHosts';
|
import InventoryHosts from './InventoryHosts/InventoryHosts';
|
||||||
import InventorySources from './InventorySources';
|
import InventorySources from './InventorySources';
|
||||||
|
import { getInventoryPath } from './shared/utils';
|
||||||
|
|
||||||
function Inventory({ setBreadcrumb }) {
|
function Inventory({ setBreadcrumb }) {
|
||||||
const [contentError, setContentError] = useState(null);
|
const [contentError, setContentError] = useState(null);
|
||||||
@@ -111,10 +112,8 @@ function Inventory({ setBreadcrumb }) {
|
|||||||
showCardHeader = false;
|
showCardHeader = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inventory?.kind === 'smart') {
|
if (inventory && inventory?.kind !== '') {
|
||||||
return (
|
return <Redirect to={`${getInventoryPath(inventory)}/details`} />;
|
||||||
<Redirect to={`/inventories/smart_inventory/${inventory.id}/details`} />
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ function InventoryList() {
|
|||||||
|
|
||||||
const addInventory = t`Add inventory`;
|
const addInventory = t`Add inventory`;
|
||||||
const addSmartInventory = t`Add smart inventory`;
|
const addSmartInventory = t`Add smart inventory`;
|
||||||
|
const addConstructedInventory = t`Add constructed inventory`;
|
||||||
const addButton = (
|
const addButton = (
|
||||||
<AddDropDownButton
|
<AddDropDownButton
|
||||||
ouiaId="add-inventory-button"
|
ouiaId="add-inventory-button"
|
||||||
@@ -158,6 +159,15 @@ function InventoryList() {
|
|||||||
>
|
>
|
||||||
{addSmartInventory}
|
{addSmartInventory}
|
||||||
</DropdownItem>,
|
</DropdownItem>,
|
||||||
|
<DropdownItem
|
||||||
|
ouiaId="add-constructed-inventory-item"
|
||||||
|
to={`${match.url}/constructed_inventory/add/`}
|
||||||
|
component={Link}
|
||||||
|
key={addConstructedInventory}
|
||||||
|
aria-label={addConstructedInventory}
|
||||||
|
>
|
||||||
|
{addConstructedInventory}
|
||||||
|
</DropdownItem>,
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -261,11 +271,6 @@ function InventoryList() {
|
|||||||
inventory={inventory}
|
inventory={inventory}
|
||||||
rowIndex={index}
|
rowIndex={index}
|
||||||
fetchInventories={fetchInventories}
|
fetchInventories={fetchInventories}
|
||||||
detailUrl={
|
|
||||||
inventory.kind === 'smart'
|
|
||||||
? `${match.url}/smart_inventory/${inventory.id}/details`
|
|
||||||
: `${match.url}/inventory/${inventory.id}/details`
|
|
||||||
}
|
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
if (!inventory.pending_deletion) {
|
if (!inventory.pending_deletion) {
|
||||||
handleSelect(inventory);
|
handleSelect(inventory);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useCallback } from 'react';
|
import React, { useState, useCallback } from 'react';
|
||||||
import { string, bool, func } from 'prop-types';
|
import { bool, func } from 'prop-types';
|
||||||
|
|
||||||
import { Button, Label } from '@patternfly/react-core';
|
import { Button, Label } from '@patternfly/react-core';
|
||||||
import { Tr, Td } from '@patternfly/react-table';
|
import { Tr, Td } from '@patternfly/react-table';
|
||||||
@@ -12,6 +12,7 @@ import { Inventory } from 'types';
|
|||||||
import { ActionsTd, ActionItem, TdBreakWord } from 'components/PaginatedTable';
|
import { ActionsTd, ActionItem, TdBreakWord } from 'components/PaginatedTable';
|
||||||
import CopyButton from 'components/CopyButton';
|
import CopyButton from 'components/CopyButton';
|
||||||
import StatusLabel from 'components/StatusLabel';
|
import StatusLabel from 'components/StatusLabel';
|
||||||
|
import { getInventoryPath } from '../shared/utils';
|
||||||
|
|
||||||
function InventoryListItem({
|
function InventoryListItem({
|
||||||
inventory,
|
inventory,
|
||||||
@@ -19,12 +20,10 @@ function InventoryListItem({
|
|||||||
isSelected,
|
isSelected,
|
||||||
onSelect,
|
onSelect,
|
||||||
onCopy,
|
onCopy,
|
||||||
detailUrl,
|
|
||||||
fetchInventories,
|
fetchInventories,
|
||||||
}) {
|
}) {
|
||||||
InventoryListItem.propTypes = {
|
InventoryListItem.propTypes = {
|
||||||
inventory: Inventory.isRequired,
|
inventory: Inventory.isRequired,
|
||||||
detailUrl: string.isRequired,
|
|
||||||
isSelected: bool.isRequired,
|
isSelected: bool.isRequired,
|
||||||
onSelect: func.isRequired,
|
onSelect: func.isRequired,
|
||||||
};
|
};
|
||||||
@@ -50,6 +49,12 @@ function InventoryListItem({
|
|||||||
|
|
||||||
const labelId = `check-action-${inventory.id}`;
|
const labelId = `check-action-${inventory.id}`;
|
||||||
|
|
||||||
|
const typeLabel = {
|
||||||
|
'': t`Inventory`,
|
||||||
|
smart: t`Smart Inventory`,
|
||||||
|
constructed: t`Constructed Inventory`,
|
||||||
|
};
|
||||||
|
|
||||||
let syncStatus = 'disabled';
|
let syncStatus = 'disabled';
|
||||||
if (inventory.isSourceSyncRunning) {
|
if (inventory.isSourceSyncRunning) {
|
||||||
syncStatus = 'syncing';
|
syncStatus = 'syncing';
|
||||||
@@ -93,16 +98,20 @@ function InventoryListItem({
|
|||||||
{inventory.pending_deletion ? (
|
{inventory.pending_deletion ? (
|
||||||
<b>{inventory.name}</b>
|
<b>{inventory.name}</b>
|
||||||
) : (
|
) : (
|
||||||
<Link to={`${detailUrl}`}>
|
<Link to={`${getInventoryPath(inventory)}/details`}>
|
||||||
<b>{inventory.name}</b>
|
<b>{inventory.name}</b>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</TdBreakWord>
|
</TdBreakWord>
|
||||||
<Td dataLabel={t`Status`}>
|
<Td dataLabel={t`Status`}>
|
||||||
{inventory.kind !== 'smart' &&
|
{inventory.kind === '' &&
|
||||||
(inventory.has_inventory_sources ? (
|
(inventory.has_inventory_sources ? (
|
||||||
<Link
|
<Link
|
||||||
to={`/inventories/inventory/${inventory.id}/jobs?job.or__inventoryupdate__inventory_source__inventory__id=${inventory.id}`}
|
to={`${getInventoryPath(
|
||||||
|
inventory
|
||||||
|
)}/jobs?job.or__inventoryupdate__inventory_source__inventory__id=${
|
||||||
|
inventory.id
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<StatusLabel
|
<StatusLabel
|
||||||
status={syncStatus}
|
status={syncStatus}
|
||||||
@@ -113,9 +122,7 @@ function InventoryListItem({
|
|||||||
<StatusLabel status={syncStatus} tooltipContent={tooltipContent} />
|
<StatusLabel status={syncStatus} tooltipContent={tooltipContent} />
|
||||||
))}
|
))}
|
||||||
</Td>
|
</Td>
|
||||||
<Td dataLabel={t`Type`}>
|
<Td dataLabel={t`Type`}>{typeLabel[inventory.kind]}</Td>
|
||||||
{inventory.kind === 'smart' ? t`Smart Inventory` : t`Inventory`}
|
|
||||||
</Td>
|
|
||||||
<TdBreakWord key="organization" dataLabel={t`Organization`}>
|
<TdBreakWord key="organization" dataLabel={t`Organization`}>
|
||||||
<Link
|
<Link
|
||||||
to={`/organizations/${inventory?.summary_fields?.organization?.id}/details`}
|
to={`/organizations/${inventory?.summary_fields?.organization?.id}/details`}
|
||||||
@@ -139,9 +146,7 @@ function InventoryListItem({
|
|||||||
aria-label={t`Edit Inventory`}
|
aria-label={t`Edit Inventory`}
|
||||||
variant="plain"
|
variant="plain"
|
||||||
component={Link}
|
component={Link}
|
||||||
to={`/inventories/${
|
to={`${getInventoryPath(inventory)}edit`}
|
||||||
inventory.kind === 'smart' ? 'smart_inventory' : 'inventory'
|
|
||||||
}/${inventory.id}/edit`}
|
|
||||||
>
|
>
|
||||||
<PencilAltIcon />
|
<PencilAltIcon />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import RelatedTemplateList from 'components/RelatedTemplateList';
|
|||||||
import SmartInventoryDetail from './SmartInventoryDetail';
|
import SmartInventoryDetail from './SmartInventoryDetail';
|
||||||
import SmartInventoryEdit from './SmartInventoryEdit';
|
import SmartInventoryEdit from './SmartInventoryEdit';
|
||||||
import SmartInventoryHosts from './SmartInventoryHosts';
|
import SmartInventoryHosts from './SmartInventoryHosts';
|
||||||
|
import { getInventoryPath } from './shared/utils';
|
||||||
|
|
||||||
function SmartInventory({ setBreadcrumb }) {
|
function SmartInventory({ setBreadcrumb }) {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@@ -101,8 +102,8 @@ function SmartInventory({ setBreadcrumb }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inventory?.kind === '') {
|
if (inventory && inventory?.kind !== 'smart') {
|
||||||
return <Redirect to={`/inventories/inventory/${inventory.id}/details`} />;
|
return <Redirect to={`${getInventoryPath(inventory)}/details`} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
let showCardHeader = true;
|
let showCardHeader = true;
|
||||||
|
|||||||
@@ -8,3 +8,12 @@ const parseHostFilter = (value) => {
|
|||||||
return value;
|
return value;
|
||||||
};
|
};
|
||||||
export default parseHostFilter;
|
export default parseHostFilter;
|
||||||
|
|
||||||
|
export function getInventoryPath(inventory) {
|
||||||
|
const url = {
|
||||||
|
'': `/inventories/inventory/${inventory.id}`,
|
||||||
|
smart: `/inventories/smart_inventory/${inventory.id}`,
|
||||||
|
constructed: `/inventories/constructed_inventory/${inventory.id}`,
|
||||||
|
};
|
||||||
|
return url[inventory.kind];
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import parseHostFilter from './utils';
|
import parseHostFilter, { getInventoryPath } from './utils';
|
||||||
|
|
||||||
describe('parseHostFilter', () => {
|
describe('parseHostFilter', () => {
|
||||||
test('parse host filter', () => {
|
test('parse host filter', () => {
|
||||||
@@ -19,3 +19,21 @@ describe('parseHostFilter', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getInventoryPath', () => {
|
||||||
|
test('should return inventory path', () => {
|
||||||
|
expect(getInventoryPath({ id: 1, kind: '' })).toMatch(
|
||||||
|
'/inventories/inventory/1'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
test('should return smart inventory path', () => {
|
||||||
|
expect(getInventoryPath({ id: 2, kind: 'smart' })).toMatch(
|
||||||
|
'/inventories/smart_inventory/2'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
test('should return constructed inventory path', () => {
|
||||||
|
expect(getInventoryPath({ id: 3, kind: 'constructed' })).toMatch(
|
||||||
|
'/inventories/constructed_inventory/3'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user