Merge pull request #7613 from keithjgrant/6622-template-list-websockets

Add websocket support to TemplateList

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

View File

@ -17,7 +17,7 @@ import PaginatedDataList, {
} from '../../../components/PaginatedDataList';
import useRequest, { useDeleteItems } from '../../../util/useRequest';
import { getQSConfig, parseQueryString } from '../../../util/qs';
import useWsTemplates from './useWsTemplates';
import AddDropDownButton from '../../../components/AddDropDownButton';
import TemplateListItem from './TemplateListItem';
@ -36,27 +36,27 @@ function TemplateList({ i18n }) {
const [selected, setSelected] = useState([]);
const {
result: { templates, count, jtActions, wfjtActions },
result: { results, count, jtActions, wfjtActions },
error: contentError,
isLoading,
request: fetchTemplates,
} = useRequest(
useCallback(async () => {
const params = parseQueryString(QS_CONFIG, location.search);
const results = await Promise.all([
const responses = await Promise.all([
UnifiedJobTemplatesAPI.read(params),
JobTemplatesAPI.readOptions(),
WorkflowJobTemplatesAPI.readOptions(),
]);
return {
templates: results[0].data.results,
count: results[0].data.count,
jtActions: results[1].data.actions,
wfjtActions: results[2].data.actions,
results: responses[0].data.results,
count: responses[0].data.count,
jtActions: responses[1].data.actions,
wfjtActions: responses[2].data.actions,
};
}, [location]),
{
templates: [],
results: [],
count: 0,
jtActions: {},
wfjtActions: {},
@ -67,6 +67,8 @@ function TemplateList({ i18n }) {
fetchTemplates();
}, [fetchTemplates]);
const templates = useWsTemplates(results);
const isAllSelected =
selected.length === templates.length && selected.length > 0;
const {

View File

@ -75,6 +75,7 @@ const mockTemplates = [
];
describe('<TemplateList />', () => {
let debug;
beforeEach(() => {
UnifiedJobTemplatesAPI.read.mockResolvedValue({
data: {
@ -88,10 +89,13 @@ describe('<TemplateList />', () => {
actions: [],
},
});
debug = global.console.debug; // eslint-disable-line prefer-destructuring
global.console.debug = () => {};
});
afterEach(() => {
jest.clearAllMocks();
global.console.debug = debug;
});
test('initially renders successfully', async () => {

View File

@ -0,0 +1,63 @@
import { useState, useEffect } from 'react';
import useWebsocket from '../../../util/useWebsocket';
export default function useWsTemplates(initialTemplates) {
const [templates, setTemplates] = useState(initialTemplates);
const lastMessage = useWebsocket({
jobs: ['status_changed'],
control: ['limit_reached_1'],
});
useEffect(() => {
setTemplates(initialTemplates);
}, [initialTemplates]);
useEffect(
function parseWsMessage() {
if (!lastMessage?.unified_job_id) {
return;
}
const index = templates.findIndex(
t => t.id === lastMessage.unified_job_template_id
);
if (index === -1) {
return;
}
const template = templates[index];
const updated = [...templates];
updated[index] = updateTemplate(template, lastMessage);
setTemplates(updated);
},
[lastMessage] // eslint-disable-line react-hooks/exhaustive-deps
);
return templates;
}
function updateTemplate(template, message) {
const recentJobs = [...(template.summary_fields.recent_jobs || [])];
const job = {
id: message.unified_job_id,
status: message.status,
finished: message.finished || null,
type: message.type,
};
const index = recentJobs.findIndex(j => j.id === job.id);
if (index > -1) {
recentJobs[index] = {
...recentJobs[index],
...job,
};
} else {
recentJobs.unshift(job);
}
return {
...template,
summary_fields: {
...template.summary_fields,
recent_jobs: recentJobs.slice(0, 10),
},
};
}

View File

@ -0,0 +1,193 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import WS from 'jest-websocket-mock';
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
import useWsTemplates from './useWsTemplates';
/*
Jest mock timers dont play well with jest-websocket-mock,
so we'll stub out throttling to resolve immediately
*/
jest.mock('../../../util/useThrottle', () => ({
__esModule: true,
default: jest.fn(val => val),
}));
function TestInner() {
return <div />;
}
function Test({ templates }) {
const syncedTemplates = useWsTemplates(templates);
return <TestInner templates={syncedTemplates} />;
}
describe('useWsTemplates hook', () => {
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 templates list', () => {
const templates = [{ id: 1 }];
wrapper = mountWithContexts(<Test templates={templates} />);
expect(wrapper.find('TestInner').prop('templates')).toEqual(templates);
WS.clean();
});
test('should establish websocket connection', async () => {
global.document.cookie = 'csrftoken=abc123';
const mockServer = new WS('wss://localhost/websocket/');
const templates = [{ id: 1 }];
await act(async () => {
wrapper = await mountWithContexts(<Test templates={templates} />);
});
await mockServer.connected;
await expect(mockServer).toReceiveMessage(
JSON.stringify({
xrftoken: 'abc123',
groups: {
jobs: ['status_changed'],
control: ['limit_reached_1'],
},
})
);
WS.clean();
});
test('should update recent job status', async () => {
global.document.cookie = 'csrftoken=abc123';
const mockServer = new WS('wss://localhost/websocket/');
const templates = [
{
id: 1,
summary_fields: {
recent_jobs: [
{
id: 10,
type: 'job',
status: 'running',
},
{
id: 11,
type: 'job',
status: 'successful',
},
],
},
},
];
await act(async () => {
wrapper = await mountWithContexts(<Test templates={templates} />);
});
await mockServer.connected;
await expect(mockServer).toReceiveMessage(
JSON.stringify({
xrftoken: 'abc123',
groups: {
jobs: ['status_changed'],
control: ['limit_reached_1'],
},
})
);
expect(
wrapper.find('TestInner').prop('templates')[0].summary_fields
.recent_jobs[0].status
).toEqual('running');
act(() => {
mockServer.send(
JSON.stringify({
unified_job_template_id: 1,
unified_job_id: 10,
type: 'job',
status: 'successful',
})
);
});
wrapper.update();
expect(
wrapper.find('TestInner').prop('templates')[0].summary_fields
.recent_jobs[0].status
).toEqual('successful');
WS.clean();
});
test('should add new job status', async () => {
global.document.cookie = 'csrftoken=abc123';
const mockServer = new WS('wss://localhost/websocket/');
const templates = [
{
id: 1,
summary_fields: {
recent_jobs: [
{
id: 10,
type: 'job',
status: 'running',
},
{
id: 11,
type: 'job',
status: 'successful',
},
],
},
},
];
await act(async () => {
wrapper = await mountWithContexts(<Test templates={templates} />);
});
await mockServer.connected;
await expect(mockServer).toReceiveMessage(
JSON.stringify({
xrftoken: 'abc123',
groups: {
jobs: ['status_changed'],
control: ['limit_reached_1'],
},
})
);
expect(
wrapper.find('TestInner').prop('templates')[0].summary_fields
.recent_jobs[0].status
).toEqual('running');
act(() => {
mockServer.send(
JSON.stringify({
unified_job_template_id: 1,
unified_job_id: 13,
type: 'job',
status: 'running',
})
);
});
wrapper.update();
expect(
wrapper.find('TestInner').prop('templates')[0].summary_fields.recent_jobs
).toHaveLength(3);
expect(
wrapper.find('TestInner').prop('templates')[0].summary_fields
.recent_jobs[0]
).toEqual({
id: 13,
status: 'running',
finished: null,
type: 'job',
});
WS.clean();
});
});