Merge pull request #10987 from nixocio/ui_issue_9013

Add websockets to Inventory Source Details
This commit is contained in:
Kersom 2021-09-03 12:37:53 -04:00 committed by GitHub
commit 7bf3ee69ef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 259 additions and 19 deletions

View File

@ -7,22 +7,27 @@ import {
TextListItem,
TextListVariants,
TextListItemVariants,
Tooltip,
} from '@patternfly/react-core';
import AlertModal from 'components/AlertModal';
import { CardBody, CardActionsRow } from 'components/Card';
import { VariablesDetail } from 'components/CodeEditor';
import ContentError from 'components/ContentError';
import ContentLoading from 'components/ContentLoading';
import CredentialChip from 'components/CredentialChip';
import DeleteButton from 'components/DeleteButton';
import ExecutionEnvironmentDetail from 'components/ExecutionEnvironmentDetail';
import { DetailList, Detail, UserDateDetail } from 'components/DetailList';
import ErrorDetail from 'components/ErrorDetail';
import ExecutionEnvironmentDetail from 'components/ExecutionEnvironmentDetail';
import JobCancelButton from 'components/JobCancelButton';
import StatusLabel from 'components/StatusLabel';
import { CardBody, CardActionsRow } from 'components/Card';
import { DetailList, Detail, UserDateDetail } from 'components/DetailList';
import { VariablesDetail } from 'components/CodeEditor';
import useRequest from 'hooks/useRequest';
import { InventorySourcesAPI } from 'api';
import { relatedResourceDeleteRequests } from 'util/getRelatedResourceDeleteDetails';
import useIsMounted from 'hooks/useIsMounted';
import { formatDateString } from 'util/dates';
import InventorySourceSyncButton from '../shared/InventorySourceSyncButton';
import useWsInventorySourcesDetails from '../InventorySources/useWsInventorySourcesDetails';
function InventorySourceDetail({ inventorySource }) {
const {
@ -44,17 +49,20 @@ function InventorySourceDetail({ inventorySource }) {
enabled_var,
enabled_value,
host_filter,
summary_fields: {
created_by,
credentials,
inventory,
modified_by,
organization,
source_project,
user_capabilities,
execution_environment,
},
} = inventorySource;
summary_fields,
} = useWsInventorySourcesDetails(inventorySource);
const {
created_by,
credentials,
inventory,
modified_by,
organization,
source_project,
user_capabilities,
execution_environment,
} = summary_fields;
const [deletionError, setDeletionError] = useState(false);
const history = useHistory();
const isMounted = useIsMounted();
@ -144,10 +152,51 @@ function InventorySourceDetail({ inventorySource }) {
return <ContentError error={error} />;
}
const generateLastJobTooltip = (job) => (
<>
<div>{t`MOST RECENT SYNC`}</div>
<div>
{t`JOB ID:`} {job.id}
</div>
<div>
{t`STATUS:`} {job.status.toUpperCase()}
</div>
{job.finished && (
<div>
{t`FINISHED:`} {formatDateString(job.finished)}
</div>
)}
</>
);
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>
<DetailList gutter="sm">
<Detail label={t`Name`} value={name} />
<Detail
label={t`Last Job Status`}
value={
job && (
<Tooltip
position="top"
content={generateLastJobTooltip(job)}
key={job.id}
>
<Link to={`/jobs/inventory/${job.id}`}>
<StatusLabel status={job.status} />
</Link>
</Tooltip>
)
}
/>
<Detail label={t`Description`} value={description} />
<Detail label={t`Source`} value={sourceChoices[source]} />
{organization && (
@ -226,9 +275,18 @@ function InventorySourceDetail({ inventorySource }) {
{t`Edit`}
</Button>
)}
{user_capabilities?.start && (
<InventorySourceSyncButton source={inventorySource} icon={false} />
)}
{user_capabilities?.start &&
(['new', 'running', 'pending', 'waiting'].includes(job?.status) ? (
<JobCancelButton
job={{ id: job.id, type: 'inventory_update' }}
errorTitle={t`Inventory Source Sync Error`}
title={t`Cancel Inventory Source Sync`}
errorMessage={t`Failed to cancel Inventory Source Sync`}
buttonText={t`Cancel Sync`}
/>
) : (
<InventorySourceSyncButton source={inventorySource} icon={false} />
))}
{user_capabilities?.delete && (
<DeleteButton
name={name}
@ -236,6 +294,7 @@ function InventorySourceDetail({ inventorySource }) {
onConfirm={handleDelete}
deleteDetailsRequests={deleteDetailsRequests}
deleteMessage={t`This inventory source is currently being used by other resources that rely on it. Are you sure you want to delete it?`}
isDisabled={job?.status === 'running'}
>
{t`Delete`}
</DeleteButton>

View File

@ -56,6 +56,29 @@ describe('InventorySourceDetail', () => {
jest.clearAllMocks();
});
test('should render cancel button while job is running', async () => {
await act(async () => {
wrapper = mountWithContexts(
<InventorySourceDetail
inventorySource={{
...mockInvSource,
summary_fields: {
...mockInvSource.summary_fields,
current_job: {
id: 42,
status: 'running',
},
},
}}
/>
);
});
await waitForElement(wrapper, 'ContentLoading', (el) => el.length === 0);
expect(wrapper.find('InventorySourceDetail')).toHaveLength(1);
expect(wrapper.find('JobCancelButton').length).toBe(1);
});
test('should render expected details', async () => {
await act(async () => {
wrapper = mountWithContexts(

View File

@ -0,0 +1,42 @@
import { useState, useEffect } from 'react';
import useWebsocket from 'hooks/useWebsocket';
export default function useWsInventorySourcesDetails(initialSources) {
const [sources, setSources] = useState(initialSources);
const lastMessage = useWebsocket({
jobs: ['status_changed'],
control: ['limit_reached_1'],
});
useEffect(() => {
setSources(initialSources);
}, [initialSources]);
useEffect(
() => {
if (
!lastMessage?.unified_job_id ||
!lastMessage?.inventory_source_id ||
lastMessage.type !== 'inventory_update'
) {
return;
}
const updateSource = {
...sources,
summary_fields: {
...sources.summary_fields,
current_job: {
id: lastMessage.unified_job_id,
status: lastMessage.status,
finished: lastMessage.finished,
},
},
};
setSources(updateSource);
},
[lastMessage] // eslint-disable-line react-hooks/exhaustive-deps
);
return sources;
}

View File

@ -0,0 +1,116 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import WS from 'jest-websocket-mock';
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
import useWsInventorySourceDetails from './useWsInventorySourcesDetails';
function TestInner() {
return <div />;
}
function Test({ inventorySource }) {
const synced = useWsInventorySourceDetails(inventorySource);
return <TestInner inventorySource={synced} />;
}
describe('useWsProject', () => {
let wrapper;
test('should return inventory source detail', async () => {
const inventorySource = { id: 1 };
await act(async () => {
wrapper = await mountWithContexts(
<Test inventorySource={inventorySource} />
);
});
expect(wrapper.find('TestInner').prop('inventorySource')).toEqual(
inventorySource
);
WS.clean();
});
test('should establish websocket connection', async () => {
global.document.cookie = 'csrftoken=abc123';
const mockServer = new WS('ws://localhost/websocket/');
const inventorySource = { id: 1 };
await act(async () => {
wrapper = await mountWithContexts(
<Test inventorySource={inventorySource} />
);
});
await mockServer.connected;
await expect(mockServer).toReceiveMessage(
JSON.stringify({
xrftoken: 'abc123',
groups: {
jobs: ['status_changed'],
control: ['limit_reached_1'],
},
})
);
WS.clean();
});
test('should update inventory source status', async () => {
global.document.cookie = 'csrftoken=abc123';
const mockServer = new WS('ws://localhost/websocket/');
const inventorySource = {
id: 1,
summary_fields: {
current_job: {
id: 1,
status: 'running',
finished: null,
},
},
};
await act(async () => {
wrapper = await mountWithContexts(
<Test inventorySource={inventorySource} />
);
});
await mockServer.connected;
await expect(mockServer).toReceiveMessage(
JSON.stringify({
xrftoken: 'abc123',
groups: {
jobs: ['status_changed'],
control: ['limit_reached_1'],
},
})
);
expect(
wrapper.find('TestInner').prop('inventorySource').summary_fields
.current_job.status
).toEqual('running');
await act(async () => {
mockServer.send(
JSON.stringify({
group_name: 'jobs',
inventory_id: 1,
status: 'pending',
type: 'inventory_source',
unified_job_id: 2,
unified_job_template_id: 1,
inventory_source_id: 1,
})
);
});
wrapper.update();
expect(
wrapper.find('TestInner').prop('inventorySource').summary_fields
.current_job
).toEqual({
id: 1,
status: 'running',
finished: null,
});
WS.clean();
});
});