mirror of
https://github.com/ansible/awx.git
synced 2026-05-09 02:17:37 -02:30
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:
@@ -17,7 +17,7 @@ import PaginatedDataList, {
|
|||||||
} from '../../../components/PaginatedDataList';
|
} from '../../../components/PaginatedDataList';
|
||||||
import useRequest, { useDeleteItems } from '../../../util/useRequest';
|
import useRequest, { useDeleteItems } from '../../../util/useRequest';
|
||||||
import { getQSConfig, parseQueryString } from '../../../util/qs';
|
import { getQSConfig, parseQueryString } from '../../../util/qs';
|
||||||
|
import useWsTemplates from './useWsTemplates';
|
||||||
import AddDropDownButton from '../../../components/AddDropDownButton';
|
import AddDropDownButton from '../../../components/AddDropDownButton';
|
||||||
import TemplateListItem from './TemplateListItem';
|
import TemplateListItem from './TemplateListItem';
|
||||||
|
|
||||||
@@ -36,27 +36,27 @@ function TemplateList({ i18n }) {
|
|||||||
const [selected, setSelected] = useState([]);
|
const [selected, setSelected] = useState([]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
result: { templates, count, jtActions, wfjtActions },
|
result: { results, count, jtActions, wfjtActions },
|
||||||
error: contentError,
|
error: contentError,
|
||||||
isLoading,
|
isLoading,
|
||||||
request: fetchTemplates,
|
request: fetchTemplates,
|
||||||
} = useRequest(
|
} = useRequest(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const params = parseQueryString(QS_CONFIG, location.search);
|
const params = parseQueryString(QS_CONFIG, location.search);
|
||||||
const results = await Promise.all([
|
const responses = await Promise.all([
|
||||||
UnifiedJobTemplatesAPI.read(params),
|
UnifiedJobTemplatesAPI.read(params),
|
||||||
JobTemplatesAPI.readOptions(),
|
JobTemplatesAPI.readOptions(),
|
||||||
WorkflowJobTemplatesAPI.readOptions(),
|
WorkflowJobTemplatesAPI.readOptions(),
|
||||||
]);
|
]);
|
||||||
return {
|
return {
|
||||||
templates: results[0].data.results,
|
results: responses[0].data.results,
|
||||||
count: results[0].data.count,
|
count: responses[0].data.count,
|
||||||
jtActions: results[1].data.actions,
|
jtActions: responses[1].data.actions,
|
||||||
wfjtActions: results[2].data.actions,
|
wfjtActions: responses[2].data.actions,
|
||||||
};
|
};
|
||||||
}, [location]),
|
}, [location]),
|
||||||
{
|
{
|
||||||
templates: [],
|
results: [],
|
||||||
count: 0,
|
count: 0,
|
||||||
jtActions: {},
|
jtActions: {},
|
||||||
wfjtActions: {},
|
wfjtActions: {},
|
||||||
@@ -67,6 +67,8 @@ function TemplateList({ i18n }) {
|
|||||||
fetchTemplates();
|
fetchTemplates();
|
||||||
}, [fetchTemplates]);
|
}, [fetchTemplates]);
|
||||||
|
|
||||||
|
const templates = useWsTemplates(results);
|
||||||
|
|
||||||
const isAllSelected =
|
const isAllSelected =
|
||||||
selected.length === templates.length && selected.length > 0;
|
selected.length === templates.length && selected.length > 0;
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ const mockTemplates = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
describe('<TemplateList />', () => {
|
describe('<TemplateList />', () => {
|
||||||
|
let debug;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
UnifiedJobTemplatesAPI.read.mockResolvedValue({
|
UnifiedJobTemplatesAPI.read.mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
@@ -88,10 +89,13 @@ describe('<TemplateList />', () => {
|
|||||||
actions: [],
|
actions: [],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
debug = global.console.debug; // eslint-disable-line prefer-destructuring
|
||||||
|
global.console.debug = () => {};
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
|
global.console.debug = debug;
|
||||||
});
|
});
|
||||||
|
|
||||||
test('initially renders successfully', async () => {
|
test('initially renders successfully', async () => {
|
||||||
|
|||||||
@@ -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),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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 don’t 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user