diff --git a/awx/ui_next/src/api/models/Inventories.js b/awx/ui_next/src/api/models/Inventories.js index 9769eb4707..ab828e32d6 100644 --- a/awx/ui_next/src/api/models/Inventories.js +++ b/awx/ui_next/src/api/models/Inventories.js @@ -88,6 +88,12 @@ class Inventories extends InstanceGroupsMixin(Base) { `How did you get here? Source not found for Inventory ID: ${inventoryId}` ); } + + syncAllSources(inventoryId) { + return this.http.post( + `${this.baseUrl}${inventoryId}/update_inventory_sources/` + ); + } } export default Inventories; diff --git a/awx/ui_next/src/screens/Inventory/InventorySources/InventorySourceList.jsx b/awx/ui_next/src/screens/Inventory/InventorySources/InventorySourceList.jsx index 8d5b11ebf9..9fa43925ab 100644 --- a/awx/ui_next/src/screens/Inventory/InventorySources/InventorySourceList.jsx +++ b/awx/ui_next/src/screens/Inventory/InventorySources/InventorySourceList.jsx @@ -2,6 +2,8 @@ import React, { useCallback, useEffect } from 'react'; import { useParams, useLocation } from 'react-router-dom'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; +import { Button, Tooltip } from '@patternfly/react-core'; + import useRequest, { useDeleteItems } from '../../../util/useRequest'; import { getQSConfig, parseQueryString } from '../../../util/qs'; import { InventoriesAPI, InventorySourcesAPI } from '../../../api'; @@ -38,7 +40,6 @@ function InventorySourceList({ i18n }) { InventoriesAPI.readSources(id, params), InventorySourcesAPI.readOptions(), ]); - return { sources: results[0].data.results, sourceCount: results[0].data.count, @@ -49,8 +50,24 @@ function InventorySourceList({ i18n }) { { sources: [], sourceCount: 0, + sourceChoices: [], } ); + const canSyncSources = + sources.length > 0 && + sources.every(source => source.summary_fields.user_capabilities.start); + const { + isLoading: isSyncAllLoading, + error: syncAllError, + request: syncAll, + } = useRequest( + useCallback(async () => { + if (canSyncSources) { + await InventoriesAPI.syncAllSources(id); + fetchSources(); + } + }, [id, fetchSources, canSyncSources]) + ); useEffect(() => { fetchSources(); @@ -92,8 +109,8 @@ function InventorySourceList({ i18n }) { return ( <> , + ...(canSyncSources + ? [ + + + , + ] + : []), ]} /> )} diff --git a/awx/ui_next/src/screens/Inventory/InventorySources/InventorySourceList.test.jsx b/awx/ui_next/src/screens/Inventory/InventorySources/InventorySourceList.test.jsx index b34ed5952c..2012f4355a 100644 --- a/awx/ui_next/src/screens/Inventory/InventorySources/InventorySourceList.test.jsx +++ b/awx/ui_next/src/screens/Inventory/InventorySources/InventorySourceList.test.jsx @@ -225,6 +225,47 @@ describe('', () => { expect(wrapper.find('ContentError').length).toBe(1); }); + test('should render sync all button and make api call to start sync for all', async () => { + const readSourcesResponse = { + data: { + results: [ + { + id: 1, + name: 'Source Foo', + status: '', + source: 'ec2', + url: '/api/v2/inventory_sources/56/', + summary_fields: { + user_capabilities: { + edit: true, + delete: true, + start: true, + schedule: true, + }, + }, + }, + ], + count: 1, + }, + }; + InventoriesAPI.readSources + .mockResolvedValue({ ...readSourcesResponse, status: 'pending' }) + .mockResolvedValueOnce(readSourcesResponse); + InventorySourcesAPI.readOptions.mockResolvedValue({ + data: { + actions: { + GET: { source: { choices: [['scm', 'SCM'], ['ec2', 'EC2']] } }, + POST: {}, + }, + }, + }); + await waitForElement(wrapper, 'InventorySourceList', el => el.length > 0); + const syncAllButton = wrapper.find('Button[aria-label="Sync All"]'); + expect(syncAllButton.length).toBe(1); + await act(async () => syncAllButton.prop('onClick')()); + expect(InventoriesAPI.syncAllSources).toBeCalled(); + expect(InventoriesAPI.readSources).toBeCalled(); + }); }); describe(' RBAC testing', () => { @@ -296,4 +337,80 @@ describe(' RBAC testing', () => { newWrapper.unmount(); jest.clearAllMocks(); }); + test('should not render Sync All button', async () => { + InventoriesAPI.readSources.mockResolvedValue({ + data: { + results: [ + { + id: 1, + name: 'Source Foo', + status: '', + source: 'ec2', + url: '/api/v2/inventory_sources/56/', + summary_fields: { + user_capabilities: { + edit: true, + delete: true, + start: false, + schedule: true, + }, + }, + }, + { + id: 2, + name: 'Source Bar', + status: '', + source: 'scm', + url: '/api/v2/inventory_sources/57/', + summary_fields: { + user_capabilities: { + edit: true, + delete: true, + start: true, + schedule: true, + }, + }, + }, + ], + count: 1, + }, + }); + InventorySourcesAPI.readOptions.mockResolvedValue({ + data: { + actions: { + GET: { source: { choices: [['scm', 'SCM'], ['ec2', 'EC2']] } }, + }, + }, + }); + let newWrapper; + const history = createMemoryHistory({ + initialEntries: ['/inventories/inventory/2/sources'], + }); + await act(async () => { + newWrapper = mountWithContexts( + + + , + { + context: { + router: { + history, + route: { + location: { search: '' }, + match: { params: { id: 2 } }, + }, + }, + }, + } + ); + }); + await waitForElement( + newWrapper, + 'InventorySourceList', + el => el.length > 0 + ); + expect(newWrapper.find('Button[aria-label="Sync All"]').length).toBe(0); + newWrapper.unmount(); + jest.clearAllMocks(); + }); }); diff --git a/awx/ui_next/src/screens/Inventory/InventorySources/InventorySourceListItem.jsx b/awx/ui_next/src/screens/Inventory/InventorySources/InventorySourceListItem.jsx index 4af6cf0c97..94e442d96f 100644 --- a/awx/ui_next/src/screens/Inventory/InventorySources/InventorySourceListItem.jsx +++ b/awx/ui_next/src/screens/Inventory/InventorySources/InventorySourceListItem.jsx @@ -24,6 +24,7 @@ function InventorySourceListItem({ i18n, detailUrl, label, + onFetchSources, }) { const [isSyncLoading, setIsSyncLoading] = useState(false); @@ -99,6 +100,7 @@ function InventorySourceListItem({ onSyncLoading={isLoading => { setIsSyncLoading(isLoading); }} + onFetchSources={onFetchSources} source={source} /> )} diff --git a/awx/ui_next/src/screens/Inventory/InventorySources/InventorySourceSyncButton.jsx b/awx/ui_next/src/screens/Inventory/InventorySources/InventorySourceSyncButton.jsx index 02d66345ac..b7df832ca7 100644 --- a/awx/ui_next/src/screens/Inventory/InventorySources/InventorySourceSyncButton.jsx +++ b/awx/ui_next/src/screens/Inventory/InventorySources/InventorySourceSyncButton.jsx @@ -1,4 +1,4 @@ -import React, { useCallback, useState, useEffect } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import PropTypes from 'prop-types'; @@ -9,9 +9,12 @@ import AlertModal from '../../../components/AlertModal/AlertModal'; import ErrorDetail from '../../../components/ErrorDetail/ErrorDetail'; import { InventoryUpdatesAPI, InventorySourcesAPI } from '../../../api'; -function InventorySourceSyncButton({ onSyncLoading, source, i18n }) { - const [updateStatus, setUpdateStatus] = useState(source.status); - +function InventorySourceSyncButton({ + onSyncLoading, + source, + i18n, + onFetchSources, +}) { const { isLoading: startSyncLoading, error: startSyncError, @@ -21,11 +24,10 @@ function InventorySourceSyncButton({ onSyncLoading, source, i18n }) { const { data: { status }, } = await InventorySourcesAPI.createSyncStart(source.id); - - setUpdateStatus(status); + onFetchSources(); return status; - }, [source.id]), + }, [source.id, onFetchSources]), {} ); @@ -44,8 +46,8 @@ function InventorySourceSyncButton({ onSyncLoading, source, i18n }) { } = await InventorySourcesAPI.readDetail(source.id); await InventoryUpdatesAPI.createSyncCancel(id); - setUpdateStatus(null); - }, [source.id]) + onFetchSources(); + }, [source.id, onFetchSources]) ); useEffect(() => onSyncLoading(startSyncLoading || cancelSyncLoading), [ @@ -60,7 +62,7 @@ function InventorySourceSyncButton({ onSyncLoading, source, i18n }) { return ( <> - {updateStatus === 'pending' ? ( + {source.status === 'pending' ? (