diff --git a/awx/ui_next/src/screens/Inventory/InventorySources/InventorySourceList.jsx b/awx/ui_next/src/screens/Inventory/InventorySources/InventorySourceList.jsx
index a11614e2bf..3334600117 100644
--- a/awx/ui_next/src/screens/Inventory/InventorySources/InventorySourceList.jsx
+++ b/awx/ui_next/src/screens/Inventory/InventorySources/InventorySourceList.jsx
@@ -19,6 +19,7 @@ import DatalistToolbar from '../../../components/DataListToolbar';
import AlertModal from '../../../components/AlertModal/AlertModal';
import ErrorDetail from '../../../components/ErrorDetail/ErrorDetail';
import InventorySourceListItem from './InventorySourceListItem';
+import useWsInventorySources from './useWsInventorySources';
const QS_CONFIG = getQSConfig('inventory', {
not__source: '',
@@ -34,7 +35,7 @@ function InventorySourceList({ i18n }) {
const {
isLoading,
error: fetchError,
- result: { sources, sourceCount, sourceChoices, sourceChoicesOptions },
+ result: { result, sourceCount, sourceChoices, sourceChoicesOptions },
request: fetchSources,
} = useRequest(
useCallback(async () => {
@@ -44,18 +45,21 @@ function InventorySourceList({ i18n }) {
InventorySourcesAPI.readOptions(),
]);
return {
- sources: results[0].data.results,
+ result: results[0].data.results,
sourceCount: results[0].data.count,
sourceChoices: results[1].data.actions.GET.source.choices,
sourceChoicesOptions: results[1].data.actions,
};
}, [id, search]),
{
- sources: [],
+ result: [],
sourceCount: 0,
sourceChoices: [],
}
);
+
+ const sources = useWsInventorySources(result);
+
const canSyncSources =
sources.length > 0 &&
sources.every(source => source.summary_fields.user_capabilities.start);
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 9ed97cfb1f..97b2d4c8e3 100644
--- a/awx/ui_next/src/screens/Inventory/InventorySources/InventorySourceList.test.jsx
+++ b/awx/ui_next/src/screens/Inventory/InventorySources/InventorySourceList.test.jsx
@@ -55,8 +55,11 @@ const sources = {
describe('', () => {
let wrapper;
let history;
+ let debug;
beforeEach(async () => {
+ debug = global.console.debug; // eslint-disable-line prefer-destructuring
+ global.console.debug = () => {};
InventoriesAPI.readSources.mockResolvedValue(sources);
InventorySourcesAPI.readOptions.mockResolvedValue({
data: {
@@ -98,6 +101,7 @@ describe('', () => {
afterEach(() => {
wrapper.unmount();
jest.clearAllMocks();
+ global.console.debug = debug;
});
test('should mount properly', async () => {
diff --git a/awx/ui_next/src/screens/Inventory/InventorySources/useWsInventorySources.js b/awx/ui_next/src/screens/Inventory/InventorySources/useWsInventorySources.js
new file mode 100644
index 0000000000..49a9b4f387
--- /dev/null
+++ b/awx/ui_next/src/screens/Inventory/InventorySources/useWsInventorySources.js
@@ -0,0 +1,48 @@
+import { useState, useEffect } from 'react';
+import useWebsocket from '../../../util/useWebsocket';
+
+export default function useWsJobs(initialSources) {
+ const [sources, setSources] = useState(initialSources);
+ const lastMessage = useWebsocket({
+ jobs: ['status_changed'],
+ control: ['limit_reached_1'],
+ });
+
+ useEffect(() => {
+ setSources(initialSources);
+ }, [initialSources]);
+
+ useEffect(
+ function parseWsMessage() {
+ if (!lastMessage?.unified_job_id || !lastMessage?.inventory_source_id) {
+ return;
+ }
+
+ const sourceId = lastMessage.inventory_source_id;
+ const index = sources.findIndex(s => s.id === sourceId);
+ if (index > -1) {
+ setSources(updateSource(sources, index, lastMessage));
+ }
+ },
+ [lastMessage] // eslint-disable-line react-hooks/exhaustive-deps
+ );
+
+ return sources;
+}
+
+function updateSource(sources, index, message) {
+ const source = {
+ ...sources[index],
+ status: message.status,
+ last_updated: message.finished,
+ summary_fields: {
+ ...sources[index].summary_fields,
+ last_job: {
+ id: message.unified_job_id,
+ status: message.status,
+ finished: message.finished,
+ },
+ },
+ };
+ return [...sources.slice(0, index), source, ...sources.slice(index + 1)];
+}
diff --git a/awx/ui_next/src/screens/Inventory/InventorySources/useWsInventorySources.test.jsx b/awx/ui_next/src/screens/Inventory/InventorySources/useWsInventorySources.test.jsx
new file mode 100644
index 0000000000..b0e5668624
--- /dev/null
+++ b/awx/ui_next/src/screens/Inventory/InventorySources/useWsInventorySources.test.jsx
@@ -0,0 +1,124 @@
+import React from 'react';
+import { act } from 'react-dom/test-utils';
+import WS from 'jest-websocket-mock';
+import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
+import useWsInventorySources from './useWsInventorySources';
+
+/*
+ Jest mock timers don’t play well with jest-websocket-mock,
+ so we'll stub out throttling to resolve immediately
+*/
+jest.mock('../../../util/useThrottle', () => ({
+ __esModule: true,
+ default: jest.fn(val => val),
+}));
+
+function TestInner() {
+ return
;
+}
+function Test({ sources }) {
+ const syncedSources = useWsInventorySources(sources);
+ return ;
+}
+
+describe('useWsInventorySources hook', () => {
+ let debug;
+ let wrapper;
+ beforeEach(() => {
+ debug = global.console.debug; // eslint-disable-line prefer-destructuring
+ global.console.debug = () => {};
+ });
+
+ afterEach(() => {
+ global.console.debug = debug;
+ });
+
+ test('should return sources list', () => {
+ const sources = [{ id: 1 }];
+ wrapper = mountWithContexts();
+
+ expect(wrapper.find('TestInner').prop('sources')).toEqual(sources);
+ WS.clean();
+ });
+
+ test('should establish websocket connection', async () => {
+ global.document.cookie = 'csrftoken=abc123';
+ const mockServer = new WS('wss://localhost/websocket/');
+
+ const sources = [{ id: 1 }];
+ await act(async () => {
+ wrapper = await mountWithContexts();
+ });
+
+ await mockServer.connected;
+ await expect(mockServer).toReceiveMessage(
+ JSON.stringify({
+ xrftoken: 'abc123',
+ groups: {
+ jobs: ['status_changed'],
+ control: ['limit_reached_1'],
+ },
+ })
+ );
+ WS.clean();
+ });
+
+ test('should update last job status', async () => {
+ global.document.cookie = 'csrftoken=abc123';
+ const mockServer = new WS('wss://localhost/websocket/');
+
+ const sources = [
+ {
+ id: 3,
+ status: 'running',
+ summary_fields: {
+ last_job: {
+ id: 5,
+ status: 'running',
+ },
+ },
+ },
+ ];
+ await act(async () => {
+ wrapper = await mountWithContexts();
+ });
+
+ await mockServer.connected;
+ await expect(mockServer).toReceiveMessage(
+ JSON.stringify({
+ xrftoken: 'abc123',
+ groups: {
+ jobs: ['status_changed'],
+ control: ['limit_reached_1'],
+ },
+ })
+ );
+ act(() => {
+ mockServer.send(
+ JSON.stringify({
+ unified_job_id: 5,
+ inventory_source_id: 3,
+ type: 'job',
+ status: 'successful',
+ finished: 'the_time',
+ })
+ );
+ });
+ wrapper.update();
+
+ const source = wrapper.find('TestInner').prop('sources')[0];
+ expect(source).toEqual({
+ id: 3,
+ status: 'successful',
+ last_updated: 'the_time',
+ summary_fields: {
+ last_job: {
+ id: 5,
+ status: 'successful',
+ finished: 'the_time',
+ },
+ },
+ });
+ WS.clean();
+ });
+});