From 15ae0976ddb80f9af273de7e7a1684b7ab45d973 Mon Sep 17 00:00:00 2001 From: Keith Grant Date: Tue, 7 Jul 2020 16:36:59 -0700 Subject: [PATCH 1/5] add sync status indicator to inventory list --- .../SyncStatusIndicator.jsx | 46 +++++++++ .../components/SyncStatusIndicator/index.js | 1 + .../Inventory/InventoryList/InventoryList.jsx | 11 ++- .../InventoryList/InventoryListItem.jsx | 15 ++- .../InventoryList/useWsInventories.js | 95 +++++++++++++++++++ 5 files changed, 160 insertions(+), 8 deletions(-) create mode 100644 awx/ui_next/src/components/SyncStatusIndicator/SyncStatusIndicator.jsx create mode 100644 awx/ui_next/src/components/SyncStatusIndicator/index.js create mode 100644 awx/ui_next/src/screens/Inventory/InventoryList/useWsInventories.js diff --git a/awx/ui_next/src/components/SyncStatusIndicator/SyncStatusIndicator.jsx b/awx/ui_next/src/components/SyncStatusIndicator/SyncStatusIndicator.jsx new file mode 100644 index 0000000000..77cb158548 --- /dev/null +++ b/awx/ui_next/src/components/SyncStatusIndicator/SyncStatusIndicator.jsx @@ -0,0 +1,46 @@ +import React from 'react'; +import 'styled-components/macro'; +import styled, { keyframes } from 'styled-components'; +import { oneOf, string } from 'prop-types'; +import { CloudIcon } from '@patternfly/react-icons'; + +const COLORS = { + success: '--pf-global--palette--green-400', + syncing: '--pf-global--palette--green-400', + error: '--pf-global--danger-color--100', + disabled: '--pf-global--disabled-color--200', +}; + +const Pulse = keyframes` + from { + opacity: 0; + } + to { + opacity: 1.0; + } +`; + +const PulseWrapper = styled.div` + animation: ${Pulse} 1.5s linear infinite alternate; +`; + +export default function SyncStatusIndicator({ status, title }) { + const color = COLORS[status] || COLORS.disabled; + + if (status === 'syncing') { + return ( + + + + ); + } + + return ; +} +SyncStatusIndicator.propTypes = { + status: oneOf(['success', 'error', 'disabled', 'syncing']).isRequired, + title: string, +}; +SyncStatusIndicator.defaultProps = { + title: null, +}; diff --git a/awx/ui_next/src/components/SyncStatusIndicator/index.js b/awx/ui_next/src/components/SyncStatusIndicator/index.js new file mode 100644 index 0000000000..8a25d03365 --- /dev/null +++ b/awx/ui_next/src/components/SyncStatusIndicator/index.js @@ -0,0 +1 @@ +export { default } from './SyncStatusIndicator'; diff --git a/awx/ui_next/src/screens/Inventory/InventoryList/InventoryList.jsx b/awx/ui_next/src/screens/Inventory/InventoryList/InventoryList.jsx index dcb817fe59..e0aa888f97 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryList/InventoryList.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryList/InventoryList.jsx @@ -1,7 +1,6 @@ import React, { useState, useCallback, useEffect } from 'react'; import { useLocation, useRouteMatch } from 'react-router-dom'; import { withI18n } from '@lingui/react'; - import { t } from '@lingui/macro'; import { Card, PageSection } from '@patternfly/react-core'; @@ -13,8 +12,8 @@ import ErrorDetail from '../../../components/ErrorDetail'; import PaginatedDataList, { ToolbarDeleteButton, } from '../../../components/PaginatedDataList'; - import { getQSConfig, parseQueryString } from '../../../util/qs'; +import useWsInventories from './useWsInventories'; import AddDropDownButton from '../../../components/AddDropDownButton'; import InventoryListItem from './InventoryListItem'; @@ -30,7 +29,7 @@ function InventoryList({ i18n }) { const [selected, setSelected] = useState([]); const { - result: { inventories, itemCount, actions }, + result: { results, itemCount, actions }, error: contentError, isLoading, request: fetchInventories, @@ -42,13 +41,13 @@ function InventoryList({ i18n }) { InventoriesAPI.readOptions(), ]); return { - inventories: response.data.results, + results: response.data.results, itemCount: response.data.count, actions: actionsResponse.data.actions, }; }, [location]), { - inventories: [], + results: [], itemCount: 0, actions: {}, } @@ -58,6 +57,8 @@ function InventoryList({ i18n }) { fetchInventories(); }, [fetchInventories]); + const inventories = useWsInventories(results); + const isAllSelected = selected.length === inventories.length && selected.length > 0; const { diff --git a/awx/ui_next/src/screens/Inventory/InventoryList/InventoryListItem.jsx b/awx/ui_next/src/screens/Inventory/InventoryList/InventoryListItem.jsx index 9d272ace69..eb091495c7 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryList/InventoryListItem.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryList/InventoryListItem.jsx @@ -10,16 +10,16 @@ import { DataListItemRow, Tooltip, } from '@patternfly/react-core'; - +import { PencilAltIcon } from '@patternfly/react-icons'; import { t } from '@lingui/macro'; import { Link } from 'react-router-dom'; import styled from 'styled-components'; -import { PencilAltIcon } from '@patternfly/react-icons'; import { timeOfDay } from '../../../util/dates'; import { InventoriesAPI } from '../../../api'; import { Inventory } from '../../../types'; import DataListCell from '../../../components/DataListCell'; import CopyButton from '../../../components/CopyButton'; +import SyncStatusIndicator from '../../../components/SyncStatusIndicator'; const DataListAction = styled(_DataListAction)` align-items: center; @@ -52,6 +52,12 @@ function InventoryListItem({ }, [inventory.id, inventory.name, fetchInventories]); const labelId = `check-action-${inventory.id}`; + + let syncStatus = 'disabled'; + if (inventory.has_inventory_sources) { + syncStatus = + inventory.inventory_sources_with_failures > 0 ? 'error' : 'success'; + } return ( + + + , + {inventory.name} diff --git a/awx/ui_next/src/screens/Inventory/InventoryList/useWsInventories.js b/awx/ui_next/src/screens/Inventory/InventoryList/useWsInventories.js new file mode 100644 index 0000000000..29d438d3bf --- /dev/null +++ b/awx/ui_next/src/screens/Inventory/InventoryList/useWsInventories.js @@ -0,0 +1,95 @@ +import { useState, useEffect, useRef } from 'react'; + +export default function useWsProjects(initialInventories) { + const [inventories, setInventories] = useState(initialInventories); + const [lastMessage, setLastMessage] = useState(null); + const ws = useRef(null); + + useEffect(() => { + setInventories(initialInventories); + }, [initialInventories]); + + // const messageExample = { + // unified_job_id: 533, + // status: 'pending', + // type: 'inventory_update', + // inventory_source_id: 53, + // inventory_id: 5, + // group_name: 'jobs', + // unified_job_template_id: 53, + // }; + useEffect(() => { + if (!lastMessage?.unified_job_id || lastMessage.type !== 'project_update') { + return; + } + const index = inventories.findIndex(p => p.id === lastMessage.project_id); + if (index === -1) { + return; + } + + const inventory = inventories[index]; + const updatedProject = { + ...inventory, + summary_fields: { + ...inventory.summary_fields, + // last_job: { + // id: lastMessage.unified_job_id, + // status: lastMessage.status, + // finished: lastMessage.finished, + // }, + }, + }; + setInventories([ + ...inventories.slice(0, index), + updatedProject, + ...inventories.slice(index + 1), + ]); + }, [lastMessage]); // eslint-disable-line react-hooks/exhaustive-deps + + useEffect(() => { + ws.current = new WebSocket(`wss://${window.location.host}/websocket/`); + + const connect = () => { + const xrftoken = `; ${document.cookie}` + .split('; csrftoken=') + .pop() + .split(';') + .shift(); + ws.current.send( + JSON.stringify({ + xrftoken, + groups: { + inventories: ['status_changed'], + jobs: ['status_changed'], + control: ['limit_reached_1'], + }, + }) + ); + }; + ws.current.onopen = connect; + + ws.current.onmessage = e => { + setLastMessage(JSON.parse(e.data)); + }; + + ws.current.onclose = e => { + // eslint-disable-next-line no-console + console.debug('Socket closed. Reconnecting...', e); + setTimeout(() => { + connect(); + }, 1000); + }; + + ws.current.onerror = err => { + // eslint-disable-next-line no-console + console.debug('Socket error: ', err, 'Disconnecting...'); + ws.current.close(); + }; + + return () => { + ws.current.close(); + }; + }, []); + + return inventories; +} From 84bea3d34834e478471c7f55bb6402718c439fa9 Mon Sep 17 00:00:00 2001 From: Keith Grant Date: Wed, 8 Jul 2020 15:20:42 -0700 Subject: [PATCH 2/5] websockets: show live inventory source sync status --- .../Inventory/InventoryList/InventoryList.jsx | 11 +- .../InventoryList/InventoryListItem.jsx | 4 +- .../InventoryList/useWsInventories.js | 108 ++++++++++++------ 3 files changed, 86 insertions(+), 37 deletions(-) diff --git a/awx/ui_next/src/screens/Inventory/InventoryList/InventoryList.jsx b/awx/ui_next/src/screens/Inventory/InventoryList/InventoryList.jsx index e0aa888f97..65d7bed75a 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryList/InventoryList.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryList/InventoryList.jsx @@ -57,7 +57,16 @@ function InventoryList({ i18n }) { fetchInventories(); }, [fetchInventories]); - const inventories = useWsInventories(results); + const fetchInventoriesById = useCallback( + async ids => { + const params = parseQueryString(QS_CONFIG, location.search); + params.id__in = ids.join(','); + const { data } = await InventoriesAPI.read(params); + return data.results; + }, + [location.search] // eslint-disable-line react-hooks/exhaustive-deps + ); + const inventories = useWsInventories(results, fetchInventoriesById); const isAllSelected = selected.length === inventories.length && selected.length > 0; diff --git a/awx/ui_next/src/screens/Inventory/InventoryList/InventoryListItem.jsx b/awx/ui_next/src/screens/Inventory/InventoryList/InventoryListItem.jsx index eb091495c7..9070656bba 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryList/InventoryListItem.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryList/InventoryListItem.jsx @@ -54,7 +54,9 @@ function InventoryListItem({ const labelId = `check-action-${inventory.id}`; let syncStatus = 'disabled'; - if (inventory.has_inventory_sources) { + if (inventory.isSourceSyncRunning) { + syncStatus = 'syncing'; + } else if (inventory.has_inventory_sources) { syncStatus = inventory.inventory_sources_with_failures > 0 ? 'error' : 'success'; } diff --git a/awx/ui_next/src/screens/Inventory/InventoryList/useWsInventories.js b/awx/ui_next/src/screens/Inventory/InventoryList/useWsInventories.js index 29d438d3bf..d9886c29c4 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryList/useWsInventories.js +++ b/awx/ui_next/src/screens/Inventory/InventoryList/useWsInventories.js @@ -1,50 +1,88 @@ import { useState, useEffect, useRef } from 'react'; +import useThrottle from '../../../util/useThrottle'; -export default function useWsProjects(initialInventories) { +export default function useWsProjects( + initialInventories, + fetchInventoriesById +) { const [inventories, setInventories] = useState(initialInventories); const [lastMessage, setLastMessage] = useState(null); + const [inventoriesToFetch, setInventoriesToFetch] = useState([]); + const throttledInventoriesToFetch = useThrottle(inventoriesToFetch, 5000); const ws = useRef(null); useEffect(() => { setInventories(initialInventories); }, [initialInventories]); - // const messageExample = { - // unified_job_id: 533, - // status: 'pending', - // type: 'inventory_update', - // inventory_source_id: 53, - // inventory_id: 5, - // group_name: 'jobs', - // unified_job_template_id: 53, - // }; - useEffect(() => { - if (!lastMessage?.unified_job_id || lastMessage.type !== 'project_update') { - return; - } - const index = inventories.findIndex(p => p.id === lastMessage.project_id); - if (index === -1) { - return; + const enqueueId = id => { + if (!inventoriesToFetch.includes(id)) { + setInventoriesToFetch(ids => ids.concat(id)); } + }; + useEffect( + function fetchUpdatedInventories() { + (async () => { + if (!throttledInventoriesToFetch.length) { + return; + } + setInventoriesToFetch([]); + const newInventories = await fetchInventoriesById( + throttledInventoriesToFetch + ); + let updated = inventories; + newInventories.forEach(inventory => { + const index = inventories.findIndex(i => i.id === inventory.id); + if (index === -1) { + return; + } + updated = [ + ...updated.slice(0, index), + inventory, + ...updated.slice(index + 1), + ]; + }); + setInventories(updated); + })(); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [throttledInventoriesToFetch, fetchInventoriesById] + ); - const inventory = inventories[index]; - const updatedProject = { - ...inventory, - summary_fields: { - ...inventory.summary_fields, - // last_job: { - // id: lastMessage.unified_job_id, - // status: lastMessage.status, - // finished: lastMessage.finished, - // }, - }, - }; - setInventories([ - ...inventories.slice(0, index), - updatedProject, - ...inventories.slice(index + 1), - ]); - }, [lastMessage]); // eslint-disable-line react-hooks/exhaustive-deps + useEffect( + function processWsMessage() { + if ( + !lastMessage?.inventory_id || + lastMessage.type !== 'inventory_update' + ) { + return; + } + const index = inventories.findIndex( + p => p.id === lastMessage.inventory_id + ); + if (index === -1) { + return; + } + + if (!['pending', 'waiting', 'running'].includes(lastMessage.status)) { + enqueueId(lastMessage.inventory_id); + return; + } + + const inventory = inventories[index]; + const updatedInventory = { + ...inventory, + isSourceSyncRunning: true, + }; + setInventories([ + ...inventories.slice(0, index), + updatedInventory, + ...inventories.slice(index + 1), + ]); + }, + // eslint-disable-next-line react-hooks/exhaustive-deps, + [lastMessage] + ); useEffect(() => { ws.current = new WebSocket(`wss://${window.location.host}/websocket/`); From 455e01809971e8e0fd3f095846c9c5ce2d3ccdc8 Mon Sep 17 00:00:00 2001 From: Keith Grant Date: Thu, 9 Jul 2020 10:14:03 -0700 Subject: [PATCH 3/5] refactor shared WS logic into useWebsocket hook --- .../src/components/JobList/useWsJobs.js | 56 +++---------------- .../InventoryList/useWsInventories.js | 55 +++--------------- .../Project/ProjectList/useWsProjects.js | 53 ++---------------- awx/ui_next/src/util/useWebsocket.js | 49 ++++++++++++++++ 4 files changed, 69 insertions(+), 144 deletions(-) create mode 100644 awx/ui_next/src/util/useWebsocket.js diff --git a/awx/ui_next/src/components/JobList/useWsJobs.js b/awx/ui_next/src/components/JobList/useWsJobs.js index 9c939f25d9..4560930818 100644 --- a/awx/ui_next/src/components/JobList/useWsJobs.js +++ b/awx/ui_next/src/components/JobList/useWsJobs.js @@ -1,5 +1,6 @@ -import { useState, useEffect, useRef } from 'react'; +import { useState, useEffect } from 'react'; import { useLocation } from 'react-router-dom'; +import useWebsocket from '../../util/useWebsocket'; import useThrottle from '../../util/useThrottle'; import { parseQueryString } from '../../util/qs'; import sortJobs from './sortJobs'; @@ -7,9 +8,13 @@ import sortJobs from './sortJobs'; export default function useWsJobs(initialJobs, fetchJobsById, qsConfig) { const location = useLocation(); const [jobs, setJobs] = useState(initialJobs); - const [lastMessage, setLastMessage] = useState(null); const [jobsToFetch, setJobsToFetch] = useState([]); const throttledJobsToFetch = useThrottle(jobsToFetch, 5000); + const lastMessage = useWebsocket({ + jobs: ['status_changed'], + schedules: ['changed'], + control: ['limit_reached_1'], + }); useEffect(() => { setJobs(initialJobs); @@ -37,8 +42,6 @@ export default function useWsJobs(initialJobs, fetchJobsById, qsConfig) { })(); }, [throttledJobsToFetch, fetchJobsById]); // eslint-disable-line react-hooks/exhaustive-deps - const ws = useRef(null); - useEffect(() => { if (!lastMessage || !lastMessage.unified_job_id) { return; @@ -61,51 +64,6 @@ export default function useWsJobs(initialJobs, fetchJobsById, qsConfig) { } }, [lastMessage]); // eslint-disable-line react-hooks/exhaustive-deps - useEffect(() => { - ws.current = new WebSocket(`wss://${window.location.host}/websocket/`); - - const connect = () => { - const xrftoken = `; ${document.cookie}` - .split('; csrftoken=') - .pop() - .split(';') - .shift(); - ws.current.send( - JSON.stringify({ - xrftoken, - groups: { - jobs: ['status_changed'], - schedules: ['changed'], - control: ['limit_reached_1'], - }, - }) - ); - }; - ws.current.onopen = connect; - - ws.current.onmessage = e => { - setLastMessage(JSON.parse(e.data)); - }; - - ws.current.onclose = e => { - // eslint-disable-next-line no-console - console.debug('Socket closed. Reconnecting...', e); - setTimeout(() => { - connect(); - }, 1000); - }; - - ws.current.onerror = err => { - // eslint-disable-next-line no-console - console.debug('Socket error: ', err, 'Disconnecting...'); - ws.current.close(); - }; - - return () => { - ws.current.close(); - }; - }, []); - return jobs; } diff --git a/awx/ui_next/src/screens/Inventory/InventoryList/useWsInventories.js b/awx/ui_next/src/screens/Inventory/InventoryList/useWsInventories.js index d9886c29c4..13871490f3 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryList/useWsInventories.js +++ b/awx/ui_next/src/screens/Inventory/InventoryList/useWsInventories.js @@ -1,4 +1,5 @@ -import { useState, useEffect, useRef } from 'react'; +import { useState, useEffect } from 'react'; +import useWebsocket from '../../../util/useWebsocket'; import useThrottle from '../../../util/useThrottle'; export default function useWsProjects( @@ -6,10 +7,13 @@ export default function useWsProjects( fetchInventoriesById ) { const [inventories, setInventories] = useState(initialInventories); - const [lastMessage, setLastMessage] = useState(null); const [inventoriesToFetch, setInventoriesToFetch] = useState([]); const throttledInventoriesToFetch = useThrottle(inventoriesToFetch, 5000); - const ws = useRef(null); + const lastMessage = useWebsocket({ + inventories: ['status_changed'], + jobs: ['status_changed'], + control: ['limit_reached_1'], + }); useEffect(() => { setInventories(initialInventories); @@ -84,50 +88,5 @@ export default function useWsProjects( [lastMessage] ); - useEffect(() => { - ws.current = new WebSocket(`wss://${window.location.host}/websocket/`); - - const connect = () => { - const xrftoken = `; ${document.cookie}` - .split('; csrftoken=') - .pop() - .split(';') - .shift(); - ws.current.send( - JSON.stringify({ - xrftoken, - groups: { - inventories: ['status_changed'], - jobs: ['status_changed'], - control: ['limit_reached_1'], - }, - }) - ); - }; - ws.current.onopen = connect; - - ws.current.onmessage = e => { - setLastMessage(JSON.parse(e.data)); - }; - - ws.current.onclose = e => { - // eslint-disable-next-line no-console - console.debug('Socket closed. Reconnecting...', e); - setTimeout(() => { - connect(); - }, 1000); - }; - - ws.current.onerror = err => { - // eslint-disable-next-line no-console - console.debug('Socket error: ', err, 'Disconnecting...'); - ws.current.close(); - }; - - return () => { - ws.current.close(); - }; - }, []); - return inventories; } diff --git a/awx/ui_next/src/screens/Project/ProjectList/useWsProjects.js b/awx/ui_next/src/screens/Project/ProjectList/useWsProjects.js index a5c46319ba..38303c9ed3 100644 --- a/awx/ui_next/src/screens/Project/ProjectList/useWsProjects.js +++ b/awx/ui_next/src/screens/Project/ProjectList/useWsProjects.js @@ -1,9 +1,12 @@ -import { useState, useEffect, useRef } from 'react'; +import { useState, useEffect } from 'react'; +import useWebsocket from '../../../util/useWebsocket'; export default function useWsProjects(initialProjects) { const [projects, setProjects] = useState(initialProjects); - const [lastMessage, setLastMessage] = useState(null); - const ws = useRef(null); + const lastMessage = useWebsocket({ + jobs: ['status_changed'], + control: ['limit_reached_1'], + }); useEffect(() => { setProjects(initialProjects); @@ -37,49 +40,5 @@ export default function useWsProjects(initialProjects) { ]); }, [lastMessage]); // eslint-disable-line react-hooks/exhaustive-deps - useEffect(() => { - ws.current = new WebSocket(`wss://${window.location.host}/websocket/`); - - const connect = () => { - const xrftoken = `; ${document.cookie}` - .split('; csrftoken=') - .pop() - .split(';') - .shift(); - ws.current.send( - JSON.stringify({ - xrftoken, - groups: { - jobs: ['status_changed'], - control: ['limit_reached_1'], - }, - }) - ); - }; - ws.current.onopen = connect; - - ws.current.onmessage = e => { - setLastMessage(JSON.parse(e.data)); - }; - - ws.current.onclose = e => { - // eslint-disable-next-line no-console - console.debug('Socket closed. Reconnecting...', e); - setTimeout(() => { - connect(); - }, 1000); - }; - - ws.current.onerror = err => { - // eslint-disable-next-line no-console - console.debug('Socket error: ', err, 'Disconnecting...'); - ws.current.close(); - }; - - return () => { - ws.current.close(); - }; - }, []); - return projects; } diff --git a/awx/ui_next/src/util/useWebsocket.js b/awx/ui_next/src/util/useWebsocket.js new file mode 100644 index 0000000000..c04d086f35 --- /dev/null +++ b/awx/ui_next/src/util/useWebsocket.js @@ -0,0 +1,49 @@ +import { useState, useEffect, useRef } from 'react'; + +export default function useWebsocket(subscribeGroups) { + const [lastMessage, setLastMessage] = useState(null); + const ws = useRef(null); + + useEffect(function setupSocket() { + ws.current = new WebSocket(`wss://${window.location.host}/websocket/`); + + const connect = () => { + const xrftoken = `; ${document.cookie}` + .split('; csrftoken=') + .pop() + .split(';') + .shift(); + ws.current.send( + JSON.stringify({ + xrftoken, + groups: subscribeGroups, + }) + ); + }; + ws.current.onopen = connect; + + ws.current.onmessage = e => { + setLastMessage(JSON.parse(e.data)); + }; + + ws.current.onclose = e => { + // eslint-disable-next-line no-console + console.debug('Socket closed. Reconnecting...', e); + setTimeout(() => { + connect(); + }, 1000); + }; + + ws.current.onerror = err => { + // eslint-disable-next-line no-console + console.debug('Socket error: ', err, 'Disconnecting...'); + ws.current.close(); + }; + + return () => { + ws.current.close(); + }; + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + return lastMessage; +} From f225df1acd2c4a9ed98cd8ca996565b4504b13e7 Mon Sep 17 00:00:00 2001 From: Keith Grant Date: Thu, 9 Jul 2020 14:12:56 -0700 Subject: [PATCH 4/5] add useWsInventories tests --- .../InventoryList/InventoryList.test.jsx | 4 + .../InventoryList/useWsInventories.test.jsx | 129 ++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 awx/ui_next/src/screens/Inventory/InventoryList/useWsInventories.test.jsx diff --git a/awx/ui_next/src/screens/Inventory/InventoryList/InventoryList.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryList/InventoryList.test.jsx index 0db85887bb..e9b256e05e 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryList/InventoryList.test.jsx +++ b/awx/ui_next/src/screens/Inventory/InventoryList/InventoryList.test.jsx @@ -119,6 +119,7 @@ const mockInventories = [ ]; describe('', () => { + let debug; beforeEach(() => { InventoriesAPI.read.mockResolvedValue({ data: { @@ -135,10 +136,13 @@ describe('', () => { }, }, }); + debug = global.console.debug; // eslint-disable-line prefer-destructuring + global.console.debug = () => {}; }); afterEach(() => { jest.clearAllMocks(); + global.console.debug = debug; }); test('should load and render inventories', async () => { diff --git a/awx/ui_next/src/screens/Inventory/InventoryList/useWsInventories.test.jsx b/awx/ui_next/src/screens/Inventory/InventoryList/useWsInventories.test.jsx new file mode 100644 index 0000000000..196166add6 --- /dev/null +++ b/awx/ui_next/src/screens/Inventory/InventoryList/useWsInventories.test.jsx @@ -0,0 +1,129 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import WS from 'jest-websocket-mock'; +import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; +import useWsInventories from './useWsInventories'; + +/* + 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({ inventories, fetch }) { + const syncedJobs = useWsInventories(inventories, fetch); + return ; +} + +describe('useWsInventories 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 inventories list', () => { + const inventories = [{ id: 1 }]; + wrapper = mountWithContexts(); + + expect(wrapper.find('TestInner').prop('inventories')).toEqual(inventories); + WS.clean(); + }); + + test('should establish websocket connection', async () => { + global.document.cookie = 'csrftoken=abc123'; + const mockServer = new WS('wss://localhost/websocket/'); + + const inventories = [{ id: 1 }]; + await act(async () => { + wrapper = await mountWithContexts(); + }); + + await mockServer.connected; + await expect(mockServer).toReceiveMessage( + JSON.stringify({ + xrftoken: 'abc123', + groups: { + inventories: ['status_changed'], + jobs: ['status_changed'], + control: ['limit_reached_1'], + }, + }) + ); + WS.clean(); + }); + + test('should update inventory sync status', async () => { + global.document.cookie = 'csrftoken=abc123'; + const mockServer = new WS('wss://localhost/websocket/'); + + const inventories = [{ id: 1 }]; + await act(async () => { + wrapper = await mountWithContexts(); + }); + + await mockServer.connected; + await expect(mockServer).toReceiveMessage( + JSON.stringify({ + xrftoken: 'abc123', + groups: { + inventories: ['status_changed'], + jobs: ['status_changed'], + control: ['limit_reached_1'], + }, + }) + ); + act(() => { + mockServer.send( + JSON.stringify({ + inventory_id: 1, + type: 'inventory_update', + status: 'running', + }) + ); + }); + wrapper.update(); + + expect( + wrapper.find('TestInner').prop('inventories')[0].isSourceSyncRunning + ).toEqual(true); + WS.clean(); + }); + + test('should fetch fresh inventory after sync runs', async () => { + global.document.cookie = 'csrftoken=abc123'; + const mockServer = new WS('wss://localhost/websocket/'); + const inventories = [{ id: 1 }]; + const fetch = jest.fn(() => []); + await act(async () => { + wrapper = await mountWithContexts( + + ); + }); + + await mockServer.connected; + await act(async () => { + mockServer.send( + JSON.stringify({ + inventory_id: 1, + type: 'inventory_update', + status: 'successful', + }) + ); + }); + + expect(fetch).toHaveBeenCalledWith([1]); + WS.clean(); + }); +}); From 38e09686907288fb8276c22de00126987cdcf189 Mon Sep 17 00:00:00 2001 From: Keith Grant Date: Fri, 10 Jul 2020 08:33:35 -0700 Subject: [PATCH 5/5] useWsInventories cleanup --- .../screens/Inventory/InventoryList/useWsInventories.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/awx/ui_next/src/screens/Inventory/InventoryList/useWsInventories.js b/awx/ui_next/src/screens/Inventory/InventoryList/useWsInventories.js index 13871490f3..eea15f9a2c 100644 --- a/awx/ui_next/src/screens/Inventory/InventoryList/useWsInventories.js +++ b/awx/ui_next/src/screens/Inventory/InventoryList/useWsInventories.js @@ -34,17 +34,13 @@ export default function useWsProjects( const newInventories = await fetchInventoriesById( throttledInventoriesToFetch ); - let updated = inventories; + const updated = [...inventories]; newInventories.forEach(inventory => { const index = inventories.findIndex(i => i.id === inventory.id); if (index === -1) { return; } - updated = [ - ...updated.slice(0, index), - inventory, - ...updated.slice(index + 1), - ]; + updated[index] = inventory; }); setInventories(updated); })();