Merge pull request #7540 from keithjgrant/6618-websocket-projects-list

Add Websocket support to Projects List

Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
softwarefactory-project-zuul[bot] 2020-07-07 19:00:10 +00:00 committed by GitHub
commit bf28bdecb7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 210 additions and 5 deletions

View File

@ -1,6 +1,6 @@
import { useState, useEffect, useRef } from 'react';
import { useLocation } from 'react-router-dom';
import useThrottle from './useThrottle';
import useThrottle from '../../util/useThrottle';
import { parseQueryString } from '../../util/qs';
import sortJobs from './sortJobs';

View File

@ -8,7 +8,7 @@ import useWsJobs from './useWsJobs';
Jest mock timers dont play well with jest-websocket-mock,
so we'll stub out throttling to resolve immediately
*/
jest.mock('./useThrottle', () => ({
jest.mock('../../util/useThrottle', () => ({
__esModule: true,
default: jest.fn(val => val),
}));
@ -90,6 +90,7 @@ describe('useWsJobs hook', () => {
mockServer.send(
JSON.stringify({
unified_job_id: 1,
type: 'job',
status: 'successful',
})
);
@ -116,6 +117,7 @@ describe('useWsJobs hook', () => {
mockServer.send(
JSON.stringify({
unified_job_id: 2,
type: 'job',
status: 'running',
})
);

View File

@ -13,6 +13,7 @@ import PaginatedDataList, {
ToolbarAddButton,
ToolbarDeleteButton,
} from '../../../components/PaginatedDataList';
import useWsProjects from './useWsProjects';
import { getQSConfig, parseQueryString } from '../../../util/qs';
import ProjectListItem from './ProjectListItem';
@ -29,7 +30,7 @@ function ProjectList({ i18n }) {
const [selected, setSelected] = useState([]);
const {
result: { projects, itemCount, actions },
result: { results, itemCount, actions },
error: contentError,
isLoading,
request: fetchProjects,
@ -41,13 +42,13 @@ function ProjectList({ i18n }) {
ProjectsAPI.readOptions(),
]);
return {
projects: response.data.results,
results: response.data.results,
itemCount: response.data.count,
actions: actionsResponse.data.actions,
};
}, [location]),
{
projects: [],
results: [],
itemCount: 0,
actions: {},
}
@ -57,6 +58,8 @@ function ProjectList({ i18n }) {
fetchProjects();
}, [fetchProjects]);
const projects = useWsProjects(results);
const isAllSelected =
selected.length === projects.length && selected.length > 0;
const {

View File

@ -0,0 +1,85 @@
import { useState, useEffect, useRef } from 'react';
export default function useWsProjects(initialProjects) {
const [projects, setProjects] = useState(initialProjects);
const [lastMessage, setLastMessage] = useState(null);
const ws = useRef(null);
useEffect(() => {
setProjects(initialProjects);
}, [initialProjects]);
useEffect(() => {
if (!lastMessage?.unified_job_id || lastMessage.type !== 'project_update') {
return;
}
const index = projects.findIndex(p => p.id === lastMessage.project_id);
if (index === -1) {
return;
}
const project = projects[index];
const updatedProject = {
...project,
summary_fields: {
...project.summary_fields,
last_job: {
id: lastMessage.unified_job_id,
status: lastMessage.status,
finished: lastMessage.finished,
},
},
};
setProjects([
...projects.slice(0, index),
updatedProject,
...projects.slice(index + 1),
]);
}, [lastMessage]); // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => {
ws.current = new WebSocket(`wss://${window.location.host}/websocket/`);
const connect = () => {
const xrftoken = `; ${document.cookie}`
.split('; csrftoken=')
.pop()
.split(';')
.shift();
ws.current.send(
JSON.stringify({
xrftoken,
groups: {
jobs: ['status_changed'],
control: ['limit_reached_1'],
},
})
);
};
ws.current.onopen = connect;
ws.current.onmessage = e => {
setLastMessage(JSON.parse(e.data));
};
ws.current.onclose = e => {
// eslint-disable-next-line no-console
console.debug('Socket closed. Reconnecting...', e);
setTimeout(() => {
connect();
}, 1000);
};
ws.current.onerror = err => {
// eslint-disable-next-line no-console
console.debug('Socket error: ', err, 'Disconnecting...');
ws.current.close();
};
return () => {
ws.current.close();
};
}, []);
return projects;
}

View File

@ -0,0 +1,115 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import WS from 'jest-websocket-mock';
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
import useWsProjects from './useWsProjects';
function TestInner() {
return <div />;
}
function Test({ projects }) {
const synced = useWsProjects(projects);
return <TestInner projects={synced} />;
}
describe('useWsProjects', () => {
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 projects list', async () => {
const projects = [{ id: 1 }];
await act(async () => {
wrapper = await mountWithContexts(<Test projects={projects} />);
});
expect(wrapper.find('TestInner').prop('projects')).toEqual(projects);
WS.clean();
});
test('should establish websocket connection', async () => {
global.document.cookie = 'csrftoken=abc123';
const mockServer = new WS('wss://localhost/websocket/');
const projects = [{ id: 1 }];
await act(async () => {
wrapper = await mountWithContexts(<Test projects={projects} />);
});
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('wss://localhost/websocket/');
const projects = [
{
id: 1,
summary_fields: {
last_job: {
id: 1,
status: 'running',
finished: null,
},
},
},
];
await act(async () => {
wrapper = await mountWithContexts(<Test projects={projects} />);
});
await mockServer.connected;
await expect(mockServer).toReceiveMessage(
JSON.stringify({
xrftoken: 'abc123',
groups: {
jobs: ['status_changed'],
control: ['limit_reached_1'],
},
})
);
expect(
wrapper.find('TestInner').prop('projects')[0].summary_fields.last_job
.status
).toEqual('running');
await act(async () => {
mockServer.send(
JSON.stringify({
project_id: 1,
unified_job_id: 12,
type: 'project_update',
status: 'successful',
finished: '2020-07-02T16:28:31.839071Z',
})
);
});
wrapper.update();
expect(
wrapper.find('TestInner').prop('projects')[0].summary_fields.last_job
).toEqual({
id: 12,
status: 'successful',
finished: '2020-07-02T16:28:31.839071Z',
});
WS.clean();
});
});