From 30d78e885772dcc1431d40716ba18f6bf3fe0c0d Mon Sep 17 00:00:00 2001 From: seiwailai Date: Sat, 1 May 2021 01:45:44 +0800 Subject: [PATCH] Project: Added project last job status UI with websocket feature 1. Activate web socket once get into project detail page to ensure job status update synchronization.\n 2. Show last job status if there is no current job.\n 3. Show current job status if there is any current pending, waiting or running job. --- awx/ui_next/src/screens/Project/Project.jsx | 15 ++-- .../Project/ProjectDetail/ProjectDetail.jsx | 23 +++++-- .../Project/ProjectDetail/useWsProject.js | 42 ++++++++++++ .../{ => ProjectDetail}/useWsProject.test.jsx | 28 ++++---- .../src/screens/Project/useWsProject.js | 68 ------------------- 5 files changed, 80 insertions(+), 96 deletions(-) create mode 100644 awx/ui_next/src/screens/Project/ProjectDetail/useWsProject.js rename awx/ui_next/src/screens/Project/{ => ProjectDetail}/useWsProject.test.jsx (85%) delete mode 100644 awx/ui_next/src/screens/Project/useWsProject.js 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; -}