mirror of
https://github.com/ansible/awx.git
synced 2026-01-10 15:32:07 -03:30
Merge pull request #10004 from AlexSCorey/9864-AddEEtoAdHocWizard
Adds an execution environment step to the ad hoc commands SUMMARY This addresses some of #9864 by adding a step to select an execution environment to the ad hoc commands wizard ISSUE TYPE Bugfix Pull Request COMPONENT NAME UI AWX VERSION ADDITIONAL INFORMATION Reviewed-by: Kersom <None> Reviewed-by: Tiago Góes <tiago.goes2009@gmail.com>
This commit is contained in:
commit
43d33281a5
@ -12,10 +12,9 @@ import AlertModal from '../AlertModal';
|
||||
import ErrorDetail from '../ErrorDetail';
|
||||
import AdHocCommandsWizard from './AdHocCommandsWizard';
|
||||
import { KebabifiedContext } from '../../contexts/Kebabified';
|
||||
import ContentLoading from '../ContentLoading';
|
||||
import ContentError from '../ContentError';
|
||||
|
||||
function AdHocCommands({ adHocItems, i18n, hasListItems }) {
|
||||
function AdHocCommands({ adHocItems, i18n, hasListItems, onLaunchLoading }) {
|
||||
const history = useHistory();
|
||||
const { id } = useParams();
|
||||
|
||||
@ -36,22 +35,29 @@ function AdHocCommands({ adHocItems, i18n, hasListItems }) {
|
||||
}, [isKebabified, isWizardOpen, onKebabModalChange]);
|
||||
|
||||
const {
|
||||
result: { moduleOptions, credentialTypeId, isAdHocDisabled },
|
||||
result: {
|
||||
moduleOptions,
|
||||
credentialTypeId,
|
||||
isAdHocDisabled,
|
||||
organizationId,
|
||||
},
|
||||
request: fetchData,
|
||||
error: fetchError,
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
const [options, cred] = await Promise.all([
|
||||
const [options, { data }, cred] = await Promise.all([
|
||||
InventoriesAPI.readAdHocOptions(id),
|
||||
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 }
|
||||
{ moduleOptions: [], isAdHocDisabled: true, organizationId: null }
|
||||
);
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
@ -76,19 +82,20 @@ function AdHocCommands({ adHocItems, i18n, hasListItems }) {
|
||||
);
|
||||
|
||||
const handleSubmit = async values => {
|
||||
const { credential, ...remainingValues } = values;
|
||||
const { credential, execution_environment, ...remainingValues } = values;
|
||||
const newCredential = credential[0].id;
|
||||
|
||||
const manipulatedValues = {
|
||||
credential: newCredential,
|
||||
execution_environment: execution_environment[0]?.id,
|
||||
...remainingValues,
|
||||
};
|
||||
await launchAdHocCommands(manipulatedValues);
|
||||
};
|
||||
|
||||
if (isLaunchLoading) {
|
||||
return <ContentLoading />;
|
||||
}
|
||||
useEffect(() => onLaunchLoading(isLaunchLoading), [
|
||||
isLaunchLoading,
|
||||
onLaunchLoading,
|
||||
]);
|
||||
|
||||
if (error && isWizardOpen) {
|
||||
return (
|
||||
@ -141,6 +148,7 @@ function AdHocCommands({ adHocItems, i18n, hasListItems }) {
|
||||
{isWizardOpen && (
|
||||
<AdHocCommandsWizard
|
||||
adHocItems={adHocItems}
|
||||
organizationId={organizationId}
|
||||
moduleOptions={moduleOptions}
|
||||
verbosityOptions={verbosityOptions}
|
||||
credentialTypeId={credentialTypeId}
|
||||
|
||||
@ -4,12 +4,19 @@ import {
|
||||
mountWithContexts,
|
||||
waitForElement,
|
||||
} from '../../../testUtils/enzymeHelpers';
|
||||
import { CredentialTypesAPI, InventoriesAPI, CredentialsAPI } from '../../api';
|
||||
import {
|
||||
CredentialTypesAPI,
|
||||
InventoriesAPI,
|
||||
CredentialsAPI,
|
||||
ExecutionEnvironmentsAPI,
|
||||
} from '../../api';
|
||||
import AdHocCommands from './AdHocCommands';
|
||||
|
||||
jest.mock('../../api/models/CredentialTypes');
|
||||
jest.mock('../../api/models/Inventories');
|
||||
jest.mock('../../api/models/Credentials');
|
||||
jest.mock('../../api/models/ExecutionEnvironments');
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useParams: () => ({
|
||||
@ -51,6 +58,15 @@ describe('<AdHocCommands />', () => {
|
||||
CredentialTypesAPI.read.mockResolvedValue({
|
||||
data: { count: 1, results: [{ id: 1, name: 'cred' }] },
|
||||
});
|
||||
ExecutionEnvironmentsAPI.read.mockResolvedValue({
|
||||
data: {
|
||||
results: [
|
||||
{ id: 1, name: 'EE1 1', url: 'wwww.google.com' },
|
||||
{ id: 2, name: 'EE2', url: 'wwww.google.com' },
|
||||
],
|
||||
count: 2,
|
||||
},
|
||||
});
|
||||
});
|
||||
let wrapper;
|
||||
afterEach(() => {
|
||||
@ -61,7 +77,11 @@ describe('<AdHocCommands />', () => {
|
||||
test('mounts successfully', async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<AdHocCommands adHocItems={adHocItems} hasListItems />
|
||||
<AdHocCommands
|
||||
adHocItems={adHocItems}
|
||||
hasListItems
|
||||
onLaunchLoading={() => jest.fn()}
|
||||
/>
|
||||
);
|
||||
});
|
||||
expect(wrapper.find('AdHocCommands').length).toBe(1);
|
||||
@ -83,12 +103,26 @@ describe('<AdHocCommands />', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
InventoriesAPI.readDetail.mockResolvedValue({ data: { organization: 1 } });
|
||||
CredentialTypesAPI.read.mockResolvedValue({
|
||||
data: { results: [{ id: 1 }] },
|
||||
});
|
||||
ExecutionEnvironmentsAPI.read.mockResolvedValue({
|
||||
data: {
|
||||
results: [
|
||||
{ id: 1, name: 'EE1 1', url: 'wwww.google.com' },
|
||||
{ id: 2, name: 'EE2', url: 'wwww.google.com' },
|
||||
],
|
||||
count: 2,
|
||||
},
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<AdHocCommands adHocItems={adHocItems} hasListItems />
|
||||
<AdHocCommands
|
||||
adHocItems={adHocItems}
|
||||
hasListItems
|
||||
onLaunchLoading={() => jest.fn()}
|
||||
/>
|
||||
);
|
||||
});
|
||||
await act(async () =>
|
||||
@ -102,15 +136,35 @@ describe('<AdHocCommands />', () => {
|
||||
|
||||
test('should submit properly', async () => {
|
||||
InventoriesAPI.launchAdHocCommands.mockResolvedValue({ data: { id: 1 } });
|
||||
InventoriesAPI.readDetail.mockResolvedValue({
|
||||
data: { organization: 1 },
|
||||
});
|
||||
|
||||
CredentialsAPI.read.mockResolvedValue({
|
||||
data: {
|
||||
results: credentials,
|
||||
count: 5,
|
||||
},
|
||||
});
|
||||
ExecutionEnvironmentsAPI.read.mockResolvedValue({
|
||||
data: {
|
||||
results: [
|
||||
{ id: 1, name: 'EE1 1', url: 'wwww.google.com' },
|
||||
{ id: 2, name: 'EE2', url: 'wwww.google.com' },
|
||||
],
|
||||
count: 2,
|
||||
},
|
||||
});
|
||||
ExecutionEnvironmentsAPI.readOptions.mockResolvedValue({
|
||||
data: { actions: { GET: {} } },
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<AdHocCommands adHocItems={adHocItems} hasListItems />
|
||||
<AdHocCommands
|
||||
adHocItems={adHocItems}
|
||||
hasListItems
|
||||
onLaunchLoading={() => jest.fn()}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@ -147,8 +201,27 @@ describe('<AdHocCommands />', () => {
|
||||
wrapper.find('Button[type="submit"]').prop('onClick')()
|
||||
);
|
||||
await waitForElement(wrapper, 'ContentEmpty', el => el.length === 0);
|
||||
|
||||
// second step of wizard
|
||||
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find('input[aria-labelledby="check-action-item-2"]')
|
||||
.simulate('change', { target: { checked: true } });
|
||||
});
|
||||
|
||||
wrapper.update();
|
||||
|
||||
expect(
|
||||
wrapper.find('CheckboxListItem[label="EE2"]').prop('isSelected')
|
||||
).toBe(true);
|
||||
|
||||
await act(async () =>
|
||||
wrapper.find('Button[type="submit"]').prop('onClick')()
|
||||
);
|
||||
// third step of wizard
|
||||
await waitForElement(wrapper, 'ContentEmpty', el => el.length === 0);
|
||||
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find('input[aria-labelledby="check-action-item-4"]')
|
||||
@ -176,6 +249,7 @@ describe('<AdHocCommands />', () => {
|
||||
limit: 'Inventory 1 Org 0, Inventory 2 Org 0',
|
||||
module_name: 'command',
|
||||
verbosity: 1,
|
||||
execution_environment: 2,
|
||||
});
|
||||
});
|
||||
|
||||
@ -202,13 +276,24 @@ describe('<AdHocCommands />', () => {
|
||||
['foo', 'foo'],
|
||||
],
|
||||
},
|
||||
verbosity: { choices: [[1], [2]] },
|
||||
verbosity: {
|
||||
choices: [[1], [2]],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
InventoriesAPI.readDetail.mockResolvedValue({
|
||||
data: { organization: 1 },
|
||||
});
|
||||
CredentialTypesAPI.read.mockResolvedValue({
|
||||
data: { results: [{ id: 1 }] },
|
||||
data: {
|
||||
results: [
|
||||
{
|
||||
id: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
CredentialsAPI.read.mockResolvedValue({
|
||||
data: {
|
||||
@ -216,9 +301,33 @@ describe('<AdHocCommands />', () => {
|
||||
count: 5,
|
||||
},
|
||||
});
|
||||
ExecutionEnvironmentsAPI.read.mockResolvedValue({
|
||||
data: {
|
||||
results: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'EE1 1',
|
||||
url: 'wwww.google.com',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'EE2',
|
||||
url: 'wwww.google.com',
|
||||
},
|
||||
],
|
||||
count: 2,
|
||||
},
|
||||
});
|
||||
ExecutionEnvironmentsAPI.readOptions.mockResolvedValue({
|
||||
data: { actions: { GET: {} } },
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<AdHocCommands adHocItems={adHocItems} hasListItems />
|
||||
<AdHocCommands
|
||||
adHocItems={adHocItems}
|
||||
hasListItems
|
||||
onLaunchLoading={() => jest.fn()}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@ -240,7 +349,10 @@ describe('<AdHocCommands />', () => {
|
||||
'command'
|
||||
);
|
||||
wrapper.find('input#module_args').simulate('change', {
|
||||
target: { value: 'foo', name: 'module_args' },
|
||||
target: {
|
||||
value: 'foo',
|
||||
name: 'module_args',
|
||||
},
|
||||
});
|
||||
wrapper.find('AnsibleSelect[name="verbosity"]').prop('onChange')({}, 1);
|
||||
});
|
||||
@ -259,10 +371,36 @@ describe('<AdHocCommands />', () => {
|
||||
|
||||
// second step of wizard
|
||||
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find('input[aria-labelledby="check-action-item-2"]')
|
||||
.simulate('change', {
|
||||
target: {
|
||||
checked: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
wrapper.update();
|
||||
|
||||
expect(
|
||||
wrapper.find('CheckboxListItem[label="EE2"]').prop('isSelected')
|
||||
).toBe(true);
|
||||
|
||||
await act(async () =>
|
||||
wrapper.find('Button[type="submit"]').prop('onClick')()
|
||||
);
|
||||
// third step of wizard
|
||||
await waitForElement(wrapper, 'ContentEmpty', el => el.length === 0);
|
||||
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find('input[aria-labelledby="check-action-item-4"]')
|
||||
.simulate('change', { target: { checked: true } });
|
||||
.simulate('change', {
|
||||
target: {
|
||||
checked: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
wrapper.update();
|
||||
@ -291,7 +429,11 @@ describe('<AdHocCommands />', () => {
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<AdHocCommands adHocItems={adHocItems} hasListItems />
|
||||
<AdHocCommands
|
||||
adHocItems={adHocItems}
|
||||
hasListItems
|
||||
onLaunchLoading={() => jest.fn()}
|
||||
/>
|
||||
);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
@ -312,7 +454,11 @@ describe('<AdHocCommands />', () => {
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<AdHocCommands adHocItems={adHocItems} hasListItems={false} />
|
||||
<AdHocCommands
|
||||
adHocItems={adHocItems}
|
||||
hasListItems={false}
|
||||
onLaunchLoading={() => jest.fn()}
|
||||
/>
|
||||
);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
@ -335,7 +481,11 @@ describe('<AdHocCommands />', () => {
|
||||
);
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<AdHocCommands adHocItems={adHocItems} hasListItems />
|
||||
<AdHocCommands
|
||||
adHocItems={adHocItems}
|
||||
hasListItems
|
||||
onLaunchLoading={() => jest.fn()}
|
||||
/>
|
||||
);
|
||||
});
|
||||
await act(async () => wrapper.find('button').prop('onClick')());
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
|
||||
import { t } from '@lingui/macro';
|
||||
import { ExclamationCircleIcon as PFExclamationCircleIcon } from '@patternfly/react-icons';
|
||||
import { Tooltip } from '@patternfly/react-core';
|
||||
@ -10,6 +10,7 @@ import styled from 'styled-components';
|
||||
import Wizard from '../Wizard';
|
||||
import AdHocCredentialStep from './AdHocCredentialStep';
|
||||
import AdHocDetailsStep from './AdHocDetailsStep';
|
||||
import AdHocExecutionEnvironmentStep from './AdHocExecutionEnvironmentStep';
|
||||
|
||||
const AlertText = styled.div`
|
||||
color: var(--pf-global--danger-color--200);
|
||||
@ -23,11 +24,11 @@ const ExclamationCircleIcon = styled(PFExclamationCircleIcon)`
|
||||
|
||||
function AdHocCommandsWizard({
|
||||
onLaunch,
|
||||
i18n,
|
||||
moduleOptions,
|
||||
verbosityOptions,
|
||||
onCloseWizard,
|
||||
credentialTypeId,
|
||||
organizationId,
|
||||
}) {
|
||||
const [currentStepId, setCurrentStepId] = useState(1);
|
||||
const [enableLaunch, setEnableLaunch] = useState(false);
|
||||
@ -57,17 +58,17 @@ function AdHocCommandsWizard({
|
||||
key: 1,
|
||||
name: hasDetailsStepError ? (
|
||||
<AlertText>
|
||||
{i18n._(t`Details`)}
|
||||
{t`Details`}
|
||||
<Tooltip
|
||||
position="right"
|
||||
content={i18n._(t`This step contains errors`)}
|
||||
content={t`This step contains errors`}
|
||||
trigger="click mouseenter focus"
|
||||
>
|
||||
<ExclamationCircleIcon />
|
||||
</Tooltip>
|
||||
</AlertText>
|
||||
) : (
|
||||
i18n._(t`Details`)
|
||||
t`Details`
|
||||
),
|
||||
component: (
|
||||
<AdHocDetailsStep
|
||||
@ -76,12 +77,25 @@ function AdHocCommandsWizard({
|
||||
/>
|
||||
),
|
||||
enableNext: enabledNextOnDetailsStep(),
|
||||
nextButtonText: i18n._(t`Next`),
|
||||
nextButtonText: t`Next`,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
key: 2,
|
||||
name: i18n._(t`Machine credential`),
|
||||
name: t`Execution Environment`,
|
||||
component: (
|
||||
<AdHocExecutionEnvironmentStep organizationId={organizationId} />
|
||||
),
|
||||
// Removed this line when https://github.com/patternfly/patternfly-react/issues/5729 is fixed
|
||||
stepNavItemProps: { style: { 'white-space': 'nowrap' } },
|
||||
enableNext: true,
|
||||
nextButtonText: t`Next`,
|
||||
canJumpTo: currentStepId >= 2,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
key: 3,
|
||||
name: t`Machine credential`,
|
||||
component: (
|
||||
<AdHocCredentialStep
|
||||
credentialTypeId={credentialTypeId}
|
||||
@ -89,7 +103,7 @@ function AdHocCommandsWizard({
|
||||
/>
|
||||
),
|
||||
enableNext: enableLaunch && Object.values(errors).length === 0,
|
||||
nextButtonText: i18n._(t`Launch`),
|
||||
nextButtonText: t`Launch`,
|
||||
canJumpTo: currentStepId >= 2,
|
||||
},
|
||||
];
|
||||
@ -106,10 +120,10 @@ function AdHocCommandsWizard({
|
||||
onLaunch(values);
|
||||
}}
|
||||
steps={steps}
|
||||
title={i18n._(t`Run command`)}
|
||||
title={t`Run command`}
|
||||
nextButtonText={currentStep.nextButtonText || undefined}
|
||||
backButtonText={i18n._(t`Back`)}
|
||||
cancelButtonText={i18n._(t`Cancel`)}
|
||||
backButtonText={t`Back`}
|
||||
cancelButtonText={t`Cancel`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -128,6 +142,7 @@ const FormikApp = withFormik({
|
||||
module_name: '',
|
||||
extra_vars: '---',
|
||||
job_type: 'run',
|
||||
execution_environment: '',
|
||||
};
|
||||
},
|
||||
})(AdHocCommandsWizard);
|
||||
@ -139,4 +154,4 @@ FormikApp.propTypes = {
|
||||
onCloseWizard: PropTypes.func.isRequired,
|
||||
credentialTypeId: PropTypes.number.isRequired,
|
||||
};
|
||||
export default withI18n()(FormikApp);
|
||||
export default FormikApp;
|
||||
|
||||
@ -4,12 +4,14 @@ import {
|
||||
mountWithContexts,
|
||||
waitForElement,
|
||||
} from '../../../testUtils/enzymeHelpers';
|
||||
import { CredentialsAPI } from '../../api';
|
||||
import { CredentialsAPI, ExecutionEnvironmentsAPI } from '../../api';
|
||||
import AdHocCommandsWizard from './AdHocCommandsWizard';
|
||||
|
||||
jest.mock('../../api/models/CredentialTypes');
|
||||
jest.mock('../../api/models/Inventories');
|
||||
jest.mock('../../api/models/Credentials');
|
||||
jest.mock('../../api/models/ExecutionEnvironments');
|
||||
|
||||
const verbosityOptions = [
|
||||
{ value: '0', key: '0', label: '0 (Normal)' },
|
||||
{ value: '1', key: '1', label: '1 (Verbose)' },
|
||||
@ -39,6 +41,7 @@ describe('<AdHocCommandsWizard/>', () => {
|
||||
verbosityOptions={verbosityOptions}
|
||||
onCloseWizard={() => {}}
|
||||
credentialTypeId={1}
|
||||
organizationId={1}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@ -97,6 +100,18 @@ describe('<AdHocCommandsWizard/>', () => {
|
||||
wrapper.update();
|
||||
});
|
||||
test('launch button should become active', async () => {
|
||||
ExecutionEnvironmentsAPI.read.mockResolvedValue({
|
||||
data: {
|
||||
results: [
|
||||
{ id: 1, name: 'EE 1', url: '' },
|
||||
{ id: 2, name: 'EE 2', url: '' },
|
||||
],
|
||||
count: 2,
|
||||
},
|
||||
});
|
||||
ExecutionEnvironmentsAPI.readOptions.mockResolvedValue({
|
||||
data: { actions: { GET: {} } },
|
||||
});
|
||||
CredentialsAPI.read.mockResolvedValue({
|
||||
data: {
|
||||
results: [
|
||||
@ -127,10 +142,40 @@ describe('<AdHocCommandsWizard/>', () => {
|
||||
);
|
||||
|
||||
wrapper.update();
|
||||
|
||||
// step 2
|
||||
|
||||
await waitForElement(wrapper, 'OptionsList', el => el.length > 0);
|
||||
expect(wrapper.find('CheckboxListItem').length).toBe(2);
|
||||
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(
|
||||
false
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find('input[aria-labelledby="check-action-item-1"]')
|
||||
.simulate('change', { target: { checked: true } });
|
||||
});
|
||||
|
||||
wrapper.update();
|
||||
|
||||
expect(
|
||||
wrapper.find('CheckboxListItem[label="EE 1"]').prop('isSelected')
|
||||
).toBe(true);
|
||||
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(
|
||||
false
|
||||
);
|
||||
|
||||
await act(async () =>
|
||||
wrapper.find('Button[type="submit"]').prop('onClick')()
|
||||
);
|
||||
|
||||
wrapper.update();
|
||||
// step 3
|
||||
|
||||
await waitForElement(wrapper, 'OptionsList', el => el.length > 0);
|
||||
expect(wrapper.find('CheckboxListItem').length).toBe(2);
|
||||
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(true);
|
||||
|
||||
await act(async () => {
|
||||
wrapper
|
||||
.find('input[aria-labelledby="check-action-item-1"]')
|
||||
@ -150,8 +195,21 @@ describe('<AdHocCommandsWizard/>', () => {
|
||||
wrapper.find('Button[type="submit"]').prop('onClick')()
|
||||
);
|
||||
|
||||
expect(onLaunch).toHaveBeenCalled();
|
||||
expect(onLaunch).toHaveBeenCalledWith({
|
||||
become_enabled: '',
|
||||
credential: [{ id: 1, name: 'Cred 1', url: '' }],
|
||||
diff_mode: false,
|
||||
execution_environment: [{ id: 1, name: 'EE 1', url: '' }],
|
||||
extra_vars: '---',
|
||||
forks: 0,
|
||||
job_type: 'run',
|
||||
limit: 'Inventory 1, Inventory 2, inventory 3',
|
||||
module_args: 'foo',
|
||||
module_name: 'command',
|
||||
verbosity: 1,
|
||||
});
|
||||
});
|
||||
|
||||
test('should show error in navigation bar', async () => {
|
||||
await waitForElement(wrapper, 'WizardNavItem', el => el.length > 0);
|
||||
|
||||
@ -201,6 +259,12 @@ describe('<AdHocCommandsWizard/>', () => {
|
||||
wrapper.find('Button[type="submit"]').prop('onClick')()
|
||||
);
|
||||
|
||||
wrapper.update();
|
||||
|
||||
await act(async () =>
|
||||
wrapper.find('Button[type="submit"]').prop('onClick')()
|
||||
);
|
||||
|
||||
wrapper.update();
|
||||
expect(wrapper.find('ContentError').length).toBe(1);
|
||||
});
|
||||
|
||||
@ -0,0 +1,50 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { Formik } from 'formik';
|
||||
import {
|
||||
mountWithContexts,
|
||||
waitForElement,
|
||||
} from '../../../testUtils/enzymeHelpers';
|
||||
import { ExecutionEnvironmentsAPI } from '../../api';
|
||||
import AdHocExecutionEnvironmentStep from './AdHocExecutionEnvironmentStep';
|
||||
|
||||
jest.mock('../../api/models/ExecutionEnvironments');
|
||||
|
||||
describe('<AdHocExecutionEnvironmentStep />', () => {
|
||||
let wrapper;
|
||||
beforeEach(async () => {
|
||||
ExecutionEnvironmentsAPI.read.mockResolvedValue({
|
||||
data: {
|
||||
results: [
|
||||
{ id: 1, name: 'EE1 1', url: 'wwww.google.com' },
|
||||
{ id: 2, name: 'EE2', url: 'wwww.google.com' },
|
||||
],
|
||||
count: 2,
|
||||
},
|
||||
});
|
||||
ExecutionEnvironmentsAPI.readOptions.mockResolvedValue({
|
||||
data: { actions: { GET: {} } },
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<Formik>
|
||||
<AdHocExecutionEnvironmentStep organizationId={1} />
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('should mount properly', async () => {
|
||||
await waitForElement(wrapper, 'OptionsList', el => el.length > 0);
|
||||
});
|
||||
|
||||
test('should call api', async () => {
|
||||
await waitForElement(wrapper, 'OptionsList', el => el.length > 0);
|
||||
expect(ExecutionEnvironmentsAPI.read).toHaveBeenCalled();
|
||||
expect(wrapper.find('CheckboxListItem').length).toBe(2);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,141 @@
|
||||
import React, { useEffect, useCallback } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { t } from '@lingui/macro';
|
||||
import { useField } from 'formik';
|
||||
import { Form, FormGroup } from '@patternfly/react-core';
|
||||
import { ExecutionEnvironmentsAPI } from '../../api';
|
||||
import Popover from '../Popover';
|
||||
|
||||
import { parseQueryString, getQSConfig, mergeParams } from '../../util/qs';
|
||||
import useRequest from '../../util/useRequest';
|
||||
import ContentError from '../ContentError';
|
||||
import ContentLoading from '../ContentLoading';
|
||||
import OptionsList from '../OptionsList';
|
||||
|
||||
const QS_CONFIG = getQSConfig('execution_environments', {
|
||||
page: 1,
|
||||
page_size: 5,
|
||||
order_by: 'name',
|
||||
});
|
||||
function AdHocExecutionEnvironmentStep({ organizationId }) {
|
||||
const history = useHistory();
|
||||
const [executionEnvironmentField, , executionEnvironmentHelpers] = useField(
|
||||
'execution_environment'
|
||||
);
|
||||
const {
|
||||
error,
|
||||
isLoading,
|
||||
request: fetchExecutionEnvironments,
|
||||
result: {
|
||||
executionEnvironments,
|
||||
executionEnvironmentsCount,
|
||||
relatedSearchableKeys,
|
||||
searchableKeys,
|
||||
},
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
const params = parseQueryString(QS_CONFIG, history.location.search);
|
||||
const globallyAvailableParams = { or__organization__isnull: 'True' };
|
||||
const organizationIdParams = organizationId
|
||||
? { or__organization__id: organizationId }
|
||||
: {};
|
||||
|
||||
const [
|
||||
{
|
||||
data: { results, count },
|
||||
},
|
||||
actionsResponse,
|
||||
] = await Promise.all([
|
||||
ExecutionEnvironmentsAPI.read(
|
||||
mergeParams(params, {
|
||||
...globallyAvailableParams,
|
||||
...organizationIdParams,
|
||||
})
|
||||
),
|
||||
ExecutionEnvironmentsAPI.readOptions(),
|
||||
]);
|
||||
return {
|
||||
executionEnvironments: results,
|
||||
executionEnvironmentsCount: count,
|
||||
relatedSearchableKeys: (
|
||||
actionsResponse?.data?.related_search_fields || []
|
||||
).map(val => val.slice(0, -8)),
|
||||
searchableKeys: Object.keys(
|
||||
actionsResponse.data.actions?.GET || {}
|
||||
).filter(key => actionsResponse.data.actions?.GET[key].filterable),
|
||||
};
|
||||
}, [history.location.search, organizationId]),
|
||||
{
|
||||
executionEnvironments: [],
|
||||
executionEnvironmentsCount: 0,
|
||||
relatedSearchableKeys: [],
|
||||
searchableKeys: [],
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetchExecutionEnvironments();
|
||||
}, [fetchExecutionEnvironments]);
|
||||
|
||||
if (error) {
|
||||
return <ContentError error={error} />;
|
||||
}
|
||||
if (isLoading) {
|
||||
return <ContentLoading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Form>
|
||||
<FormGroup
|
||||
fieldId="execution_enviroment"
|
||||
label={t`Execution Environments`}
|
||||
aria-label={t`Execution Environments`}
|
||||
labelIcon={
|
||||
<Popover
|
||||
content={t`Select the Execution Environment you want this command to run inside.`}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<OptionsList
|
||||
isLoading={isLoading}
|
||||
value={executionEnvironmentField.value || []}
|
||||
options={executionEnvironments}
|
||||
optionCount={executionEnvironmentsCount}
|
||||
header={t`Execution Environments`}
|
||||
qsConfig={QS_CONFIG}
|
||||
searchColumns={[
|
||||
{
|
||||
name: t`Name`,
|
||||
key: 'name__icontains',
|
||||
isDefault: true,
|
||||
},
|
||||
{
|
||||
name: t`Created By (Username)`,
|
||||
key: 'created_by__username',
|
||||
},
|
||||
{
|
||||
name: t`Modified By (Username)`,
|
||||
key: 'modified_by__username',
|
||||
},
|
||||
]}
|
||||
sortColumns={[
|
||||
{
|
||||
name: t`Name`,
|
||||
key: 'name',
|
||||
},
|
||||
]}
|
||||
name="execution_environment"
|
||||
searchableKeys={searchableKeys}
|
||||
relatedSearchableKeys={relatedSearchableKeys}
|
||||
selectItem={value => {
|
||||
executionEnvironmentHelpers.setValue([value]);
|
||||
}}
|
||||
deselectItem={() => {
|
||||
executionEnvironmentHelpers.setValue([]);
|
||||
}}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
export default AdHocExecutionEnvironmentStep;
|
||||
@ -28,6 +28,7 @@ const QS_CONFIG = getQSConfig('host', {
|
||||
});
|
||||
|
||||
function InventoryGroupHostList({ i18n }) {
|
||||
const [isAdHocLaunchLoading, setIsAdHocLaunchLoading] = useState(false);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const { id: inventoryId, groupId } = useParams();
|
||||
const location = useLocation();
|
||||
@ -172,7 +173,9 @@ function InventoryGroupHostList({ i18n }) {
|
||||
<>
|
||||
<PaginatedDataList
|
||||
contentError={contentError}
|
||||
hasContentLoading={isLoading || isDisassociateLoading}
|
||||
hasContentLoading={
|
||||
isLoading || isDisassociateLoading || isAdHocLaunchLoading
|
||||
}
|
||||
items={hosts}
|
||||
itemCount={hostCount}
|
||||
pluralizedItemName={i18n._(t`Hosts`)}
|
||||
@ -215,6 +218,7 @@ function InventoryGroupHostList({ i18n }) {
|
||||
<AdHocCommands
|
||||
adHocItems={selected}
|
||||
hasListItems={hostCount > 0}
|
||||
onLaunchLoading={setIsAdHocLaunchLoading}
|
||||
/>,
|
||||
<DisassociateButton
|
||||
key="disassociate"
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useParams, useLocation } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
@ -30,6 +30,7 @@ function cannotDelete(item) {
|
||||
function InventoryGroupsList({ i18n }) {
|
||||
const location = useLocation();
|
||||
const { id: inventoryId } = useParams();
|
||||
const [isAdHocLaunchLoading, setIsAdHocLaunchLoading] = useState(false);
|
||||
|
||||
const {
|
||||
result: {
|
||||
@ -107,7 +108,7 @@ function InventoryGroupsList({ i18n }) {
|
||||
<>
|
||||
<PaginatedDataList
|
||||
contentError={contentError}
|
||||
hasContentLoading={isLoading}
|
||||
hasContentLoading={isLoading || isAdHocLaunchLoading}
|
||||
items={groups}
|
||||
itemCount={groupCount}
|
||||
qsConfig={QS_CONFIG}
|
||||
@ -174,6 +175,7 @@ function InventoryGroupsList({ i18n }) {
|
||||
<AdHocCommands
|
||||
adHocItems={selected}
|
||||
hasListItems={groupCount > 0}
|
||||
onLaunchLoading={setIsAdHocLaunchLoading}
|
||||
/>,
|
||||
<Tooltip content={renderTooltip()} position="top" key="delete">
|
||||
<InventoryGroupsDeleteModal
|
||||
|
||||
@ -28,6 +28,7 @@ const QS_CONFIG = getQSConfig('group', {
|
||||
|
||||
function InventoryHostGroupsList({ i18n }) {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isAdHocLaunchLoading, setIsAdHocLaunchLoading] = useState(false);
|
||||
const { hostId, id: invId } = useParams();
|
||||
const { search } = useLocation();
|
||||
|
||||
@ -147,7 +148,9 @@ function InventoryHostGroupsList({ i18n }) {
|
||||
<>
|
||||
<PaginatedDataList
|
||||
contentError={contentError}
|
||||
hasContentLoading={isLoading || isDisassociateLoading}
|
||||
hasContentLoading={
|
||||
isLoading || isDisassociateLoading || isAdHocLaunchLoading
|
||||
}
|
||||
items={groups}
|
||||
itemCount={itemCount}
|
||||
qsConfig={QS_CONFIG}
|
||||
@ -205,6 +208,7 @@ function InventoryHostGroupsList({ i18n }) {
|
||||
<AdHocCommands
|
||||
adHocItems={selected}
|
||||
hasListItems={itemCount > 0}
|
||||
onLaunchLoading={setIsAdHocLaunchLoading}
|
||||
/>,
|
||||
<DisassociateButton
|
||||
key="disassociate"
|
||||
|
||||
@ -23,6 +23,7 @@ const QS_CONFIG = getQSConfig('host', {
|
||||
|
||||
function InventoryHostList({ i18n }) {
|
||||
const [selected, setSelected] = useState([]);
|
||||
const [isAdHocLaunchLoading, setIsAdHocLaunchLoading] = useState(false);
|
||||
const { id } = useParams();
|
||||
const { search } = useLocation();
|
||||
|
||||
@ -106,7 +107,7 @@ function InventoryHostList({ i18n }) {
|
||||
<>
|
||||
<PaginatedDataList
|
||||
contentError={contentError}
|
||||
hasContentLoading={isLoading || isDeleteLoading}
|
||||
hasContentLoading={isLoading || isDeleteLoading || isAdHocLaunchLoading}
|
||||
items={hosts}
|
||||
itemCount={hostCount}
|
||||
pluralizedItemName={i18n._(t`Hosts`)}
|
||||
@ -152,6 +153,7 @@ function InventoryHostList({ i18n }) {
|
||||
<AdHocCommands
|
||||
adHocItems={selected}
|
||||
hasListItems={hostCount > 0}
|
||||
onLaunchLoading={setIsAdHocLaunchLoading}
|
||||
/>,
|
||||
<ToolbarDeleteButton
|
||||
key="delete"
|
||||
|
||||
@ -26,6 +26,7 @@ const QS_CONFIG = getQSConfig('group', {
|
||||
});
|
||||
function InventoryRelatedGroupList({ i18n }) {
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isAdHocLaunchLoading, setIsAdHocLaunchLoading] = useState(false);
|
||||
const [associateError, setAssociateError] = useState(null);
|
||||
const [disassociateError, setDisassociateError] = useState(null);
|
||||
const { id: inventoryId, groupId } = useParams();
|
||||
@ -154,7 +155,7 @@ function InventoryRelatedGroupList({ i18n }) {
|
||||
<>
|
||||
<PaginatedDataList
|
||||
contentError={contentError}
|
||||
hasContentLoading={isLoading}
|
||||
hasContentLoading={isLoading || isAdHocLaunchLoading}
|
||||
items={groups}
|
||||
itemCount={itemCount}
|
||||
pluralizedItemName={i18n._(t`Related Groups`)}
|
||||
@ -197,6 +198,7 @@ function InventoryRelatedGroupList({ i18n }) {
|
||||
<AdHocCommands
|
||||
adHocItems={selected}
|
||||
hasListItems={itemCount > 0}
|
||||
onLaunchLoading={setIsAdHocLaunchLoading}
|
||||
/>,
|
||||
<DisassociateButton
|
||||
key="disassociate"
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useCallback } from 'react';
|
||||
import React, { useEffect, useCallback, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
@ -20,7 +20,7 @@ const QS_CONFIG = getQSConfig('host', {
|
||||
|
||||
function SmartInventoryHostList({ i18n, inventory }) {
|
||||
const location = useLocation();
|
||||
|
||||
const [isAdHocLaunchLoading, setIsAdHocLaunchLoading] = useState(false);
|
||||
const {
|
||||
result: { hosts, count },
|
||||
error: contentError,
|
||||
@ -56,7 +56,7 @@ function SmartInventoryHostList({ i18n, inventory }) {
|
||||
<>
|
||||
<PaginatedDataList
|
||||
contentError={contentError}
|
||||
hasContentLoading={isLoading}
|
||||
hasContentLoading={isLoading || isAdHocLaunchLoading}
|
||||
items={hosts}
|
||||
itemCount={count}
|
||||
pluralizedItemName={i18n._(t`Hosts`)}
|
||||
@ -98,6 +98,7 @@ function SmartInventoryHostList({ i18n, inventory }) {
|
||||
<AdHocCommands
|
||||
adHocItems={selected}
|
||||
hasListItems={count > 0}
|
||||
onLaunchLoading={setIsAdHocLaunchLoading}
|
||||
/>,
|
||||
]
|
||||
: []
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user