mirror of
https://github.com/ansible/awx.git
synced 2026-01-16 04:10:44 -03:30
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:
parent
07d01c49c0
commit
30d78e8857
@ -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);
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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',
|
||||
});
|
||||
@ -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;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user