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' ? (
', () => {
{}}
/>
);
});
@@ -40,6 +41,7 @@ describe('', () => {
{}}
/>
);
expect(wrapper.find('MinusCircleIcon').length).toBe(1);
@@ -54,10 +56,6 @@ describe('', () => {
wrapper.find('Button[aria-label="Start sync source"]').simulate('click')
);
expect(InventorySourcesAPI.createSyncStart).toBeCalledWith(1);
- wrapper.update();
- expect(wrapper.find('Button[aria-label="Cancel sync source"]').length).toBe(
- 1
- );
});
test('should cancel sync properly', async () => {
InventorySourcesAPI.readDetail.mockResolvedValue({
@@ -71,6 +69,7 @@ describe('', () => {
{}}
/>
);
expect(wrapper.find('Button[aria-label="Cancel sync source"]').length).toBe(
@@ -83,11 +82,5 @@ describe('', () => {
expect(InventorySourcesAPI.readDetail).toBeCalledWith(1);
expect(InventoryUpdatesAPI.createSyncCancel).toBeCalledWith(120);
-
- wrapper.update();
-
- expect(wrapper.find('Button[aria-label="Start sync source"]').length).toBe(
- 1
- );
});
});