diff --git a/awx/ui/src/api/index.js b/awx/ui/src/api/index.js index 5281ad861d..a94f8bbc8d 100644 --- a/awx/ui/src/api/index.js +++ b/awx/ui/src/api/index.js @@ -6,6 +6,7 @@ import Config from './models/Config'; import CredentialInputSources from './models/CredentialInputSources'; import CredentialTypes from './models/CredentialTypes'; import Credentials from './models/Credentials'; +import ConstructedInventories from './models/ConstructedInventories'; import Dashboard from './models/Dashboard'; import ExecutionEnvironments from './models/ExecutionEnvironments'; import Groups from './models/Groups'; @@ -53,6 +54,7 @@ const ConfigAPI = new Config(); const CredentialInputSourcesAPI = new CredentialInputSources(); const CredentialTypesAPI = new CredentialTypes(); const CredentialsAPI = new Credentials(); +const ConstructedInventoriesAPI = new ConstructedInventories(); const DashboardAPI = new Dashboard(); const ExecutionEnvironmentsAPI = new ExecutionEnvironments(); const GroupsAPI = new Groups(); @@ -101,6 +103,7 @@ export { CredentialInputSourcesAPI, CredentialTypesAPI, CredentialsAPI, + ConstructedInventoriesAPI, DashboardAPI, ExecutionEnvironmentsAPI, GroupsAPI, diff --git a/awx/ui/src/api/models/ConstructedInventories.js b/awx/ui/src/api/models/ConstructedInventories.js new file mode 100644 index 0000000000..b62bffd3f3 --- /dev/null +++ b/awx/ui/src/api/models/ConstructedInventories.js @@ -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; diff --git a/awx/ui/src/api/models/Inventories.js b/awx/ui/src/api/models/Inventories.js index fd1653045f..3867277386 100644 --- a/awx/ui/src/api/models/Inventories.js +++ b/awx/ui/src/api/models/Inventories.js @@ -13,6 +13,7 @@ class Inventories extends InstanceGroupsMixin(Base) { this.readGroups = this.readGroups.bind(this); this.readGroupsOptions = this.readGroupsOptions.bind(this); this.promoteGroup = this.promoteGroup.bind(this); + this.readSourceInventories = this.readSourceInventories.bind(this); } 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) { return this.http.get(`${this.baseUrl}${inventoryId}/inventory_sources/`, { params, diff --git a/awx/ui/src/screens/Inventory/ConstructedInventory.js b/awx/ui/src/screens/Inventory/ConstructedInventory.js new file mode 100644 index 0000000000..58b33b96d2 --- /dev/null +++ b/awx/ui/src/screens/Inventory/ConstructedInventory.js @@ -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: ( + <> + + {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 ( + + + + + + ); + } + + if (contentError) { + return ( + + + + {contentError?.response?.status === 404 && ( + + {t`Constructed Inventory not found.`}{' '} + {t`View all Inventories.`} + + )} + + + + ); + } + + if (inventory && inventory?.kind !== 'constructed') { + return ; + } + + let showCardHeader = true; + if (['edit'].some((name) => location.pathname.includes(name))) { + showCardHeader = false; + } + + return ( + + + {showCardHeader && } + + + {inventory && [ + + + , + + + , + + + , + + + , + + + , + + + , + + + , + ]} + + + {match.params.id && ( + + {t`View Constructed Inventory Details`} + + )} + + + + + + ); +} + +export { ConstructedInventory as _ConstructedInventory }; +export default ConstructedInventory; diff --git a/awx/ui/src/screens/Inventory/ConstructedInventory.test.js b/awx/ui/src/screens/Inventory/ConstructedInventory.test.js new file mode 100644 index 0000000000..da4cd56c34 --- /dev/null +++ b/awx/ui/src/screens/Inventory/ConstructedInventory.test.js @@ -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('', () => { + 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( + {}} /> + ); + }); + 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( + {}} />, + { + 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); + }); +}); diff --git a/awx/ui/src/screens/Inventory/ConstructedInventoryAdd/ConstructedInventoryAdd.js b/awx/ui/src/screens/Inventory/ConstructedInventoryAdd/ConstructedInventoryAdd.js new file mode 100644 index 0000000000..1aaa2b7679 --- /dev/null +++ b/awx/ui/src/screens/Inventory/ConstructedInventoryAdd/ConstructedInventoryAdd.js @@ -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 ( + + + +
Coming Soon!
+
+
+
+ ); +} + +export default ConstructedInventoryAdd; diff --git a/awx/ui/src/screens/Inventory/ConstructedInventoryAdd/ConstructedInventoryAdd.test.js b/awx/ui/src/screens/Inventory/ConstructedInventoryAdd/ConstructedInventoryAdd.test.js new file mode 100644 index 0000000000..0a9a6eedd5 --- /dev/null +++ b/awx/ui/src/screens/Inventory/ConstructedInventoryAdd/ConstructedInventoryAdd.test.js @@ -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('', () => { + test('initially renders successfully', async () => { + let wrapper; + await act(async () => { + wrapper = mountWithContexts(); + }); + expect(wrapper.length).toBe(1); + expect(wrapper.find('ConstructedInventoryAdd').length).toBe(1); + }); +}); diff --git a/awx/ui/src/screens/Inventory/ConstructedInventoryAdd/index.js b/awx/ui/src/screens/Inventory/ConstructedInventoryAdd/index.js new file mode 100644 index 0000000000..438115593a --- /dev/null +++ b/awx/ui/src/screens/Inventory/ConstructedInventoryAdd/index.js @@ -0,0 +1 @@ +export { default } from './ConstructedInventoryAdd'; diff --git a/awx/ui/src/screens/Inventory/ConstructedInventoryDetail/ConstructedInventoryDetail.js b/awx/ui/src/screens/Inventory/ConstructedInventoryDetail/ConstructedInventoryDetail.js new file mode 100644 index 0000000000..914e86b0b1 --- /dev/null +++ b/awx/ui/src/screens/Inventory/ConstructedInventoryDetail/ConstructedInventoryDetail.js @@ -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 ; + } + + if (contentError) { + return ; + } + + return ( + + + + + + + + {organization.name} + + } + /> + + + + + + + {instanceGroups && ( + } + isEmpty={instanceGroups.length === 0} + dataCy="constructed-inventory-instance-groups" + /> + )} + {inventory.prevent_instance_group_fallback && ( + + {inventory.prevent_instance_group_fallback && ( + + {t`Prevent Instance Group Fallback`} + + + )} + + } + /> + )} + + {inventory.summary_fields.labels?.results?.map((l) => ( + + {l.name} + + ))} + + } + isEmpty={inventory.summary_fields.labels?.results?.length === 0} + /> + + {sourceInventories?.map((sourceInventory) => ( + + + {sourceInventory.name} + + + ))} + + } + isEmpty={sourceInventories?.length === 0} + /> + + + + + + {userCapabilities.edit && ( + + )} + {userCapabilities.delete && ( + + {t`Delete`} + + )} + + {error && ( + + {t`Failed to delete inventory.`} + + + )} + + ); +} + +ConstructedInventoryDetail.propTypes = { + inventory: Inventory.isRequired, +}; + +export default ConstructedInventoryDetail; diff --git a/awx/ui/src/screens/Inventory/ConstructedInventoryDetail/ConstructedInventoryDetail.test.js b/awx/ui/src/screens/Inventory/ConstructedInventoryDetail/ConstructedInventoryDetail.test.js new file mode 100644 index 0000000000..5d924a2790 --- /dev/null +++ b/awx/ui/src/screens/Inventory/ConstructedInventoryDetail/ConstructedInventoryDetail.test.js @@ -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('', () => { + test('initially renders successfully', async () => { + let wrapper; + await act(async () => { + wrapper = mountWithContexts( + + ); + }); + expect(wrapper.length).toBe(1); + expect(wrapper.find('ConstructedInventoryDetail').length).toBe(1); + }); +}); diff --git a/awx/ui/src/screens/Inventory/ConstructedInventoryDetail/index.js b/awx/ui/src/screens/Inventory/ConstructedInventoryDetail/index.js new file mode 100644 index 0000000000..efe8b49508 --- /dev/null +++ b/awx/ui/src/screens/Inventory/ConstructedInventoryDetail/index.js @@ -0,0 +1 @@ +export { default } from './ConstructedInventoryDetail'; diff --git a/awx/ui/src/screens/Inventory/ConstructedInventoryEdit/ConstructedInventoryEdit.js b/awx/ui/src/screens/Inventory/ConstructedInventoryEdit/ConstructedInventoryEdit.js new file mode 100644 index 0000000000..a49e7eaaed --- /dev/null +++ b/awx/ui/src/screens/Inventory/ConstructedInventoryEdit/ConstructedInventoryEdit.js @@ -0,0 +1,13 @@ +/* eslint i18next/no-literal-string: "off" */ +import React from 'react'; +import { CardBody } from 'components/Card'; + +function ConstructedInventoryEdit() { + return ( + +
Coming Soon!
+
+ ); +} + +export default ConstructedInventoryEdit; diff --git a/awx/ui/src/screens/Inventory/ConstructedInventoryEdit/ConstructedInventoryEdit.test.js b/awx/ui/src/screens/Inventory/ConstructedInventoryEdit/ConstructedInventoryEdit.test.js new file mode 100644 index 0000000000..02b0747880 --- /dev/null +++ b/awx/ui/src/screens/Inventory/ConstructedInventoryEdit/ConstructedInventoryEdit.test.js @@ -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('', () => { + test('initially renders successfully', async () => { + let wrapper; + await act(async () => { + wrapper = mountWithContexts(); + }); + expect(wrapper.length).toBe(1); + expect(wrapper.find('ConstructedInventoryEdit').length).toBe(1); + }); +}); diff --git a/awx/ui/src/screens/Inventory/ConstructedInventoryEdit/index.js b/awx/ui/src/screens/Inventory/ConstructedInventoryEdit/index.js new file mode 100644 index 0000000000..55030e87ab --- /dev/null +++ b/awx/ui/src/screens/Inventory/ConstructedInventoryEdit/index.js @@ -0,0 +1 @@ +export { default } from './ConstructedInventoryEdit'; diff --git a/awx/ui/src/screens/Inventory/ConstructedInventoryGroups/ConstructedInventoryGroups.js b/awx/ui/src/screens/Inventory/ConstructedInventoryGroups/ConstructedInventoryGroups.js new file mode 100644 index 0000000000..964dfa9062 --- /dev/null +++ b/awx/ui/src/screens/Inventory/ConstructedInventoryGroups/ConstructedInventoryGroups.js @@ -0,0 +1,13 @@ +/* eslint i18next/no-literal-string: "off" */ +import React from 'react'; +import { CardBody } from 'components/Card'; + +function ConstructedInventoryGroups() { + return ( + +
Coming Soon!
+
+ ); +} + +export default ConstructedInventoryGroups; diff --git a/awx/ui/src/screens/Inventory/ConstructedInventoryGroups/ConstructedInventoryGroups.test.js b/awx/ui/src/screens/Inventory/ConstructedInventoryGroups/ConstructedInventoryGroups.test.js new file mode 100644 index 0000000000..db2720ff44 --- /dev/null +++ b/awx/ui/src/screens/Inventory/ConstructedInventoryGroups/ConstructedInventoryGroups.test.js @@ -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('', () => { + test('initially renders successfully', async () => { + let wrapper; + await act(async () => { + wrapper = mountWithContexts(); + }); + expect(wrapper.length).toBe(1); + expect(wrapper.find('ConstructedInventoryGroups').length).toBe(1); + }); +}); diff --git a/awx/ui/src/screens/Inventory/ConstructedInventoryGroups/index.js b/awx/ui/src/screens/Inventory/ConstructedInventoryGroups/index.js new file mode 100644 index 0000000000..7f1b4343b2 --- /dev/null +++ b/awx/ui/src/screens/Inventory/ConstructedInventoryGroups/index.js @@ -0,0 +1 @@ +export { default } from './ConstructedInventoryGroups'; diff --git a/awx/ui/src/screens/Inventory/ConstructedInventoryHosts/ConstructedInventoryHosts.js b/awx/ui/src/screens/Inventory/ConstructedInventoryHosts/ConstructedInventoryHosts.js new file mode 100644 index 0000000000..56f0c801b8 --- /dev/null +++ b/awx/ui/src/screens/Inventory/ConstructedInventoryHosts/ConstructedInventoryHosts.js @@ -0,0 +1,13 @@ +/* eslint i18next/no-literal-string: "off" */ +import React from 'react'; +import { CardBody } from 'components/Card'; + +function ConstructedInventoryHosts() { + return ( + +
Coming Soon!
+
+ ); +} + +export default ConstructedInventoryHosts; diff --git a/awx/ui/src/screens/Inventory/ConstructedInventoryHosts/ConstructedInventoryHosts.test.js b/awx/ui/src/screens/Inventory/ConstructedInventoryHosts/ConstructedInventoryHosts.test.js new file mode 100644 index 0000000000..0d6b3d6f13 --- /dev/null +++ b/awx/ui/src/screens/Inventory/ConstructedInventoryHosts/ConstructedInventoryHosts.test.js @@ -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('', () => { + test('initially renders successfully', async () => { + let wrapper; + await act(async () => { + wrapper = mountWithContexts(); + }); + expect(wrapper.length).toBe(1); + expect(wrapper.find('ConstructedInventoryHosts').length).toBe(1); + }); +}); diff --git a/awx/ui/src/screens/Inventory/ConstructedInventoryHosts/index.js b/awx/ui/src/screens/Inventory/ConstructedInventoryHosts/index.js new file mode 100644 index 0000000000..68464720fb --- /dev/null +++ b/awx/ui/src/screens/Inventory/ConstructedInventoryHosts/index.js @@ -0,0 +1 @@ +export { default } from './ConstructedInventoryHosts'; diff --git a/awx/ui/src/screens/Inventory/Inventories.js b/awx/ui/src/screens/Inventory/Inventories.js index 49bf4d7710..dfb04a0229 100644 --- a/awx/ui/src/screens/Inventory/Inventories.js +++ b/awx/ui/src/screens/Inventory/Inventories.js @@ -9,14 +9,18 @@ import PersistentFilters from 'components/PersistentFilters'; import { InventoryList } from './InventoryList'; import Inventory from './Inventory'; import SmartInventory from './SmartInventory'; +import ConstructedInventory from './ConstructedInventory'; import InventoryAdd from './InventoryAdd'; import SmartInventoryAdd from './SmartInventoryAdd'; +import ConstructedInventoryAdd from './ConstructedInventoryAdd'; +import { getInventoryPath } from './shared/utils'; function Inventories() { const initScreenHeader = useRef({ '/inventories': t`Inventories`, '/inventories/inventory/add': t`Create new inventory`, '/inventories/smart_inventory/add': t`Create new smart inventory`, + '/inventories/constructed_inventory/add': t`Create new constructed inventory`, }); const [breadcrumbConfig, setScreenHeader] = useState( @@ -45,10 +49,7 @@ function Inventories() { return; } - const inventoryKind = - inventory.kind === 'smart' ? 'smart_inventory' : 'inventory'; - - const inventoryPath = `/inventories/${inventoryKind}/${inventory.id}`; + const inventoryPath = getInventoryPath(inventory); const inventoryHostsPath = `${inventoryPath}/hosts`; const inventoryGroupsPath = `${inventoryPath}/groups`; const inventorySourcesPath = `${inventoryPath}/sources`; @@ -109,6 +110,9 @@ function Inventories() { + + + {({ me }) => ( @@ -119,6 +123,9 @@ function Inventories() { + + + diff --git a/awx/ui/src/screens/Inventory/Inventory.js b/awx/ui/src/screens/Inventory/Inventory.js index 53da122cd6..c35a92d375 100644 --- a/awx/ui/src/screens/Inventory/Inventory.js +++ b/awx/ui/src/screens/Inventory/Inventory.js @@ -23,6 +23,7 @@ import InventoryEdit from './InventoryEdit'; import InventoryGroups from './InventoryGroups'; import InventoryHosts from './InventoryHosts/InventoryHosts'; import InventorySources from './InventorySources'; +import { getInventoryPath } from './shared/utils'; function Inventory({ setBreadcrumb }) { const [contentError, setContentError] = useState(null); @@ -111,10 +112,8 @@ function Inventory({ setBreadcrumb }) { showCardHeader = false; } - if (inventory?.kind === 'smart') { - return ( - - ); + if (inventory && inventory?.kind !== '') { + return ; } return ( diff --git a/awx/ui/src/screens/Inventory/InventoryList/InventoryList.js b/awx/ui/src/screens/Inventory/InventoryList/InventoryList.js index 0ad6dcc01b..6c63f06310 100644 --- a/awx/ui/src/screens/Inventory/InventoryList/InventoryList.js +++ b/awx/ui/src/screens/Inventory/InventoryList/InventoryList.js @@ -135,6 +135,7 @@ function InventoryList() { const addInventory = t`Add inventory`; const addSmartInventory = t`Add smart inventory`; + const addConstructedInventory = t`Add constructed inventory`; const addButton = ( {addSmartInventory} , + + {addConstructedInventory} + , ]} /> ); @@ -261,11 +271,6 @@ function InventoryList() { inventory={inventory} rowIndex={index} fetchInventories={fetchInventories} - detailUrl={ - inventory.kind === 'smart' - ? `${match.url}/smart_inventory/${inventory.id}/details` - : `${match.url}/inventory/${inventory.id}/details` - } onSelect={() => { if (!inventory.pending_deletion) { handleSelect(inventory); diff --git a/awx/ui/src/screens/Inventory/InventoryList/InventoryListItem.js b/awx/ui/src/screens/Inventory/InventoryList/InventoryListItem.js index c692c32f51..3828401045 100644 --- a/awx/ui/src/screens/Inventory/InventoryList/InventoryListItem.js +++ b/awx/ui/src/screens/Inventory/InventoryList/InventoryListItem.js @@ -1,5 +1,5 @@ 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 { Tr, Td } from '@patternfly/react-table'; @@ -12,6 +12,7 @@ import { Inventory } from 'types'; import { ActionsTd, ActionItem, TdBreakWord } from 'components/PaginatedTable'; import CopyButton from 'components/CopyButton'; import StatusLabel from 'components/StatusLabel'; +import { getInventoryPath } from '../shared/utils'; function InventoryListItem({ inventory, @@ -19,12 +20,10 @@ function InventoryListItem({ isSelected, onSelect, onCopy, - detailUrl, fetchInventories, }) { InventoryListItem.propTypes = { inventory: Inventory.isRequired, - detailUrl: string.isRequired, isSelected: bool.isRequired, onSelect: func.isRequired, }; @@ -50,6 +49,12 @@ function InventoryListItem({ const labelId = `check-action-${inventory.id}`; + const typeLabel = { + '': t`Inventory`, + smart: t`Smart Inventory`, + constructed: t`Constructed Inventory`, + }; + let syncStatus = 'disabled'; if (inventory.isSourceSyncRunning) { syncStatus = 'syncing'; @@ -93,16 +98,20 @@ function InventoryListItem({ {inventory.pending_deletion ? ( {inventory.name} ) : ( - + {inventory.name} )} - {inventory.kind !== 'smart' && + {inventory.kind === '' && (inventory.has_inventory_sources ? ( ))} - - {inventory.kind === 'smart' ? t`Smart Inventory` : t`Inventory`} - + {typeLabel[inventory.kind]} diff --git a/awx/ui/src/screens/Inventory/SmartInventory.js b/awx/ui/src/screens/Inventory/SmartInventory.js index 952cf5dc31..b91d253dc6 100644 --- a/awx/ui/src/screens/Inventory/SmartInventory.js +++ b/awx/ui/src/screens/Inventory/SmartInventory.js @@ -23,6 +23,7 @@ import RelatedTemplateList from 'components/RelatedTemplateList'; import SmartInventoryDetail from './SmartInventoryDetail'; import SmartInventoryEdit from './SmartInventoryEdit'; import SmartInventoryHosts from './SmartInventoryHosts'; +import { getInventoryPath } from './shared/utils'; function SmartInventory({ setBreadcrumb }) { const location = useLocation(); @@ -101,8 +102,8 @@ function SmartInventory({ setBreadcrumb }) { ); } - if (inventory?.kind === '') { - return ; + if (inventory && inventory?.kind !== 'smart') { + return ; } let showCardHeader = true; diff --git a/awx/ui/src/screens/Inventory/shared/utils.js b/awx/ui/src/screens/Inventory/shared/utils.js index c08710327f..5e335beeed 100644 --- a/awx/ui/src/screens/Inventory/shared/utils.js +++ b/awx/ui/src/screens/Inventory/shared/utils.js @@ -8,3 +8,12 @@ const parseHostFilter = (value) => { return value; }; 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]; +} diff --git a/awx/ui/src/screens/Inventory/shared/utils.test.js b/awx/ui/src/screens/Inventory/shared/utils.test.js index 4d659932f7..ccbf44aff1 100644 --- a/awx/ui/src/screens/Inventory/shared/utils.test.js +++ b/awx/ui/src/screens/Inventory/shared/utils.test.js @@ -1,4 +1,4 @@ -import parseHostFilter from './utils'; +import parseHostFilter, { getInventoryPath } from './utils'; describe('parseHostFilter', () => { 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' + ); + }); +});