mirror of
https://github.com/ansible/awx.git
synced 2026-01-10 15:32:07 -03:30
Merge pull request #10510 from AlexSCorey/10440-8660-fix
Renders Command Module in job detail view and improves Ad Hoc Commands execution permissions SUMMARY This closes #10440 and #8660. On the job details view we now render the command module name, and the arguments. We also remove the run command button for user that do not have permission to launch a ad hoc command. ISSUE TYPE -enhancement COMPONENT NAME UI AWX VERSION ADDITIONAL INFORMATION Reviewed-by: Kersom <None> Reviewed-by: Sarah Akus <sakus@redhat.com>
This commit is contained in:
commit
e77d297a28
@ -3,7 +3,7 @@ import { useHistory, useParams } from 'react-router-dom';
|
||||
|
||||
import { t } from '@lingui/macro';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, DropdownItem } from '@patternfly/react-core';
|
||||
import { Button, DropdownItem, Tooltip } from '@patternfly/react-core';
|
||||
|
||||
import useRequest, { useDismissableError } from 'hooks/useRequest';
|
||||
import { InventoriesAPI, CredentialTypesAPI } from 'api';
|
||||
@ -14,7 +14,12 @@ import ErrorDetail from '../ErrorDetail';
|
||||
import AdHocCommandsWizard from './AdHocCommandsWizard';
|
||||
import ContentError from '../ContentError';
|
||||
|
||||
function AdHocCommands({ adHocItems, hasListItems, onLaunchLoading }) {
|
||||
function AdHocCommands({
|
||||
adHocItems,
|
||||
hasListItems,
|
||||
onLaunchLoading,
|
||||
moduleOptions,
|
||||
}) {
|
||||
const history = useHistory();
|
||||
const { id } = useParams();
|
||||
|
||||
@ -35,29 +40,21 @@ function AdHocCommands({ adHocItems, hasListItems, onLaunchLoading }) {
|
||||
}, [isKebabified, isWizardOpen, onKebabModalChange]);
|
||||
|
||||
const {
|
||||
result: {
|
||||
moduleOptions,
|
||||
credentialTypeId,
|
||||
isAdHocDisabled,
|
||||
organizationId,
|
||||
},
|
||||
result: { credentialTypeId, organizationId },
|
||||
request: fetchData,
|
||||
error: fetchError,
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
const [options, { data }, cred] = await Promise.all([
|
||||
InventoriesAPI.readAdHocOptions(id),
|
||||
const [{ data }, cred] = await Promise.all([
|
||||
InventoriesAPI.readDetail(id),
|
||||
CredentialTypesAPI.read({ namespace: 'ssh' }),
|
||||
]);
|
||||
return {
|
||||
moduleOptions: options.data.actions.GET.module_name.choices,
|
||||
credentialTypeId: cred.data.results[0].id,
|
||||
isAdHocDisabled: !options.data.actions.POST,
|
||||
organizationId: data.organization,
|
||||
};
|
||||
}, [id]),
|
||||
{ moduleOptions: [], isAdHocDisabled: true, organizationId: null }
|
||||
{ organizationId: null }
|
||||
);
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
@ -77,10 +74,6 @@ function AdHocCommands({ adHocItems, hasListItems, onLaunchLoading }) {
|
||||
)
|
||||
);
|
||||
|
||||
const { error, dismissError } = useDismissableError(
|
||||
launchError || fetchError
|
||||
);
|
||||
|
||||
const handleSubmit = async values => {
|
||||
const { credential, execution_environment, ...remainingValues } = values;
|
||||
const newCredential = credential[0].id;
|
||||
@ -97,6 +90,10 @@ function AdHocCommands({ adHocItems, hasListItems, onLaunchLoading }) {
|
||||
onLaunchLoading,
|
||||
]);
|
||||
|
||||
const { error, dismissError } = useDismissableError(
|
||||
launchError || fetchError
|
||||
);
|
||||
|
||||
if (error && isWizardOpen) {
|
||||
return (
|
||||
<AlertModal
|
||||
@ -123,27 +120,29 @@ function AdHocCommands({ adHocItems, hasListItems, onLaunchLoading }) {
|
||||
// render buttons for drop down and for toolbar
|
||||
// if modal is open render the modal
|
||||
<>
|
||||
{isKebabified ? (
|
||||
<DropdownItem
|
||||
key="cancel-job"
|
||||
isDisabled={isAdHocDisabled || !hasListItems}
|
||||
component="button"
|
||||
aria-label={t`Run Command`}
|
||||
onClick={() => setIsWizardOpen(true)}
|
||||
>
|
||||
{t`Run Command`}
|
||||
</DropdownItem>
|
||||
) : (
|
||||
<Button
|
||||
ouiaId="run-command-button"
|
||||
variant="secondary"
|
||||
aria-label={t`Run Command`}
|
||||
onClick={() => setIsWizardOpen(true)}
|
||||
isDisabled={isAdHocDisabled || !hasListItems}
|
||||
>
|
||||
{t`Run Command`}
|
||||
</Button>
|
||||
)}
|
||||
<Tooltip content={t`Run ad hoc command`}>
|
||||
{isKebabified ? (
|
||||
<DropdownItem
|
||||
key="cancel-job"
|
||||
isDisabled={!hasListItems}
|
||||
component="button"
|
||||
aria-label={t`Run Command`}
|
||||
onClick={() => setIsWizardOpen(true)}
|
||||
>
|
||||
{t`Run Command`}
|
||||
</DropdownItem>
|
||||
) : (
|
||||
<Button
|
||||
ouiaId="run-command-button"
|
||||
variant="secondary"
|
||||
aria-label={t`Run Command`}
|
||||
onClick={() => setIsWizardOpen(true)}
|
||||
isDisabled={!hasListItems}
|
||||
>
|
||||
{t`Run Command`}
|
||||
</Button>
|
||||
)}
|
||||
</Tooltip>
|
||||
|
||||
{isWizardOpen && (
|
||||
<AdHocCommandsWizard
|
||||
|
||||
@ -47,21 +47,6 @@ describe('<AdHocCommands />', () => {
|
||||
BRAND_NAME: 'AWX',
|
||||
},
|
||||
});
|
||||
InventoriesAPI.readAdHocOptions.mockResolvedValue({
|
||||
data: {
|
||||
actions: {
|
||||
GET: {
|
||||
module_name: {
|
||||
choices: [
|
||||
['command', 'command'],
|
||||
['shell', 'shell'],
|
||||
],
|
||||
},
|
||||
},
|
||||
POST: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
CredentialTypesAPI.read.mockResolvedValue({
|
||||
data: { count: 1, results: [{ id: 1, name: 'cred' }] },
|
||||
});
|
||||
@ -95,21 +80,6 @@ describe('<AdHocCommands />', () => {
|
||||
});
|
||||
|
||||
test('should open the wizard', async () => {
|
||||
InventoriesAPI.readAdHocOptions.mockResolvedValue({
|
||||
data: {
|
||||
actions: {
|
||||
GET: {
|
||||
module_name: {
|
||||
choices: [
|
||||
['command', 'command'],
|
||||
['foo', 'foo'],
|
||||
],
|
||||
},
|
||||
verbosity: { choices: [[1], [2]] },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
InventoriesAPI.readDetail.mockResolvedValue({ data: { organization: 1 } });
|
||||
CredentialTypesAPI.read.mockResolvedValue({
|
||||
data: { results: [{ id: 1 }] },
|
||||
@ -126,12 +96,21 @@ describe('<AdHocCommands />', () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<AdHocCommands
|
||||
moduleOptions={[
|
||||
['command', 'command'],
|
||||
['foo', 'foo'],
|
||||
]}
|
||||
adHocItems={adHocItems}
|
||||
hasListItems
|
||||
onLaunchLoading={() => jest.fn()}
|
||||
/>
|
||||
);
|
||||
});
|
||||
await waitForElement(
|
||||
wrapper,
|
||||
'button[aria-label="Run Command"]',
|
||||
el => el.length === 1
|
||||
);
|
||||
await act(async () =>
|
||||
wrapper.find('button[aria-label="Run Command"]').prop('onClick')()
|
||||
);
|
||||
@ -167,18 +146,26 @@ describe('<AdHocCommands />', () => {
|
||||
},
|
||||
});
|
||||
ExecutionEnvironmentsAPI.readOptions.mockResolvedValue({
|
||||
data: { actions: { GET: {} } },
|
||||
data: { actions: { GET: {}, POST: {} } },
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<AdHocCommands
|
||||
adHocItems={adHocItems}
|
||||
hasListItems
|
||||
moduleOptions={[
|
||||
['command', 'command'],
|
||||
['foo', 'foo'],
|
||||
]}
|
||||
onLaunchLoading={() => jest.fn()}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
await waitForElement(
|
||||
wrapper,
|
||||
'button[aria-label="Run Command"]',
|
||||
el => el.length === 1
|
||||
);
|
||||
await act(async () =>
|
||||
wrapper.find('button[aria-label="Run Command"]').prop('onClick')()
|
||||
);
|
||||
@ -277,23 +264,6 @@ describe('<AdHocCommands />', () => {
|
||||
},
|
||||
})
|
||||
);
|
||||
InventoriesAPI.readAdHocOptions.mockResolvedValue({
|
||||
data: {
|
||||
actions: {
|
||||
GET: {
|
||||
module_name: {
|
||||
choices: [
|
||||
['command', 'command'],
|
||||
['foo', 'foo'],
|
||||
],
|
||||
},
|
||||
verbosity: {
|
||||
choices: [[1], [2]],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
InventoriesAPI.readDetail.mockResolvedValue({
|
||||
data: { organization: 1 },
|
||||
});
|
||||
@ -334,18 +304,26 @@ describe('<AdHocCommands />', () => {
|
||||
},
|
||||
});
|
||||
ExecutionEnvironmentsAPI.readOptions.mockResolvedValue({
|
||||
data: { actions: { GET: {} } },
|
||||
data: { actions: { GET: {}, POST: {} } },
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<AdHocCommands
|
||||
adHocItems={adHocItems}
|
||||
moduleOptions={[
|
||||
['command', 'command'],
|
||||
['foo', 'foo'],
|
||||
]}
|
||||
hasListItems
|
||||
onLaunchLoading={() => jest.fn()}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
await waitForElement(
|
||||
wrapper,
|
||||
'button[aria-label="Run Command"]',
|
||||
el => el.length === 1
|
||||
);
|
||||
await act(async () =>
|
||||
wrapper.find('button[aria-label="Run Command"]').prop('onClick')()
|
||||
);
|
||||
@ -424,45 +402,18 @@ describe('<AdHocCommands />', () => {
|
||||
await waitForElement(wrapper, 'ErrorDetail', el => el.length > 0);
|
||||
});
|
||||
|
||||
test('should disable run command button due to permissions', async () => {
|
||||
InventoriesAPI.readHosts.mockResolvedValue({
|
||||
data: { results: [], count: 0 },
|
||||
});
|
||||
InventoriesAPI.readAdHocOptions.mockResolvedValue({
|
||||
data: {
|
||||
actions: {
|
||||
GET: { module_name: { choices: [['module']] } },
|
||||
},
|
||||
},
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<AdHocCommands
|
||||
adHocItems={adHocItems}
|
||||
hasListItems
|
||||
onLaunchLoading={() => jest.fn()}
|
||||
/>
|
||||
);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
const runCommandsButton = wrapper.find('button[aria-label="Run Command"]');
|
||||
expect(runCommandsButton.prop('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
test('should disable run command button due to lack of list items', async () => {
|
||||
InventoriesAPI.readHosts.mockResolvedValue({
|
||||
data: { results: [], count: 0 },
|
||||
});
|
||||
InventoriesAPI.readAdHocOptions.mockResolvedValue({
|
||||
data: {
|
||||
actions: {
|
||||
GET: { module_name: { choices: [['module']] } },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<AdHocCommands
|
||||
moduleOptions={[
|
||||
['command', 'command'],
|
||||
['foo', 'foo'],
|
||||
]}
|
||||
adHocItems={adHocItems}
|
||||
hasListItems={false}
|
||||
onLaunchLoading={() => jest.fn()}
|
||||
@ -475,7 +426,7 @@ describe('<AdHocCommands />', () => {
|
||||
});
|
||||
|
||||
test('should open alert modal when error on fetching data', async () => {
|
||||
InventoriesAPI.readAdHocOptions.mockRejectedValue(
|
||||
InventoriesAPI.readDetail.mockRejectedValue(
|
||||
new Error({
|
||||
response: {
|
||||
config: {
|
||||
@ -490,6 +441,10 @@ describe('<AdHocCommands />', () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<AdHocCommands
|
||||
moduleOptions={[
|
||||
['command', 'command'],
|
||||
['foo', 'foo'],
|
||||
]}
|
||||
adHocItems={adHocItems}
|
||||
hasListItems
|
||||
onLaunchLoading={() => jest.fn()}
|
||||
|
||||
@ -43,6 +43,8 @@ function InventoryGroupHostList() {
|
||||
actions,
|
||||
relatedSearchableKeys,
|
||||
searchableKeys,
|
||||
moduleOptions,
|
||||
isAdHocDisabled,
|
||||
},
|
||||
error: contentError,
|
||||
isLoading,
|
||||
@ -50,12 +52,15 @@ function InventoryGroupHostList() {
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
const params = parseQueryString(QS_CONFIG, location.search);
|
||||
const [response, actionsResponse] = await Promise.all([
|
||||
const [response, actionsResponse, options] = await Promise.all([
|
||||
GroupsAPI.readAllHosts(groupId, params),
|
||||
InventoriesAPI.readHostsOptions(inventoryId),
|
||||
InventoriesAPI.readAdHocOptions(inventoryId),
|
||||
]);
|
||||
|
||||
return {
|
||||
moduleOptions: options.data.actions.GET.module_name.choices,
|
||||
isAdHocDisabled: !options.data.actions.POST,
|
||||
hosts: response.data.results,
|
||||
hostCount: response.data.count,
|
||||
actions: actionsResponse.data.actions,
|
||||
@ -68,6 +73,8 @@ function InventoryGroupHostList() {
|
||||
};
|
||||
}, [groupId, inventoryId, location.search]),
|
||||
{
|
||||
moduleOptions: [],
|
||||
isAdHocDisabled: true,
|
||||
hosts: [],
|
||||
hostCount: 0,
|
||||
actions: {},
|
||||
@ -225,11 +232,16 @@ function InventoryGroupHostList() {
|
||||
qsConfig={QS_CONFIG}
|
||||
additionalControls={[
|
||||
...(canAdd ? [addButton] : []),
|
||||
<AdHocCommands
|
||||
adHocItems={selected}
|
||||
hasListItems={hostCount > 0}
|
||||
onLaunchLoading={setIsAdHocLaunchLoading}
|
||||
/>,
|
||||
...(!isAdHocDisabled
|
||||
? [
|
||||
<AdHocCommands
|
||||
adHocItems={selected}
|
||||
hasListItems={hostCount > 0}
|
||||
moduleOptions={moduleOptions}
|
||||
onLaunchLoading={setIsAdHocLaunchLoading}
|
||||
/>,
|
||||
]
|
||||
: []),
|
||||
<DisassociateButton
|
||||
key="disassociate"
|
||||
onDisassociate={handleDisassociate}
|
||||
|
||||
@ -35,6 +35,21 @@ describe('<InventoryGroupHostList />', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
InventoriesAPI.readAdHocOptions.mockResolvedValue({
|
||||
data: {
|
||||
actions: {
|
||||
GET: {
|
||||
module_name: {
|
||||
choices: [
|
||||
['command', 'command'],
|
||||
['shell', 'shell'],
|
||||
],
|
||||
},
|
||||
},
|
||||
POST: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<InventoryGroupHostList />);
|
||||
});
|
||||
@ -52,6 +67,7 @@ describe('<InventoryGroupHostList />', () => {
|
||||
test('should fetch inventory group hosts from api and render them in the list', () => {
|
||||
expect(GroupsAPI.readAllHosts).toHaveBeenCalled();
|
||||
expect(InventoriesAPI.readHostsOptions).toHaveBeenCalled();
|
||||
expect(InventoriesAPI.readAdHocOptions).toHaveBeenCalled();
|
||||
expect(wrapper.find('InventoryGroupHostListItem').length).toBe(3);
|
||||
});
|
||||
|
||||
@ -95,7 +111,7 @@ describe('<InventoryGroupHostList />', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('should show add dropdown button according to permissions', async () => {
|
||||
test('should show add dropdown button and Run Commands according to permissions', async () => {
|
||||
expect(wrapper.find('AddDropDownButton').length).toBe(1);
|
||||
InventoriesAPI.readHostsOptions.mockResolvedValueOnce({
|
||||
data: {
|
||||
@ -109,6 +125,7 @@ describe('<InventoryGroupHostList />', () => {
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
expect(wrapper.find('AddDropDownButton').length).toBe(0);
|
||||
expect(wrapper.find('AdHocCommands').length).toBe(1);
|
||||
});
|
||||
|
||||
test('expected api calls are made for multi-delete', async () => {
|
||||
@ -271,4 +288,24 @@ describe('<InventoryGroupHostList />', () => {
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
|
||||
});
|
||||
test('should not render ad hoc commands button', async () => {
|
||||
InventoriesAPI.readAdHocOptions.mockResolvedValue({
|
||||
data: {
|
||||
actions: {
|
||||
GET: {
|
||||
module_name: {
|
||||
choices: [
|
||||
['command', 'command'],
|
||||
['shell', 'shell'],
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<InventoryGroupHostList />);
|
||||
});
|
||||
expect(wrapper.find('AdHocCommands')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
@ -38,6 +38,8 @@ function InventoryGroupsList() {
|
||||
actions,
|
||||
relatedSearchableKeys,
|
||||
searchableKeys,
|
||||
moduleOptions,
|
||||
isAdHocDisabled,
|
||||
},
|
||||
error: contentError,
|
||||
isLoading,
|
||||
@ -45,12 +47,15 @@ function InventoryGroupsList() {
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
const params = parseQueryString(QS_CONFIG, location.search);
|
||||
const [response, groupOptions] = await Promise.all([
|
||||
const [response, groupOptions, options] = await Promise.all([
|
||||
InventoriesAPI.readGroups(inventoryId, params),
|
||||
InventoriesAPI.readGroupsOptions(inventoryId),
|
||||
InventoriesAPI.readAdHocOptions(inventoryId),
|
||||
]);
|
||||
|
||||
return {
|
||||
moduleOptions: options.data.actions.GET.module_name.choices,
|
||||
isAdHocDisabled: !options.data.actions.POST,
|
||||
groups: response.data.results,
|
||||
groupCount: response.data.count,
|
||||
actions: groupOptions.data.actions,
|
||||
@ -68,6 +73,8 @@ function InventoryGroupsList() {
|
||||
actions: {},
|
||||
relatedSearchableKeys: [],
|
||||
searchableKeys: [],
|
||||
moduleOptions: [],
|
||||
isAdHocDisabled: true,
|
||||
}
|
||||
);
|
||||
|
||||
@ -171,22 +178,29 @@ function InventoryGroupsList() {
|
||||
/>,
|
||||
]
|
||||
: []),
|
||||
<AdHocCommands
|
||||
adHocItems={selected}
|
||||
hasListItems={groupCount > 0}
|
||||
onLaunchLoading={setIsAdHocLaunchLoading}
|
||||
/>,
|
||||
...(!isAdHocDisabled
|
||||
? [
|
||||
<AdHocCommands
|
||||
adHocItems={selected}
|
||||
hasListItems={groupCount > 0}
|
||||
onLaunchLoading={setIsAdHocLaunchLoading}
|
||||
moduleOptions={moduleOptions}
|
||||
/>,
|
||||
]
|
||||
: []),
|
||||
<Tooltip content={renderTooltip()} position="top" key="delete">
|
||||
<InventoryGroupsDeleteModal
|
||||
groups={selected}
|
||||
isDisabled={
|
||||
selected.length === 0 || selected.some(cannotDelete)
|
||||
}
|
||||
onAfterDelete={() => {
|
||||
fetchData();
|
||||
clearSelected();
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<InventoryGroupsDeleteModal
|
||||
groups={selected}
|
||||
isDisabled={
|
||||
selected.length === 0 || selected.some(cannotDelete)
|
||||
}
|
||||
onAfterDelete={() => {
|
||||
fetchData();
|
||||
clearSelected();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>,
|
||||
]}
|
||||
/>
|
||||
|
||||
@ -76,6 +76,21 @@ describe('<InventoryGroupsList />', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
InventoriesAPI.readAdHocOptions.mockResolvedValue({
|
||||
data: {
|
||||
actions: {
|
||||
GET: {
|
||||
module_name: {
|
||||
choices: [
|
||||
['command', 'command'],
|
||||
['shell', 'shell'],
|
||||
],
|
||||
},
|
||||
},
|
||||
POST: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
const history = createMemoryHistory({
|
||||
initialEntries: ['/inventories/inventory/3/groups'],
|
||||
});
|
||||
@ -86,7 +101,12 @@ describe('<InventoryGroupsList />', () => {
|
||||
</Route>,
|
||||
{
|
||||
context: {
|
||||
router: { history, route: { location: history.location } },
|
||||
router: {
|
||||
history,
|
||||
route: {
|
||||
location: history.location,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
@ -103,6 +123,10 @@ describe('<InventoryGroupsList />', () => {
|
||||
expect(wrapper.find('InventoryGroupItem').length).toBe(3);
|
||||
});
|
||||
|
||||
test('should render Run Commands button', async () => {
|
||||
expect(wrapper.find('AdHocCommands')).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('should check and uncheck the row item', async () => {
|
||||
expect(
|
||||
wrapper
|
||||
@ -165,6 +189,27 @@ describe('<InventoryGroupsList />', () => {
|
||||
expect(el.find('input').props().checked).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
test('should not render ad hoc commands button', async () => {
|
||||
InventoriesAPI.readAdHocOptions.mockResolvedValue({
|
||||
data: {
|
||||
actions: {
|
||||
GET: {
|
||||
module_name: {
|
||||
choices: [
|
||||
['command', 'command'],
|
||||
['shell', 'shell'],
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<InventoryGroupsList />);
|
||||
});
|
||||
expect(wrapper.find('AdHocCommands')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('<InventoryGroupsList/> error handling', () => {
|
||||
@ -185,6 +230,21 @@ describe('<InventoryGroupsList/> error handling', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
InventoriesAPI.readAdHocOptions.mockResolvedValue({
|
||||
data: {
|
||||
actions: {
|
||||
GET: {
|
||||
module_name: {
|
||||
choices: [
|
||||
['command', 'command'],
|
||||
['shell', 'shell'],
|
||||
],
|
||||
},
|
||||
},
|
||||
POST: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
GroupsAPI.destroy.mockRejectedValue(
|
||||
new Error({
|
||||
response: {
|
||||
|
||||
@ -41,6 +41,8 @@ function InventoryHostGroupsList() {
|
||||
actions,
|
||||
relatedSearchableKeys,
|
||||
searchableKeys,
|
||||
moduleOptions,
|
||||
isAdHocDisabled,
|
||||
},
|
||||
error: contentError,
|
||||
isLoading,
|
||||
@ -54,12 +56,16 @@ function InventoryHostGroupsList() {
|
||||
data: { count, results },
|
||||
},
|
||||
hostGroupOptions,
|
||||
adHocOptions,
|
||||
] = await Promise.all([
|
||||
HostsAPI.readAllGroups(hostId, params),
|
||||
HostsAPI.readGroupsOptions(hostId),
|
||||
InventoriesAPI.readAdHocOptions(invId),
|
||||
]);
|
||||
|
||||
return {
|
||||
moduleOptions: adHocOptions.data.actions.GET.module_name.choices,
|
||||
isAdHocDisabled: !adHocOptions.data.actions.POST,
|
||||
groups: results,
|
||||
itemCount: count,
|
||||
actions: hostGroupOptions.data.actions,
|
||||
@ -77,6 +83,8 @@ function InventoryHostGroupsList() {
|
||||
actions: {},
|
||||
relatedSearchableKeys: [],
|
||||
searchableKeys: [],
|
||||
moduleOptions: [],
|
||||
isAdHocDisabled: true,
|
||||
}
|
||||
);
|
||||
|
||||
@ -209,11 +217,16 @@ function InventoryHostGroupsList() {
|
||||
/>,
|
||||
]
|
||||
: []),
|
||||
<AdHocCommands
|
||||
adHocItems={selected}
|
||||
hasListItems={itemCount > 0}
|
||||
onLaunchLoading={setIsAdHocLaunchLoading}
|
||||
/>,
|
||||
...(!isAdHocDisabled
|
||||
? [
|
||||
<AdHocCommands
|
||||
adHocItems={selected}
|
||||
hasListItems={itemCount > 0}
|
||||
moduleOptions={moduleOptions}
|
||||
onLaunchLoading={setIsAdHocLaunchLoading}
|
||||
/>,
|
||||
]
|
||||
: []),
|
||||
<DisassociateButton
|
||||
key="disassociate"
|
||||
onDisassociate={handleDisassociate}
|
||||
|
||||
@ -80,6 +80,21 @@ describe('<InventoryHostGroupsList />', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
InventoriesAPI.readAdHocOptions.mockResolvedValue({
|
||||
data: {
|
||||
actions: {
|
||||
GET: {
|
||||
module_name: {
|
||||
choices: [
|
||||
['command', 'command'],
|
||||
['shell', 'shell'],
|
||||
],
|
||||
},
|
||||
},
|
||||
POST: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
const history = createMemoryHistory({
|
||||
initialEntries: ['/inventories/inventory/1/hosts/3/groups'],
|
||||
});
|
||||
@ -111,6 +126,10 @@ describe('<InventoryHostGroupsList />', () => {
|
||||
expect(wrapper.find('InventoryHostGroupItem').length).toBe(3);
|
||||
});
|
||||
|
||||
test('should render Run Commands button', async () => {
|
||||
expect(wrapper.find('AdHocCommands')).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('should check and uncheck the row item', async () => {
|
||||
expect(
|
||||
wrapper
|
||||
@ -174,6 +193,27 @@ describe('<InventoryHostGroupsList />', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('should not render Run Commands button', async () => {
|
||||
InventoriesAPI.readAdHocOptions.mockResolvedValue({
|
||||
data: {
|
||||
actions: {
|
||||
GET: {
|
||||
module_name: {
|
||||
choices: [
|
||||
['command', 'command'],
|
||||
['shell', 'shell'],
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<InventoryHostGroupsList />);
|
||||
});
|
||||
expect(wrapper.find('AdHocCommands')).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('should show content error when api throws error on initial render', async () => {
|
||||
HostsAPI.readAllGroups.mockImplementation(() =>
|
||||
Promise.reject(new Error())
|
||||
|
||||
@ -35,6 +35,8 @@ function InventoryHostList() {
|
||||
actions,
|
||||
relatedSearchableKeys,
|
||||
searchableKeys,
|
||||
moduleOptions,
|
||||
isAdHocDisabled,
|
||||
},
|
||||
error: contentError,
|
||||
isLoading,
|
||||
@ -42,12 +44,15 @@ function InventoryHostList() {
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
const params = parseQueryString(QS_CONFIG, search);
|
||||
const [response, hostOptions] = await Promise.all([
|
||||
const [response, hostOptions, adHocOptions] = await Promise.all([
|
||||
InventoriesAPI.readHosts(id, params),
|
||||
InventoriesAPI.readHostsOptions(id),
|
||||
InventoriesAPI.readAdHocOptions(id),
|
||||
]);
|
||||
|
||||
return {
|
||||
moduleOptions: adHocOptions.data.actions.GET.module_name.choices,
|
||||
isAdHocDisabled: !adHocOptions.data.actions.POST,
|
||||
hosts: response.data.results,
|
||||
hostCount: response.data.count,
|
||||
actions: hostOptions.data.actions,
|
||||
@ -65,6 +70,8 @@ function InventoryHostList() {
|
||||
actions: {},
|
||||
relatedSearchableKeys: [],
|
||||
searchableKeys: [],
|
||||
moduleOptions: [],
|
||||
isAdHocDisabled: true,
|
||||
}
|
||||
);
|
||||
|
||||
@ -152,11 +159,16 @@ function InventoryHostList() {
|
||||
/>,
|
||||
]
|
||||
: []),
|
||||
<AdHocCommands
|
||||
adHocItems={selected}
|
||||
hasListItems={hostCount > 0}
|
||||
onLaunchLoading={setIsAdHocLaunchLoading}
|
||||
/>,
|
||||
...(!isAdHocDisabled
|
||||
? [
|
||||
<AdHocCommands
|
||||
moduleOptions={moduleOptions}
|
||||
adHocItems={selected}
|
||||
hasListItems={hostCount > 0}
|
||||
onLaunchLoading={setIsAdHocLaunchLoading}
|
||||
/>,
|
||||
]
|
||||
: []),
|
||||
<ToolbarDeleteButton
|
||||
key="delete"
|
||||
onDelete={handleDeleteHosts}
|
||||
|
||||
@ -93,6 +93,23 @@ describe('<InventoryHostList />', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
InventoriesAPI.readAdHocOptions.mockResolvedValue({
|
||||
data: {
|
||||
actions: {
|
||||
GET: {
|
||||
module_name: {
|
||||
choices: [
|
||||
['command', 'command'],
|
||||
['shell', 'shell'],
|
||||
],
|
||||
},
|
||||
},
|
||||
POST: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<InventoryHostList />);
|
||||
});
|
||||
@ -108,6 +125,10 @@ describe('<InventoryHostList />', () => {
|
||||
expect(wrapper.find('InventoryHostItem').length).toBe(3);
|
||||
});
|
||||
|
||||
test('should render Run Commands button', async () => {
|
||||
expect(wrapper.find('AdHocCommands')).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('should check and uncheck the row item', async () => {
|
||||
expect(
|
||||
wrapper
|
||||
@ -322,4 +343,27 @@ describe('<InventoryHostList />', () => {
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
|
||||
});
|
||||
|
||||
test('should not render Run Commands button', async () => {
|
||||
InventoriesAPI.readAdHocOptions.mockResolvedValue({
|
||||
data: {
|
||||
actions: {
|
||||
GET: {
|
||||
module_name: {
|
||||
choices: [
|
||||
['command', 'command'],
|
||||
['shell', 'shell'],
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<InventoryHostList inventory={mockInventory} />
|
||||
);
|
||||
});
|
||||
expect(wrapper.find('AdHocCommands')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
@ -43,18 +43,23 @@ function InventoryRelatedGroupList() {
|
||||
relatedSearchableKeys,
|
||||
searchableKeys,
|
||||
canAdd,
|
||||
moduleOptions,
|
||||
isAdHocDisabled,
|
||||
},
|
||||
isLoading,
|
||||
error: contentError,
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
const params = parseQueryString(QS_CONFIG, location.search);
|
||||
const [response, actions] = await Promise.all([
|
||||
const [response, actions, adHocOptions] = await Promise.all([
|
||||
GroupsAPI.readChildren(groupId, params),
|
||||
InventoriesAPI.readGroupsOptions(inventoryId),
|
||||
InventoriesAPI.readAdHocOptions(inventoryId),
|
||||
]);
|
||||
|
||||
return {
|
||||
moduleOptions: adHocOptions.data.actions.GET.module_name.choices,
|
||||
isAdHocDisabled: !adHocOptions.data.actions.POST,
|
||||
groups: response.data.results,
|
||||
itemCount: response.data.count,
|
||||
relatedSearchableKeys: (
|
||||
@ -68,7 +73,13 @@ function InventoryRelatedGroupList() {
|
||||
Object.prototype.hasOwnProperty.call(actions.data.actions, 'POST'),
|
||||
};
|
||||
}, [groupId, location.search, inventoryId]),
|
||||
{ groups: [], itemCount: 0, canAdd: false }
|
||||
{
|
||||
groups: [],
|
||||
itemCount: 0,
|
||||
canAdd: false,
|
||||
moduleOptions: [],
|
||||
isAdHocDisabled: true,
|
||||
}
|
||||
);
|
||||
useEffect(() => {
|
||||
fetchRelated();
|
||||
@ -199,11 +210,16 @@ function InventoryRelatedGroupList() {
|
||||
qsConfig={QS_CONFIG}
|
||||
additionalControls={[
|
||||
...(canAdd ? [addButton] : []),
|
||||
<AdHocCommands
|
||||
adHocItems={selected}
|
||||
hasListItems={itemCount > 0}
|
||||
onLaunchLoading={setIsAdHocLaunchLoading}
|
||||
/>,
|
||||
...(!isAdHocDisabled
|
||||
? [
|
||||
<AdHocCommands
|
||||
adHocItems={selected}
|
||||
hasListItems={itemCount > 0}
|
||||
onLaunchLoading={setIsAdHocLaunchLoading}
|
||||
moduleOptions={moduleOptions}
|
||||
/>,
|
||||
]
|
||||
: []),
|
||||
<DisassociateButton
|
||||
key="disassociate"
|
||||
onDisassociate={disassociateGroups}
|
||||
|
||||
@ -87,6 +87,21 @@ describe('<InventoryRelatedGroupList />', () => {
|
||||
],
|
||||
},
|
||||
});
|
||||
InventoriesAPI.readAdHocOptions.mockResolvedValue({
|
||||
data: {
|
||||
actions: {
|
||||
GET: {
|
||||
module_name: {
|
||||
choices: [
|
||||
['command', 'command'],
|
||||
['shell', 'shell'],
|
||||
],
|
||||
},
|
||||
},
|
||||
POST: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<InventoryRelatedGroupList />);
|
||||
});
|
||||
@ -107,6 +122,10 @@ describe('<InventoryRelatedGroupList />', () => {
|
||||
expect(wrapper.find('InventoryRelatedGroupListItem').length).toBe(3);
|
||||
});
|
||||
|
||||
test('should render Run Commands Button', async () => {
|
||||
expect(wrapper.find('AdHocCommands')).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('should check and uncheck the row item', async () => {
|
||||
expect(
|
||||
wrapper.find('input[aria-label="Select row 0"]').props().checked
|
||||
@ -220,4 +239,25 @@ describe('<InventoryRelatedGroupList />', () => {
|
||||
);
|
||||
expect(GroupsAPI.associateChildGroup).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should not render Run Commands button', async () => {
|
||||
InventoriesAPI.readAdHocOptions.mockResolvedValue({
|
||||
data: {
|
||||
actions: {
|
||||
GET: {
|
||||
module_name: {
|
||||
choices: [
|
||||
['command', 'command'],
|
||||
['shell', 'shell'],
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<InventoryRelatedGroupList />);
|
||||
});
|
||||
expect(wrapper.find('AdHocCommands')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
@ -76,7 +76,7 @@ function JobDetail({ job }) {
|
||||
project_update: t`Source Control Update`,
|
||||
inventory_update: t`Inventory Sync`,
|
||||
job: job.job_type === 'check' ? t`Playbook Check` : t`Playbook Run`,
|
||||
ad_hoc_command: t`Command`,
|
||||
ad_hoc_command: t`Run Command`,
|
||||
system_job: t`Management Job`,
|
||||
workflow_job: t`Workflow Job`,
|
||||
};
|
||||
@ -337,6 +337,8 @@ function JobDetail({ job }) {
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Detail label={t`Module Name`} value={job.module_name} />
|
||||
<Detail label={t`Module Arguments`} value={job.module_args} />
|
||||
<UserDateDetail
|
||||
label={t`Created`}
|
||||
date={job.created}
|
||||
|
||||
@ -110,6 +110,38 @@ describe('<JobDetail />', () => {
|
||||
).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('should display module name and module arguments', () => {
|
||||
wrapper = mountWithContexts(
|
||||
<JobDetail
|
||||
job={{
|
||||
...mockJobData,
|
||||
type: 'ad_hoc_command',
|
||||
module_name: 'command',
|
||||
module_args: 'echo hello_world',
|
||||
summary_fields: {
|
||||
...mockJobData.summary_fields,
|
||||
credential: {
|
||||
id: 2,
|
||||
name: 'Machine cred',
|
||||
description: '',
|
||||
kind: 'ssh',
|
||||
cloud: false,
|
||||
kubernetes: false,
|
||||
credential_type_id: 1,
|
||||
},
|
||||
source_workflow_job: {
|
||||
id: 1234,
|
||||
name: 'Test Source Workflow',
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
assertDetail('Module Name', 'command');
|
||||
assertDetail('Module Arguments', 'echo hello_world');
|
||||
assertDetail('Job Type', 'Run Command');
|
||||
});
|
||||
|
||||
test('should show schedule that launched workflow job', async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<JobDetail
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user