diff --git a/awx/ui_next/src/screens/Project/Project.jsx b/awx/ui_next/src/screens/Project/Project.jsx index e5cabdd46b..5c3a5a7564 100644 --- a/awx/ui_next/src/screens/Project/Project.jsx +++ b/awx/ui_next/src/screens/Project/Project.jsx @@ -24,7 +24,6 @@ import ProjectDetail from './ProjectDetail'; import ProjectEdit from './ProjectEdit'; import ProjectJobTemplatesList from './ProjectJobTemplatesList'; import { OrganizationsAPI, ProjectsAPI } from '../../api'; -import useWsProject from './useWsProject'; function Project({ i18n, setBreadcrumb }) { const { me = {} } = useConfig(); @@ -33,7 +32,7 @@ function Project({ i18n, setBreadcrumb }) { const { request: fetchProjectAndRoles, - result: { projectDetail, isNotifAdmin }, + result: { project, isNotifAdmin }, isLoading: hasContentLoading, error: contentError, } = useRequest( @@ -59,12 +58,12 @@ function Project({ i18n, setBreadcrumb }) { data.summary_fields.credentials = results; } return { - projectDetail: data, + project: data, isNotifAdmin: notifAdminRes.data.results.length > 0, }; }, [id]), { - projectDetail: null, + project: null, notifAdminRes: null, } ); @@ -74,12 +73,10 @@ function Project({ i18n, setBreadcrumb }) { }, [fetchProjectAndRoles, location.pathname]); useEffect(() => { - if (projectDetail) { - setBreadcrumb(projectDetail); + if (project) { + setBreadcrumb(project); } - }, [projectDetail, setBreadcrumb]); - - const project = useWsProject(projectDetail); + }, [project, setBreadcrumb]); const loadScheduleOptions = useCallback(() => { return ProjectsAPI.readScheduleOptions(project.id); diff --git a/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.jsx b/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.jsx index 6418d253e4..7e7c9a07be 100644 --- a/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.jsx +++ b/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.jsx @@ -24,6 +24,7 @@ import { relatedResourceDeleteRequests } from '../../../util/getRelatedResourceD import ProjectSyncButton from '../shared/ProjectSyncButton'; import StatusLabel from '../../../components/StatusLabel'; import { formatDateString } from '../../../util/dates'; +import useWsProject from './useWsProject'; function ProjectDetail({ project, i18n }) { const { @@ -45,7 +46,7 @@ function ProjectDetail({ project, i18n }) { scm_update_cache_timeout, scm_url, summary_fields, - } = project; + } = useWsProject(project); const history = useHistory(); const { request: deleteProject, isLoading, error: deleteError } = useRequest( @@ -105,20 +106,28 @@ function ProjectDetail({ project, i18n }) { ); }; + let job = null; + + if (summary_fields?.current_job) { + job = summary_fields.current_job; + } else if (summary_fields?.last_job) { + job = summary_fields.last_job; + } + return ( - - + + ) @@ -210,7 +219,7 @@ function ProjectDetail({ project, i18n }) { {summary_fields.user_capabilities?.start && ( )} {summary_fields.user_capabilities?.delete && ( diff --git a/awx/ui_next/src/screens/Project/ProjectDetail/useWsProject.js b/awx/ui_next/src/screens/Project/ProjectDetail/useWsProject.js new file mode 100644 index 0000000000..6c3e582c4b --- /dev/null +++ b/awx/ui_next/src/screens/Project/ProjectDetail/useWsProject.js @@ -0,0 +1,42 @@ +import { useState, useEffect } from 'react'; +import useWebsocket from '../../../util/useWebsocket'; + +export default function useWsProjects(initialProject) { + const [project, setProject] = useState(initialProject); + const lastMessage = useWebsocket({ + jobs: ['status_changed'], + control: ['limit_reached_1'], + }); + + useEffect(() => { + setProject(initialProject); + }, [initialProject]); + + useEffect( + () => { + if ( + !project || + !lastMessage?.unified_job_id || + lastMessage.type !== 'project_update' + ) { + return; + } + + const updatedProject = { + ...project, + summary_fields: { + ...project.summary_fields, + current_job: { + id: lastMessage.unified_job_id, + status: lastMessage.status, + finished: lastMessage.finished, + }, + }, + }; + setProject(updatedProject); + }, + [lastMessage] // eslint-disable-line react-hooks/exhaustive-deps + ); + + return project; +} diff --git a/awx/ui_next/src/screens/Project/useWsProject.test.jsx b/awx/ui_next/src/screens/Project/ProjectDetail/useWsProject.test.jsx similarity index 85% rename from awx/ui_next/src/screens/Project/useWsProject.test.jsx rename to awx/ui_next/src/screens/Project/ProjectDetail/useWsProject.test.jsx index fb94ce1c45..293f6ef986 100644 --- a/awx/ui_next/src/screens/Project/useWsProject.test.jsx +++ b/awx/ui_next/src/screens/Project/ProjectDetail/useWsProject.test.jsx @@ -1,7 +1,7 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import WS from 'jest-websocket-mock'; -import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; +import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; import useWsProject from './useWsProject'; function TestInner() { @@ -65,11 +65,12 @@ describe('useWsProject', () => { summary_fields: { last_job: { id: 1, - status: 'running', - finished: null, + status: 'successful', + finished: '2020-07-02T16:25:31.839071Z', }, }, }; + await act(async () => { wrapper = await mountWithContexts(); }); @@ -84,16 +85,19 @@ describe('useWsProject', () => { }, }) ); + expect( + wrapper.find('TestInner').prop('project').summary_fields.current_job + ).toBeUndefined(); expect( wrapper.find('TestInner').prop('project').summary_fields.last_job.status - ).toEqual('running'); + ).toEqual('successful'); await act(async () => { mockServer.send( JSON.stringify({ group_name: 'jobs', project_id: 1, - status: 'pending', + status: 'running', type: 'project_update', unified_job_id: 2, unified_job_template_id: 1, @@ -103,13 +107,13 @@ describe('useWsProject', () => { wrapper.update(); expect( - wrapper.find('TestInner').prop('project').summary_fields.last_job + wrapper.find('TestInner').prop('project').summary_fields.current_job ).toEqual({ - id: 1, + id: 2, status: 'running', - finished: null, + finished: undefined, }); - // Should not update status to `Pending` if there is a current running or waiting job + await act(async () => { mockServer.send( JSON.stringify({ @@ -117,7 +121,7 @@ describe('useWsProject', () => { project_id: 1, status: 'successful', type: 'project_update', - unified_job_id: 1, + unified_job_id: 2, unified_job_template_id: 1, finished: '2020-07-02T16:28:31.839071Z', }) @@ -126,9 +130,9 @@ describe('useWsProject', () => { wrapper.update(); expect( - wrapper.find('TestInner').prop('project').summary_fields.last_job + wrapper.find('TestInner').prop('project').summary_fields.current_job ).toEqual({ - id: 1, + id: 2, status: 'successful', finished: '2020-07-02T16:28:31.839071Z', }); diff --git a/awx/ui_next/src/screens/Project/useWsProject.js b/awx/ui_next/src/screens/Project/useWsProject.js deleted file mode 100644 index 74c03fd61b..0000000000 --- a/awx/ui_next/src/screens/Project/useWsProject.js +++ /dev/null @@ -1,68 +0,0 @@ -import { useState, useEffect } from 'react'; -import useWebsocket from '../../util/useWebsocket'; - -export default function useWsProjects(initialProject) { - const [project, setProject] = useState(initialProject); - const lastMessage = useWebsocket({ - jobs: ['status_changed'], - control: ['limit_reached_1'], - }); - - useEffect(() => { - setProject(initialProject); - }, [initialProject]); - - useEffect( - () => { - if ( - !project || - !lastMessage?.unified_job_id || - lastMessage.type !== 'project_update' - ) { - return; - } - - const last_status = project.summary_fields.last_job.status; - - // In case if users spam the sync button, we will need to ensure - // the fluent UI on most recent sync tooltip and last job status. - // Thus, we will not update our last job status to `Pending` if - // there is current running job. - // - // For instance, we clicked sync for particular project for twice. - // For first sync, our last job status should immediately change - // to `Pending`, then `Waiting`, then `Running`, then result - // (which are `successful`, `failed`, `error`, `cancelled`. - // For second sync, if the status response is `pending` and we have - // running or waiting jobs, we should not update our UI to `Pending`, - // otherwise our most recent sync tooltip UI will lose our current running - // job and we cannot navigate to the job link through the link provided - // by most recent sync tooltip. - // - // More ideally, we should prevent any spamming on sync button using - // backend logic to reduce overload on server and we can have a - // less complex frontend implementation for fluent UI - if ( - lastMessage.status === 'pending' && - !['successful', 'failed', 'error', 'cancelled'].includes(last_status) - ) { - return; - } - const updatedProject = { - ...project, - summary_fields: { - ...project.summary_fields, - last_job: { - id: lastMessage.unified_job_id, - status: lastMessage.status, - finished: lastMessage.finished, - }, - }, - }; - setProject(updatedProject); - }, - [lastMessage] // eslint-disable-line react-hooks/exhaustive-deps - ); - - return project; -}