mirror of
https://github.com/ansible/awx.git
synced 2026-02-26 07:26:03 -03:30
Merge pull request #10987 from nixocio/ui_issue_9013
Add websockets to Inventory Source Details
This commit is contained in:
@@ -7,22 +7,27 @@ import {
|
|||||||
TextListItem,
|
TextListItem,
|
||||||
TextListVariants,
|
TextListVariants,
|
||||||
TextListItemVariants,
|
TextListItemVariants,
|
||||||
|
Tooltip,
|
||||||
} from '@patternfly/react-core';
|
} from '@patternfly/react-core';
|
||||||
import AlertModal from 'components/AlertModal';
|
import AlertModal from 'components/AlertModal';
|
||||||
import { CardBody, CardActionsRow } from 'components/Card';
|
|
||||||
import { VariablesDetail } from 'components/CodeEditor';
|
|
||||||
import ContentError from 'components/ContentError';
|
import ContentError from 'components/ContentError';
|
||||||
import ContentLoading from 'components/ContentLoading';
|
import ContentLoading from 'components/ContentLoading';
|
||||||
import CredentialChip from 'components/CredentialChip';
|
import CredentialChip from 'components/CredentialChip';
|
||||||
import DeleteButton from 'components/DeleteButton';
|
import DeleteButton from 'components/DeleteButton';
|
||||||
import ExecutionEnvironmentDetail from 'components/ExecutionEnvironmentDetail';
|
|
||||||
import { DetailList, Detail, UserDateDetail } from 'components/DetailList';
|
|
||||||
import ErrorDetail from 'components/ErrorDetail';
|
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 useRequest from 'hooks/useRequest';
|
||||||
import { InventorySourcesAPI } from 'api';
|
import { InventorySourcesAPI } from 'api';
|
||||||
import { relatedResourceDeleteRequests } from 'util/getRelatedResourceDeleteDetails';
|
import { relatedResourceDeleteRequests } from 'util/getRelatedResourceDeleteDetails';
|
||||||
import useIsMounted from 'hooks/useIsMounted';
|
import useIsMounted from 'hooks/useIsMounted';
|
||||||
|
import { formatDateString } from 'util/dates';
|
||||||
import InventorySourceSyncButton from '../shared/InventorySourceSyncButton';
|
import InventorySourceSyncButton from '../shared/InventorySourceSyncButton';
|
||||||
|
import useWsInventorySourcesDetails from '../InventorySources/useWsInventorySourcesDetails';
|
||||||
|
|
||||||
function InventorySourceDetail({ inventorySource }) {
|
function InventorySourceDetail({ inventorySource }) {
|
||||||
const {
|
const {
|
||||||
@@ -44,17 +49,20 @@ function InventorySourceDetail({ inventorySource }) {
|
|||||||
enabled_var,
|
enabled_var,
|
||||||
enabled_value,
|
enabled_value,
|
||||||
host_filter,
|
host_filter,
|
||||||
summary_fields: {
|
summary_fields,
|
||||||
created_by,
|
} = useWsInventorySourcesDetails(inventorySource);
|
||||||
credentials,
|
|
||||||
inventory,
|
const {
|
||||||
modified_by,
|
created_by,
|
||||||
organization,
|
credentials,
|
||||||
source_project,
|
inventory,
|
||||||
user_capabilities,
|
modified_by,
|
||||||
execution_environment,
|
organization,
|
||||||
},
|
source_project,
|
||||||
} = inventorySource;
|
user_capabilities,
|
||||||
|
execution_environment,
|
||||||
|
} = summary_fields;
|
||||||
|
|
||||||
const [deletionError, setDeletionError] = useState(false);
|
const [deletionError, setDeletionError] = useState(false);
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const isMounted = useIsMounted();
|
const isMounted = useIsMounted();
|
||||||
@@ -144,10 +152,51 @@ function InventorySourceDetail({ inventorySource }) {
|
|||||||
return <ContentError error={error} />;
|
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 (
|
return (
|
||||||
<CardBody>
|
<CardBody>
|
||||||
<DetailList>
|
<DetailList gutter="sm">
|
||||||
<Detail label={t`Name`} value={name} />
|
<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`Description`} value={description} />
|
||||||
<Detail label={t`Source`} value={sourceChoices[source]} />
|
<Detail label={t`Source`} value={sourceChoices[source]} />
|
||||||
{organization && (
|
{organization && (
|
||||||
@@ -226,9 +275,18 @@ function InventorySourceDetail({ inventorySource }) {
|
|||||||
{t`Edit`}
|
{t`Edit`}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{user_capabilities?.start && (
|
{user_capabilities?.start &&
|
||||||
<InventorySourceSyncButton source={inventorySource} icon={false} />
|
(['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 && (
|
{user_capabilities?.delete && (
|
||||||
<DeleteButton
|
<DeleteButton
|
||||||
name={name}
|
name={name}
|
||||||
@@ -236,6 +294,7 @@ function InventorySourceDetail({ inventorySource }) {
|
|||||||
onConfirm={handleDelete}
|
onConfirm={handleDelete}
|
||||||
deleteDetailsRequests={deleteDetailsRequests}
|
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?`}
|
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`}
|
{t`Delete`}
|
||||||
</DeleteButton>
|
</DeleteButton>
|
||||||
|
|||||||
@@ -56,6 +56,29 @@ describe('InventorySourceDetail', () => {
|
|||||||
jest.clearAllMocks();
|
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 () => {
|
test('should render expected details', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user