diff --git a/awx/ui/src/api/models/Inventories.js b/awx/ui/src/api/models/Inventories.js index 37654478a7..4fd145e178 100644 --- a/awx/ui/src/api/models/Inventories.js +++ b/awx/ui/src/api/models/Inventories.js @@ -13,7 +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); + this.readInputInventories = this.readInputInventories.bind(this); } readAccessList(id, params) { @@ -73,7 +73,7 @@ class Inventories extends InstanceGroupsMixin(Base) { }); } - readSourceInventories(inventoryId, params) { + readInputInventories(inventoryId, params) { return this.http.get(`${this.baseUrl}${inventoryId}/input_inventories/`, { params, }); diff --git a/awx/ui/src/screens/Inventory/ConstructedInventoryDetail/ConstructedInventoryDetail.js b/awx/ui/src/screens/Inventory/ConstructedInventoryDetail/ConstructedInventoryDetail.js index 914e86b0b1..d8e136646b 100644 --- a/awx/ui/src/screens/Inventory/ConstructedInventoryDetail/ConstructedInventoryDetail.js +++ b/awx/ui/src/screens/Inventory/ConstructedInventoryDetail/ConstructedInventoryDetail.js @@ -9,50 +9,97 @@ import { TextListItem, TextListItemVariants, TextListVariants, + Tooltip, } from '@patternfly/react-core'; +import { InventoriesAPI, ConstructedInventoriesAPI } from 'api'; +import { Inventory } from 'types'; +import { formatDateString } from 'util/dates'; +import { relatedResourceDeleteRequests } from 'util/getRelatedResourceDeleteDetails'; +import useRequest, { useDismissableError } from 'hooks/useRequest'; import AlertModal from 'components/AlertModal'; import { CardBody, CardActionsRow } from 'components/Card'; -import { DetailList, Detail, UserDateDetail } from 'components/DetailList'; +import ChipGroup from 'components/ChipGroup'; 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 { DetailList, Detail, UserDateDetail } from 'components/DetailList'; +import DeleteButton from 'components/DeleteButton'; +import ErrorDetail from 'components/ErrorDetail'; import InstanceGroupLabels from 'components/InstanceGroupLabels'; +import JobCancelButton from 'components/JobCancelButton'; +import Popover from 'components/Popover'; +import StatusLabel from 'components/StatusLabel'; +import ConstructedInventorySyncButton from './ConstructedInventorySyncButton'; +import useWsInventorySourcesDetails from '../shared/useWsInventorySourcesDetails'; import getHelpText from '../shared/Inventory.helptext'; +function JobStatusLabel({ job }) { + if (!job) { + return null; + } + + return ( + +
{t`MOST RECENT SYNC`}
+
+ {t`JOB ID:`} {job.id} +
+
+ {t`STATUS:`} {job.status.toUpperCase()} +
+ {job.finished && ( +
+ {t`FINISHED:`} {formatDateString(job.finished)} +
+ )} + + } + key={job.id} + > + + + +
+ ); +} + function ConstructedInventoryDetail({ inventory }) { const history = useHistory(); const helpText = getHelpText(); const { - result: { instanceGroups, sourceInventories, actions }, + result: { instanceGroups, inputInventories, inventorySource, actions }, request: fetchRelatedDetails, error: contentError, isLoading, } = useRequest( useCallback(async () => { - const [response, sourceInvResponse, options] = await Promise.all([ + const [ + instanceGroupsResponse, + inputInventoriesResponse, + inventorySourceResponse, + optionsResponse, + ] = await Promise.all([ InventoriesAPI.readInstanceGroups(inventory.id), - InventoriesAPI.readSourceInventories(inventory.id), - ConstructedInventoriesAPI.readOptions(inventory.id), + InventoriesAPI.readInputInventories(inventory.id), + InventoriesAPI.readSources(inventory.id), + ConstructedInventoriesAPI.readOptions(), ]); return { - instanceGroups: response.data.results, - sourceInventories: sourceInvResponse.data.results, - actions: options.data.actions.GET, + instanceGroups: instanceGroupsResponse.data.results, + inputInventories: inputInventoriesResponse.data.results, + inventorySource: inventorySourceResponse.data.results[0], + actions: optionsResponse.data.actions.GET, }; }, [inventory.id]), { instanceGroups: [], - sourceInventories: [], + inputInventories: [], + inventorySource: {}, actions: {}, isLoading: true, } @@ -62,6 +109,12 @@ function ConstructedInventoryDetail({ inventory }) { fetchRelatedDetails(); }, [fetchRelatedDetails]); + const wsInventorySource = useWsInventorySourcesDetails(inventorySource); + const inventorySourceSyncJob = + wsInventorySource.summary_fields?.current_job || + wsInventorySource.summary_fields?.last_job || + null; + const { request: deleteInventory, error: deleteError } = useRequest( useCallback(async () => { await InventoriesAPI.destroy(inventory.id); @@ -71,9 +124,6 @@ function ConstructedInventoryDetail({ inventory }) { const { error, dismissError } = useDismissableError(deleteError); - const { organization, user_capabilities: userCapabilities } = - inventory.summary_fields; - const deleteDetailsRequests = relatedResourceDeleteRequests.inventory(inventory); @@ -93,6 +143,14 @@ function ConstructedInventoryDetail({ inventory }) { value={inventory.name} dataCy="constructed-inventory-name" /> + + ) + } + /> - {organization.name} + + {inventory.summary_fields?.organization.name} } /> @@ -204,26 +264,26 @@ function ConstructedInventoryDetail({ inventory }) { /> - {sourceInventories?.map((sourceInventory) => ( + {inputInventories?.map((inputInventory) => ( - - {sourceInventory.name} + + {inputInventory.name} ))} } - isEmpty={sourceInventories?.length === 0} + isEmpty={inputInventories?.length === 0} /> - {userCapabilities.edit && ( + {inventory?.summary_fields?.user_capabilities?.edit && ( + + {startError && ( + + {t`Failed to sync constructed inventory source`} + + + )} + + ); +} + +ConstructedInventorySyncButton.propTypes = { + inventoryId: PropTypes.number.isRequired, +}; + +export default ConstructedInventorySyncButton; diff --git a/awx/ui/src/screens/Inventory/ConstructedInventoryDetail/ConstructedInventorySyncButton.test.js b/awx/ui/src/screens/Inventory/ConstructedInventoryDetail/ConstructedInventorySyncButton.test.js new file mode 100644 index 0000000000..75a5900abb --- /dev/null +++ b/awx/ui/src/screens/Inventory/ConstructedInventoryDetail/ConstructedInventorySyncButton.test.js @@ -0,0 +1,41 @@ +import React from 'react'; +import { InventoriesAPI } from 'api'; +import ConstructedInventorySyncButton from './ConstructedInventorySyncButton'; +import { render, fireEvent, screen, waitFor } from '@testing-library/react'; +import '@testing-library/jest-dom'; + +jest.mock('../../../api'); + +const inventory = { id: 100, name: 'Constructed Inventory' }; + +describe('', () => { + const Component = () => ( + + ); + + test('should render start sync button', () => { + render(); + expect( + screen.getByRole('button', { name: 'Start inventory source sync' }) + ).toBeInTheDocument(); + }); + + test('should make expected api request on sync', async () => { + render(); + const syncButton = screen.queryByText('Sync'); + fireEvent.click(syncButton); + await waitFor(() => + expect(InventoriesAPI.syncAllSources).toHaveBeenCalledWith(100) + ); + }); + + test('should show alert modal on throw', async () => { + InventoriesAPI.syncAllSources.mockRejectedValueOnce(new Error()); + render(); + await waitFor(() => { + const syncButton = screen.queryByText('Sync'); + fireEvent.click(syncButton); + }); + expect(screen.getByRole('dialog', { name: 'Alert modal Error!' })); + }); +}); diff --git a/awx/ui/src/screens/Inventory/InventorySourceDetail/InventorySourceDetail.js b/awx/ui/src/screens/Inventory/InventorySourceDetail/InventorySourceDetail.js index 87e9ece5cd..af657b4fbe 100644 --- a/awx/ui/src/screens/Inventory/InventorySourceDetail/InventorySourceDetail.js +++ b/awx/ui/src/screens/Inventory/InventorySourceDetail/InventorySourceDetail.js @@ -31,7 +31,7 @@ import { formatDateString } from 'util/dates'; import Popover from 'components/Popover'; import { VERBOSITY } from 'components/VerbositySelectField'; import InventorySourceSyncButton from '../shared/InventorySourceSyncButton'; -import useWsInventorySourcesDetails from '../InventorySources/useWsInventorySourcesDetails'; +import useWsInventorySourcesDetails from '../shared/useWsInventorySourcesDetails'; import getHelpText from '../shared/Inventory.helptext'; function InventorySourceDetail({ inventorySource }) { diff --git a/awx/ui/src/screens/Inventory/InventorySources/useWsInventorySourcesDetails.js b/awx/ui/src/screens/Inventory/shared/useWsInventorySourcesDetails.js similarity index 100% rename from awx/ui/src/screens/Inventory/InventorySources/useWsInventorySourcesDetails.js rename to awx/ui/src/screens/Inventory/shared/useWsInventorySourcesDetails.js diff --git a/awx/ui/src/screens/Inventory/InventorySources/useWsInventorySourcesDetails.test.js b/awx/ui/src/screens/Inventory/shared/useWsInventorySourcesDetails.test.js similarity index 100% rename from awx/ui/src/screens/Inventory/InventorySources/useWsInventorySourcesDetails.test.js rename to awx/ui/src/screens/Inventory/shared/useWsInventorySourcesDetails.test.js