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.
This commit is contained in:
seiwailai 2021-05-01 01:45:44 +08:00
parent 07d01c49c0
commit 30d78e8857
5 changed files with 80 additions and 96 deletions

View File

@ -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);

View File

@ -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 (
<CardBody>
<DetailList gutter="sm">
<Detail
label={i18n._(t`Last Job Status`)}
value={
summary_fields.last_job && (
job && (
<Tooltip
position="top"
content={generateLastJobTooltip(summary_fields.last_job)}
key={summary_fields.last_job.id}
content={generateLastJobTooltip(job)}
key={job.id}
>
<Link to={`/jobs/project/${summary_fields.last_job.id}`}>
<StatusLabel status={summary_fields.last_job.status} />
<Link to={`/jobs/project/${job.id}`}>
<StatusLabel status={job.status} />
</Link>
</Tooltip>
)
@ -210,7 +219,7 @@ function ProjectDetail({ project, i18n }) {
{summary_fields.user_capabilities?.start && (
<ProjectSyncButton
projectId={project.id}
lastJobStatus={summary_fields.last_job.status}
lastJobStatus={job && job.status}
/>
)}
{summary_fields.user_capabilities?.delete && (

View File

@ -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;
}

View File

@ -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(<Test project={project} />);
});
@ -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',
});

View File

@ -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;
}