From b055d34139a39eca510e5022ad4b7dc9b72d9893 Mon Sep 17 00:00:00 2001 From: Keith Grant Date: Thu, 4 Jun 2020 11:30:01 -0700 Subject: [PATCH] update jobs in list based on websockets --- .../src/components/JobList/JobList.jsx | 69 ++------ .../src/components/JobList/JobListItem.jsx | 2 +- .../src/components/JobList/useWebSocket.js | 151 ------------------ .../src/components/JobList/useWsJobs.js | 138 ++++++++++++++++ 4 files changed, 156 insertions(+), 204 deletions(-) delete mode 100644 awx/ui_next/src/components/JobList/useWebSocket.js create mode 100644 awx/ui_next/src/components/JobList/useWsJobs.js diff --git a/awx/ui_next/src/components/JobList/JobList.jsx b/awx/ui_next/src/components/JobList/JobList.jsx index 68bbabd9fb..ccbb32159b 100644 --- a/awx/ui_next/src/components/JobList/JobList.jsx +++ b/awx/ui_next/src/components/JobList/JobList.jsx @@ -11,7 +11,7 @@ import PaginatedDataList, { ToolbarDeleteButton } from '../PaginatedDataList'; import useRequest, { useDeleteItems } from '../../util/useRequest'; import { getQSConfig, parseQueryString } from '../../util/qs'; import JobListItem from './JobListItem'; -import useWebSocket from './useWebSocket'; +import useWsJobs from './useWsJobs'; import { AdHocCommandsAPI, InventoryUpdatesAPI, @@ -37,59 +37,24 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) { const [selected, setSelected] = useState([]); const location = useLocation(); - const params = parseQueryString(QS_CONFIG, location.search); const { - jobs, - count: itemCount, - contentError, + result: { results, count }, + error: contentError, isLoading, - fetchJobs, - } = useWebSocket(params); - // should this happen automatically inside the hook? - // useEffect(() => { - // fetchJobs(); - // }, [fetchJobs]); - // useCallback(async () => { - // const params = parseQueryString(QS_CONFIG, location.search); - // - // const { - // data: { count, results }, - // } = await UnifiedJobsAPI.read({ ...params }); - // - // return { - // itemCount: count, - // jobs: results, - // }; - // }, [QS_CONFIG, location]) - // ); // eslint-disable-line react-hooks/exhaustive-deps + request: fetchJobs, + } = useRequest( + useCallback(async () => { + const params = parseQueryString(QS_CONFIG, location.search); + const { data } = await UnifiedJobsAPI.read({ ...params }); + return data; + }, [location]), // eslint-disable-line react-hooks/exhaustive-deps + { results: [], count: 0 } + ); + useEffect(() => { + fetchJobs(); + }, [fetchJobs]); - // const { - // result: { jobs, itemCount }, - // error: contentError, - // isLoading, - // request: fetchJobs, - // } = useRequest( - // useCallback(async () => { - // const params = parseQueryString(QS_CONFIG, location.search); - // - // const { - // data: { count, results }, - // } = await UnifiedJobsAPI.read({ ...params }); - // - // return { - // itemCount: count, - // jobs: results, - // }; - // }, [location]), // eslint-disable-line react-hooks/exhaustive-deps - // { - // jobs: [], - // itemCount: 0, - // } - // ); - // - // useEffect(() => { - // fetchJobs(); - // }, [fetchJobs]); + const jobs = useWsJobs(results, fetchJobs); const isAllSelected = selected.length === jobs.length && selected.length > 0; const { @@ -151,7 +116,7 @@ function JobList({ i18n, defaultParams, showTypeColumn = false }) { contentError={contentError} hasContentLoading={isLoading || isDeleteLoading} items={jobs} - itemCount={itemCount} + itemCount={count} pluralizedItemName={i18n._(t`Jobs`)} qsConfig={QS_CONFIG} onRowClick={handleSelect} diff --git a/awx/ui_next/src/components/JobList/JobListItem.jsx b/awx/ui_next/src/components/JobList/JobListItem.jsx index 296ecba9c7..a813641a7f 100644 --- a/awx/ui_next/src/components/JobList/JobListItem.jsx +++ b/awx/ui_next/src/components/JobList/JobListItem.jsx @@ -75,7 +75,7 @@ function JobListItem({ ] : []), - {formatDateString(job.finished)} + {job.finished ? formatDateString(job.finished) : ''} , ]} /> diff --git a/awx/ui_next/src/components/JobList/useWebSocket.js b/awx/ui_next/src/components/JobList/useWebSocket.js deleted file mode 100644 index 79c9430537..0000000000 --- a/awx/ui_next/src/components/JobList/useWebSocket.js +++ /dev/null @@ -1,151 +0,0 @@ -import { useState, useEffect, useCallback, useRef } from 'react'; -import useRequest from '../../util/useRequest'; -import { UnifiedJobsAPI } from '../../api'; - -// rename: useWsJobs ? -export default function useWebSocket(params) { - const [jobs, setJobs] = useState([]); - const { - result: { results, count }, - error: contentError, - isLoading, - request: fetchJobs, - } = useRequest( - useCallback(async () => { - console.log('fetching...'); - const { data } = await UnifiedJobsAPI.read({ ...params }); - return data; - }, [params]), - { results: [], count: 0 } - ); - useEffect(() => { - fetchJobs(); - }, []); // eslint-disable-line react-hooks/exhaustive-deps - useEffect(() => { - setJobs(results); - }, [results]); - const ws = useRef(null); - - 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 => { - const data = JSON.parse(e.data); - // console.log(JSON.parse(e.data)); - const jobId = data.unified_job_id; - if (!jobId) { - return; - } - - // TODO: Pull this function up... jobs is being closed over - const index = jobs.findIndex(j => j.id === jobId); - console.log('index', index); - console.log(jobId, typeof jobId, jobs); - if (index > -1) { - const job = { - ...jobs[index], - status: data.status, - finished: data.finished, - }; - console.log('updated job:', job); - const newJobs = [ - ...jobs.slice(0, index), - job, - ...jobs.slice(index + 1), - ]; - console.log('updating jobs: ', newJobs); - setJobs(newJobs); - } - }; - - 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.close(); - }; - - return () => { - ws.current.close(); - }; - }, []); - - return { - jobs, - count, - contentError, - isLoading, - fetchJobs, - }; -} - -const initial = { - groups_current: [ - 'schedules-changed', - 'control-limit_reached_1', - 'jobs-status_changed', - ], - groups_left: [], - groups_joined: [ - 'schedules-changed', - 'control-limit_reached_1', - 'jobs-status_changed', - ], -}; - -const one = { - unified_job_id: 292, - status: 'pending', - type: 'job', - group_name: 'jobs', - unified_job_template_id: 26, -}; -const two = { - unified_job_id: 292, - status: 'waiting', - instance_group_name: 'tower', - type: 'job', - group_name: 'jobs', - unified_job_template_id: 26, -}; -const three = { - unified_job_id: 293, - status: 'running', - type: 'job', - group_name: 'jobs', - unified_job_template_id: 26, -}; -const four = { - unified_job_id: 293, - status: 'successful', - finished: '2020-06-01T21:49:28.704114Z', - type: 'job', - group_name: 'jobs', - unified_job_template_id: 26, -}; diff --git a/awx/ui_next/src/components/JobList/useWsJobs.js b/awx/ui_next/src/components/JobList/useWsJobs.js new file mode 100644 index 0000000000..839ccd02af --- /dev/null +++ b/awx/ui_next/src/components/JobList/useWsJobs.js @@ -0,0 +1,138 @@ +import { useState, useEffect, useRef } from 'react'; + +export default function useWsJobs(initialJobs, refetchJobs) { + const [jobs, setJobs] = useState(initialJobs); + const [lastMessage, setLastMessage] = useState(null); + useEffect(() => { + setJobs(initialJobs); + }, [initialJobs]); + + const ws = useRef(null); + + useEffect(() => { + if (!lastMessage || !lastMessage.unified_job_id) { + return; + } + + const jobId = lastMessage.unified_job_id; + const index = jobs.findIndex(j => j.id === jobId); + if (index > -1) { + setJobs(updateJob(jobs, index, lastMessage)); + } else { + setJobs(addJobStub(jobs, lastMessage)); + refetchJobs(); + } + }, [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.close(); + }; + + return () => { + ws.current.close(); + }; + }, []); + + return jobs; +} + +function updateJob(jobs, index, message) { + const job = { + ...jobs[index], + status: message.status, + finished: message.finished, + }; + return [...jobs.slice(0, index), job, ...jobs.slice(index + 1)]; +} + +function addJobStub(jobs, message) { + const job = { + id: message.unified_job_id, + status: message.status, + type: message.type, + url: `/api/v2/jobs/${message.unified_job_id}`, + }; + return [job, ...jobs]; +} + +// +// const initial = { +// groups_current: [ +// 'schedules-changed', +// 'control-limit_reached_1', +// 'jobs-status_changed', +// ], +// groups_left: [], +// groups_joined: [ +// 'schedules-changed', +// 'control-limit_reached_1', +// 'jobs-status_changed', +// ], +// }; +// +// const one = { +// unified_job_id: 292, +// status: 'pending', +// type: 'job', +// group_name: 'jobs', +// unified_job_template_id: 26, +// }; +// const two = { +// unified_job_id: 292, +// status: 'waiting', +// instance_group_name: 'tower', +// type: 'job', +// group_name: 'jobs', +// unified_job_template_id: 26, +// }; +// const three = { +// unified_job_id: 293, +// status: 'running', +// type: 'job', +// group_name: 'jobs', +// unified_job_template_id: 26, +// }; +// const four = { +// unified_job_id: 293, +// status: 'successful', +// finished: '2020-06-01T21:49:28.704114Z', +// type: 'job', +// group_name: 'jobs', +// unified_job_template_id: 26, +// };