From 03d8987d939a6b67c73201a998f2d9f895bd4ee6 Mon Sep 17 00:00:00 2001 From: seiwailai Date: Fri, 16 Apr 2021 23:28:09 +0800 Subject: [PATCH 1/7] project: Add last job status as for feedback feature. --- awx/ui_next/src/screens/Project/Project.jsx | 15 ++-- .../Project/ProjectDetail/ProjectDetail.jsx | 41 ++++++++++- .../src/screens/Project/useWsProject.js | 68 +++++++++++++++++++ 3 files changed, 116 insertions(+), 8 deletions(-) create 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 5c3a5a7564..e5cabdd46b 100644 --- a/awx/ui_next/src/screens/Project/Project.jsx +++ b/awx/ui_next/src/screens/Project/Project.jsx @@ -24,6 +24,7 @@ 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(); @@ -32,7 +33,7 @@ function Project({ i18n, setBreadcrumb }) { const { request: fetchProjectAndRoles, - result: { project, isNotifAdmin }, + result: { projectDetail, isNotifAdmin }, isLoading: hasContentLoading, error: contentError, } = useRequest( @@ -58,12 +59,12 @@ function Project({ i18n, setBreadcrumb }) { data.summary_fields.credentials = results; } return { - project: data, + projectDetail: data, isNotifAdmin: notifAdminRes.data.results.length > 0, }; }, [id]), { - project: null, + projectDetail: null, notifAdminRes: null, } ); @@ -73,10 +74,12 @@ function Project({ i18n, setBreadcrumb }) { }, [fetchProjectAndRoles, location.pathname]); useEffect(() => { - if (project) { - setBreadcrumb(project); + if (projectDetail) { + setBreadcrumb(projectDetail); } - }, [project, setBreadcrumb]); + }, [projectDetail, setBreadcrumb]); + + const project = useWsProject(projectDetail); 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 0d33d9dfef..0e82027ba7 100644 --- a/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.jsx +++ b/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.jsx @@ -1,8 +1,8 @@ -import React, { useCallback } from 'react'; +import React, { Fragment, useCallback } from 'react'; import { Link, useHistory } from 'react-router-dom'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; -import { Button, List, ListItem } from '@patternfly/react-core'; +import { Button, List, ListItem, Tooltip } from '@patternfly/react-core'; import { Project } from '../../../types'; import { Config } from '../../../contexts/Config'; @@ -22,6 +22,8 @@ import { toTitleCase } from '../../../util/strings'; import useRequest, { useDismissableError } from '../../../util/useRequest'; import { relatedResourceDeleteRequests } from '../../../util/getRelatedResourceDeleteDetails'; import ProjectSyncButton from '../shared/ProjectSyncButton'; +import StatusLabel from '../../../components/StatusLabel'; +import { formatDateString } from '../../../util/dates'; function ProjectDetail({ project, i18n }) { const { @@ -84,9 +86,44 @@ function ProjectDetail({ project, i18n }) { ); } + const generateLastJobTooltip = job => { + return ( + +
{i18n._(t`MOST RECENT SYNC`)}
+
+ {i18n._(t`JOB ID:`)} {job.id} +
+
+ {i18n._(t`STATUS:`)} {job.status.toUpperCase()} +
+ {job.finished && ( +
+ {i18n._(t`FINISHED:`)} {formatDateString(job.finished)} +
+ )} +
+ ); + }; + return ( + + + + + + ) + } + /> { + 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 and 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; +} From e6735b595c92f98ac7159d9ba828df04ab60182f Mon Sep 17 00:00:00 2001 From: seiwailai Date: Sat, 24 Apr 2021 01:17:32 +0800 Subject: [PATCH 2/7] project: Add disable sync button feature. Disable sync button if there is any pending, waiting or running job --- .../Project/ProjectDetail/ProjectDetail.jsx | 5 ++- .../Project/shared/ProjectSyncButton.jsx | 43 ++++++++++++++----- .../src/screens/Project/useWsProject.js | 2 +- 3 files changed, 38 insertions(+), 12 deletions(-) diff --git a/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.jsx b/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.jsx index 0e82027ba7..6418d253e4 100644 --- a/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.jsx +++ b/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.jsx @@ -208,7 +208,10 @@ function ProjectDetail({ project, i18n }) { )} {summary_fields.user_capabilities?.start && ( - + )} {summary_fields.user_capabilities?.delete && ( - + {isDisabled ? ( + +
+ +
+
+ ) : ( + + )} {error && ( Date: Sun, 25 Apr 2021 00:19:18 +0800 Subject: [PATCH 3/7] useWsProject: Add tests for useWsProject.jsx. Add tests like 'should return project detail', 'should establish websocket connection' and 'should update project status'. --- .../src/screens/Project/useWsProject.test.jsx | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 awx/ui_next/src/screens/Project/useWsProject.test.jsx diff --git a/awx/ui_next/src/screens/Project/useWsProject.test.jsx b/awx/ui_next/src/screens/Project/useWsProject.test.jsx new file mode 100644 index 0000000000..fb94ce1c45 --- /dev/null +++ b/awx/ui_next/src/screens/Project/useWsProject.test.jsx @@ -0,0 +1,137 @@ +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import WS from 'jest-websocket-mock'; +import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; +import useWsProject from './useWsProject'; + +function TestInner() { + return
; +} +function Test({ project }) { + const synced = useWsProject(project); + return ; +} + +describe('useWsProject', () => { + 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 project detail', async () => { + const project = { id: 1 }; + await act(async () => { + wrapper = await mountWithContexts(); + }); + + expect(wrapper.find('TestInner').prop('project')).toEqual(project); + WS.clean(); + }); + + test('should establish websocket connection', async () => { + global.document.cookie = 'csrftoken=abc123'; + const mockServer = new WS('ws://localhost/websocket/'); + + const project = { 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 project status', async () => { + global.document.cookie = 'csrftoken=abc123'; + const mockServer = new WS('ws://localhost/websocket/'); + + const project = { + id: 1, + summary_fields: { + last_job: { + id: 1, + status: 'running', + finished: null, + }, + }, + }; + 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'], + }, + }) + ); + expect( + wrapper.find('TestInner').prop('project').summary_fields.last_job.status + ).toEqual('running'); + + await act(async () => { + mockServer.send( + JSON.stringify({ + group_name: 'jobs', + project_id: 1, + status: 'pending', + type: 'project_update', + unified_job_id: 2, + unified_job_template_id: 1, + }) + ); + }); + wrapper.update(); + + expect( + wrapper.find('TestInner').prop('project').summary_fields.last_job + ).toEqual({ + id: 1, + status: 'running', + finished: null, + }); + // Should not update status to `Pending` if there is a current running or waiting job + await act(async () => { + mockServer.send( + JSON.stringify({ + group_name: 'jobs', + project_id: 1, + status: 'successful', + type: 'project_update', + unified_job_id: 1, + unified_job_template_id: 1, + finished: '2020-07-02T16:28:31.839071Z', + }) + ); + }); + wrapper.update(); + + expect( + wrapper.find('TestInner').prop('project').summary_fields.last_job + ).toEqual({ + id: 1, + status: 'successful', + finished: '2020-07-02T16:28:31.839071Z', + }); + WS.clean(); + }); +}); From bb896c0b0257a9a3cc2424bd3f9b49c30bceb5dd Mon Sep 17 00:00:00 2001 From: seiwailai Date: Sun, 25 Apr 2021 01:44:35 +0800 Subject: [PATCH 4/7] ProjectSyncButton: Add tests. Add 'disable button and set onClick to undefined on sync' and 'should render tooltip on sync' --- .../Project/shared/ProjectSyncButton.test.jsx | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/awx/ui_next/src/screens/Project/shared/ProjectSyncButton.test.jsx b/awx/ui_next/src/screens/Project/shared/ProjectSyncButton.test.jsx index 9f53dfb339..dceb8d7d16 100644 --- a/awx/ui_next/src/screens/Project/shared/ProjectSyncButton.test.jsx +++ b/awx/ui_next/src/screens/Project/shared/ProjectSyncButton.test.jsx @@ -41,6 +41,32 @@ describe('ProjectSyncButton', () => { expect(ProjectsAPI.sync).toHaveBeenCalledWith(1); }); + test('disable button and set onClick to undefined on sync', async () => { + await act(async () => { + wrapper = mountWithContexts( + + {children} + + ); + }); + + expect(wrapper.find('Button').prop('isDisabled')).toBe(true); + expect(wrapper.find('Button').prop('onClick')).toBe(undefined); + }); + test('should render tooltip on sync', async () => { + await act(async () => { + wrapper = mountWithContexts( + + {children} + + ); + }); + + expect(wrapper.find('Tooltip')).toHaveLength(1); + expect(wrapper.find('Tooltip').prop('content')).toEqual( + 'This project is currently on sync and cannot be clicked until sync process completed' + ); + }); test('displays error modal after unsuccessful sync', async () => { ProjectsAPI.sync.mockRejectedValue( new Error({ From 07d01c49c0c3495158592db482f9d55284d2114d Mon Sep 17 00:00:00 2001 From: seiwailai Date: Thu, 29 Apr 2021 15:50:53 +0800 Subject: [PATCH 5/7] ProjectListItem: Disabled sync button and render tooltip on sync. Added feature of disabling sync button when users click sync button and rendering tooltip when disabled sync button on hover. --- .../src/screens/Project/ProjectList/ProjectListItem.jsx | 5 ++++- .../src/screens/Project/ProjectList/ProjectListItem.test.jsx | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.jsx b/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.jsx index ebdb83e36c..f291a00403 100644 --- a/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.jsx +++ b/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.jsx @@ -169,7 +169,10 @@ function ProjectListItem({ visible={project.summary_fields.user_capabilities.start} tooltip={i18n._(t`Sync Project`)} > - + ', () => { description: '', name: 'Mock org', }, + last_job: { + id: 9000, + status: 'successful', + }, user_capabilities: { start: true, }, From 30d78e885772dcc1431d40716ba18f6bf3fe0c0d Mon Sep 17 00:00:00 2001 From: seiwailai Date: Sat, 1 May 2021 01:45:44 +0800 Subject: [PATCH 6/7] 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; -} From 0886414c723f6056534203ba71ce854bbfad66eb Mon Sep 17 00:00:00 2001 From: seiwailai Date: Sat, 1 May 2021 03:29:59 +0800 Subject: [PATCH 7/7] ProjectList: Change job status UI update logic. 1. Render current job status if there is current waiting, pending or running job.\n 2. Render last job status if there is no current job. --- .../Project/ProjectList/ProjectListItem.jsx | 20 +++++++++++++------ .../Project/ProjectList/useWsProjects.js | 2 +- .../ProjectList/useWsProjects.test.jsx | 6 +++--- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.jsx b/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.jsx index f291a00403..08058020f5 100644 --- a/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.jsx +++ b/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.jsx @@ -93,6 +93,14 @@ function ProjectListItem({ const missingExecutionEnvironment = project.custom_virtualenv && !project.default_environment; + let job = null; + + if (project.summary_fields?.current_job) { + job = project.summary_fields.current_job; + } else if (project.summary_fields?.last_job) { + job = project.summary_fields.last_job; + } + return ( <> @@ -132,14 +140,14 @@ function ProjectListItem({ )} - {project.summary_fields.last_job && ( + {job && ( - - + + )} @@ -171,7 +179,7 @@ function ProjectListItem({ > { { id: 1, summary_fields: { - last_job: { + current_job: { id: 1, status: 'running', finished: null, @@ -87,7 +87,7 @@ describe('useWsProjects', () => { }) ); expect( - wrapper.find('TestInner').prop('projects')[0].summary_fields.last_job + wrapper.find('TestInner').prop('projects')[0].summary_fields.current_job .status ).toEqual('running'); await act(async () => { @@ -104,7 +104,7 @@ describe('useWsProjects', () => { wrapper.update(); expect( - wrapper.find('TestInner').prop('projects')[0].summary_fields.last_job + wrapper.find('TestInner').prop('projects')[0].summary_fields.current_job ).toEqual({ id: 12, status: 'successful',