mirror of
https://github.com/ansible/awx.git
synced 2026-01-16 12:20:45 -03:30
Adds Ad Hoc Commands Wizard
This commit is contained in:
parent
caa7b43fe0
commit
e6ae171f4b
@ -99,6 +99,17 @@ class Inventories extends InstanceGroupsMixin(Base) {
|
||||
`${this.baseUrl}${inventoryId}/update_inventory_sources/`
|
||||
);
|
||||
}
|
||||
|
||||
readAdHocOptions(inventoryId) {
|
||||
return this.http.options(`${this.baseUrl}${inventoryId}/ad_hoc_commands/`);
|
||||
}
|
||||
|
||||
launchAdHocCommands(inventoryId, values) {
|
||||
return this.http.post(
|
||||
`${this.baseUrl}${inventoryId}/ad_hoc_commands/`,
|
||||
values
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Inventories;
|
||||
|
||||
145
awx/ui_next/src/components/AdHocCommands/AdHocCommands.jsx
Normal file
145
awx/ui_next/src/components/AdHocCommands/AdHocCommands.jsx
Normal file
@ -0,0 +1,145 @@
|
||||
import React, { useState, Fragment, useCallback, useEffect } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
|
||||
import useRequest, { useDismissableError } from '../../util/useRequest';
|
||||
import AlertModal from '../AlertModal';
|
||||
import { CredentialTypesAPI } from '../../api';
|
||||
import ErrorDetail from '../ErrorDetail';
|
||||
import AdHocCommandsForm from './AdHocCommandsWizard';
|
||||
import ContentLoading from '../ContentLoading';
|
||||
import ContentError from '../ContentError';
|
||||
|
||||
function AdHocCommands({ children, apiModule, adHocItems, itemId, i18n }) {
|
||||
const [isWizardOpen, setIsWizardOpen] = useState(false);
|
||||
const history = useHistory();
|
||||
|
||||
const {
|
||||
error: fetchError,
|
||||
request: fetchModuleOptions,
|
||||
result: { moduleOptions, verbosityOptions, credentialTypeId },
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
const [choices, credId] = await Promise.all([
|
||||
apiModule.readAdHocOptions(itemId),
|
||||
CredentialTypesAPI.read({ namespace: 'ssh' }),
|
||||
]);
|
||||
const itemObject = (item, index) => {
|
||||
return {
|
||||
key: index,
|
||||
value: item,
|
||||
label: `${item}`,
|
||||
isDisabled: false,
|
||||
};
|
||||
};
|
||||
|
||||
const options = choices.data.actions.GET.module_name.choices.map(
|
||||
(choice, index) => itemObject(choice[0], index)
|
||||
);
|
||||
|
||||
const verbosityItems = choices.data.actions.GET.verbosity.choices.map(
|
||||
(choice, index) => itemObject(choice[0], index)
|
||||
);
|
||||
|
||||
return {
|
||||
moduleOptions: [itemObject('', -1), ...options],
|
||||
verbosityOptions: [itemObject('', -1), ...verbosityItems],
|
||||
credentialTypeId: credId.data.results[0].id,
|
||||
};
|
||||
}, [itemId, apiModule]),
|
||||
{ moduleOptions: [] }
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetchModuleOptions();
|
||||
}, [fetchModuleOptions]);
|
||||
|
||||
const {
|
||||
isloading: isLaunchLoading,
|
||||
error: launchError,
|
||||
request: launchAdHocCommands,
|
||||
} = useRequest(
|
||||
useCallback(
|
||||
async values => {
|
||||
const { data } = await apiModule.launchAdHocCommands(itemId, values);
|
||||
history.push(`/jobs/${data.module_name}/${data.id}/output`);
|
||||
},
|
||||
[apiModule, itemId, history]
|
||||
)
|
||||
);
|
||||
|
||||
const { error, dismissError } = useDismissableError(
|
||||
launchError || fetchError
|
||||
);
|
||||
|
||||
const handleSubmit = async (values, limitTypedValue) => {
|
||||
const { credential, limit, ...remainingValues } = values;
|
||||
const newCredential = credential[0].id;
|
||||
if (limitTypedValue) {
|
||||
values.limit = limit.concat(limitTypedValue);
|
||||
}
|
||||
const stringifyLimit = values.limit.join(', ').trim();
|
||||
|
||||
const manipulatedValues = {
|
||||
limit: stringifyLimit[0],
|
||||
credential: newCredential,
|
||||
...remainingValues,
|
||||
};
|
||||
await launchAdHocCommands(manipulatedValues);
|
||||
setIsWizardOpen(false);
|
||||
};
|
||||
|
||||
if (isLaunchLoading) {
|
||||
return <ContentLoading />;
|
||||
}
|
||||
|
||||
if (error && isWizardOpen) {
|
||||
return (
|
||||
<AlertModal
|
||||
isOpen={error}
|
||||
variant="error"
|
||||
title={i18n._(t`Error!`)}
|
||||
onClose={() => {
|
||||
dismissError();
|
||||
setIsWizardOpen(false);
|
||||
}}
|
||||
>
|
||||
<ContentError error={error} />
|
||||
</AlertModal>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Fragment>
|
||||
{children({
|
||||
openAdHocCommands: () => setIsWizardOpen(true),
|
||||
})}
|
||||
|
||||
{isWizardOpen && (
|
||||
<AdHocCommandsForm
|
||||
adHocItems={adHocItems}
|
||||
moduleOptions={moduleOptions}
|
||||
verbosityOptions={verbosityOptions}
|
||||
credentialTypeId={credentialTypeId}
|
||||
onCloseWizard={() => setIsWizardOpen(false)}
|
||||
onLaunch={handleSubmit}
|
||||
error={error}
|
||||
onDismissError={() => dismissError()}
|
||||
/>
|
||||
)}
|
||||
{launchError && (
|
||||
<AlertModal
|
||||
isOpen={error}
|
||||
variant="error"
|
||||
title={i18n._(t`Error!`)}
|
||||
onClose={dismissError}
|
||||
>
|
||||
{i18n._(t`Failed to launch job.`)}
|
||||
<ErrorDetail error={error} />
|
||||
</AlertModal>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
export default withI18n()(AdHocCommands);
|
||||
339
awx/ui_next/src/components/AdHocCommands/AdHocCommands.test.jsx
Normal file
339
awx/ui_next/src/components/AdHocCommands/AdHocCommands.test.jsx
Normal file
@ -0,0 +1,339 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import {
|
||||
mountWithContexts,
|
||||
waitForElement,
|
||||
} from '../../../testUtils/enzymeHelpers';
|
||||
import { CredentialTypesAPI, InventoriesAPI, CredentialsAPI } from '../../api';
|
||||
import AdHocCommands from './AdHocCommands';
|
||||
|
||||
jest.mock('../../api/models/CredentialTypes');
|
||||
jest.mock('../../api/models/Inventories');
|
||||
jest.mock('../../api/models/Credentials');
|
||||
|
||||
const credentials = [
|
||||
{ id: 1, kind: 'cloud', name: 'Cred 1', url: 'www.google.com' },
|
||||
{ id: 2, kind: 'ssh', name: 'Cred 2', url: 'www.google.com' },
|
||||
{ id: 3, kind: 'Ansible', name: 'Cred 3', url: 'www.google.com' },
|
||||
{ id: 4, kind: 'Machine', name: 'Cred 4', url: 'www.google.com' },
|
||||
{ id: 5, kind: 'Machine', name: 'Cred 5', url: 'www.google.com' },
|
||||
];
|
||||
const adHocItems = [
|
||||
{
|
||||
name: ' Inventory 1 Org 0',
|
||||
},
|
||||
{ name: 'Inventory 2 Org 0' },
|
||||
];
|
||||
|
||||
const children = ({ openAdHocCommands }) => (
|
||||
<button type="submit" onClick={() => openAdHocCommands()} />
|
||||
);
|
||||
|
||||
describe('<AdHocCOmmands />', () => {
|
||||
let wrapper;
|
||||
afterEach(() => {
|
||||
wrapper.unmount();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('mounts successfully', async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<AdHocCommands
|
||||
apiModule={InventoriesAPI}
|
||||
adHocItems={adHocItems}
|
||||
itemId={1}
|
||||
>
|
||||
{children}
|
||||
</AdHocCommands>
|
||||
);
|
||||
});
|
||||
expect(wrapper.find('AdHocCommands').length).toBe(1);
|
||||
});
|
||||
test('calls api on Mount', async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<AdHocCommands
|
||||
apiModule={InventoriesAPI}
|
||||
adHocItems={adHocItems}
|
||||
itemId={1}
|
||||
>
|
||||
{children}
|
||||
</AdHocCommands>
|
||||
);
|
||||
});
|
||||
expect(wrapper.find('AdHocCommands').length).toBe(1);
|
||||
expect(InventoriesAPI.readAdHocOptions).toBeCalledWith(1);
|
||||
expect(CredentialTypesAPI.read).toBeCalledWith({ namespace: 'ssh' });
|
||||
});
|
||||
test('should open the wizard', async () => {
|
||||
InventoriesAPI.readAdHocOptions.mockResolvedValue({
|
||||
data: {
|
||||
actions: {
|
||||
GET: {
|
||||
module_name: {
|
||||
choices: [
|
||||
['command', 'command'],
|
||||
['foo', 'foo'],
|
||||
],
|
||||
},
|
||||
verbosity: { choices: [[1], [2]] },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
CredentialTypesAPI.read.mockResolvedValue({
|
||||
data: { results: [{ id: 1 }] },
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<AdHocCommands
|
||||
apiModule={InventoriesAPI}
|
||||
adHocItems={adHocItems}
|
||||
itemId={1}
|
||||
>
|
||||
{children}
|
||||
</AdHocCommands>
|
||||
);
|
||||
});
|
||||
wrapper.find('button').prop('onClick')();
|
||||
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find('AdHocCommandsWizard').length).toBe(1);
|
||||
});
|
||||
|
||||
test('should submit properly', async () => {
|
||||
InventoriesAPI.readAdHocOptions.mockResolvedValue({
|
||||
data: {
|
||||
actions: {
|
||||
GET: {
|
||||
module_name: {
|
||||
choices: [
|
||||
['command', 'command'],
|
||||
['foo', 'foo'],
|
||||
],
|
||||
},
|
||||
verbosity: { choices: [[1], [2]] },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
CredentialTypesAPI.read.mockResolvedValue({
|
||||
data: { results: [{ id: 1 }] },
|
||||
});
|
||||
CredentialsAPI.read.mockResolvedValue({
|
||||
data: {
|
||||
results: credentials,
|
||||
count: 5,
|
||||
},
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<AdHocCommands
|
||||
apiModule={InventoriesAPI}
|
||||
adHocItems={adHocItems}
|
||||
itemId={1}
|
||||
>
|
||||
{children}
|
||||
</AdHocCommands>
|
||||
);
|
||||
});
|
||||
wrapper.find('button').prop('onClick')();
|
||||
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(true);
|
||||
expect(
|
||||
wrapper
|
||||
.find('WizardNavItem[content="Machine Credential"]')
|
||||
.prop('isDisabled')
|
||||
).toBe(true);
|
||||
|
||||
act(() => {
|
||||
wrapper.find('AnsibleSelect[name="module_args"]').prop('onChange')(
|
||||
{},
|
||||
'command'
|
||||
);
|
||||
wrapper.find('input#arguments').simulate('change', {
|
||||
target: { value: 'foo', name: 'arguments' },
|
||||
});
|
||||
wrapper.find('AnsibleSelect[name="verbosity"]').prop('onChange')({}, 1);
|
||||
});
|
||||
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(
|
||||
false
|
||||
);
|
||||
await act(async () =>
|
||||
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-4"]')
|
||||
.simulate('change', { target: { checked: true } });
|
||||
});
|
||||
|
||||
wrapper.update();
|
||||
|
||||
expect(
|
||||
wrapper.find('CheckboxListItem[label="Cred 4"]').prop('isSelected')
|
||||
).toBe(true);
|
||||
|
||||
await act(async () =>
|
||||
wrapper.find('Button[type="submit"]').prop('onClick')()
|
||||
);
|
||||
|
||||
expect(InventoriesAPI.launchAdHocCommands).toBeCalledWith(1, {
|
||||
arguments: 'foo',
|
||||
changes: false,
|
||||
credential: 4,
|
||||
escalation: false,
|
||||
extra_vars: '---',
|
||||
forks: 0,
|
||||
limit: 'I',
|
||||
module_args: 'command',
|
||||
verbosity: 1,
|
||||
});
|
||||
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find('AdHocCommandsWizard').length).toBe(0);
|
||||
});
|
||||
test('should throw error on submission properly', async () => {
|
||||
InventoriesAPI.launchAdHocCommands.mockRejectedValue(
|
||||
new Error({
|
||||
response: {
|
||||
config: {
|
||||
method: 'post',
|
||||
url: '/api/v2/inventories/1/ad_hoc_commands',
|
||||
},
|
||||
data: 'An error occurred',
|
||||
status: 403,
|
||||
},
|
||||
})
|
||||
);
|
||||
InventoriesAPI.readAdHocOptions.mockResolvedValue({
|
||||
data: {
|
||||
actions: {
|
||||
GET: {
|
||||
module_name: {
|
||||
choices: [
|
||||
['command', 'command'],
|
||||
['foo', 'foo'],
|
||||
],
|
||||
},
|
||||
verbosity: { choices: [[1], [2]] },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
CredentialTypesAPI.read.mockResolvedValue({
|
||||
data: { results: [{ id: 1 }] },
|
||||
});
|
||||
CredentialsAPI.read.mockResolvedValue({
|
||||
data: {
|
||||
results: credentials,
|
||||
count: 5,
|
||||
},
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<AdHocCommands
|
||||
apiModule={InventoriesAPI}
|
||||
adHocItems={adHocItems}
|
||||
itemId="a"
|
||||
>
|
||||
{children}
|
||||
</AdHocCommands>
|
||||
);
|
||||
});
|
||||
wrapper.find('button').prop('onClick')();
|
||||
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(true);
|
||||
expect(
|
||||
wrapper
|
||||
.find('WizardNavItem[content="Machine Credential"]')
|
||||
.prop('isDisabled')
|
||||
).toBe(true);
|
||||
|
||||
act(() => {
|
||||
wrapper.find('AnsibleSelect[name="module_args"]').prop('onChange')(
|
||||
{},
|
||||
'command'
|
||||
);
|
||||
wrapper.find('input#arguments').simulate('change', {
|
||||
target: { value: 'foo', name: 'arguments' },
|
||||
});
|
||||
wrapper.find('AnsibleSelect[name="verbosity"]').prop('onChange')({}, 1);
|
||||
});
|
||||
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(
|
||||
false
|
||||
);
|
||||
|
||||
await act(async () =>
|
||||
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-4"]')
|
||||
.simulate('change', { target: { checked: true } });
|
||||
});
|
||||
|
||||
wrapper.update();
|
||||
|
||||
expect(
|
||||
wrapper.find('CheckboxListItem[label="Cred 4"]').prop('isSelected')
|
||||
).toBe(true);
|
||||
|
||||
await act(async () =>
|
||||
wrapper.find('Button[type="submit"]').prop('onClick')()
|
||||
);
|
||||
|
||||
wrapper.update();
|
||||
|
||||
expect(wrapper.find('AdHocCommandsWizard').length).toBe(0);
|
||||
expect(wrapper.find('ErrorDetail').length).toBe(1);
|
||||
});
|
||||
test('should open alert modal when error on fetching data', async () => {
|
||||
InventoriesAPI.readAdHocOptions.mockRejectedValue(
|
||||
new Error({
|
||||
response: {
|
||||
config: {
|
||||
method: 'options',
|
||||
url: '/api/v2/inventories/1/',
|
||||
},
|
||||
data: 'An error occurred',
|
||||
status: 403,
|
||||
},
|
||||
})
|
||||
);
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<AdHocCommands
|
||||
apiModule={InventoriesAPI}
|
||||
adHocItems={adHocItems}
|
||||
itemId={1}
|
||||
>
|
||||
{children}
|
||||
</AdHocCommands>
|
||||
);
|
||||
});
|
||||
wrapper.find('button').prop('onClick')();
|
||||
wrapper.update();
|
||||
expect(wrapper.find('ErrorDetail').length).toBe(1);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,95 @@
|
||||
import React, { useState } from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { withFormik, useFormikContext } from 'formik';
|
||||
|
||||
import Wizard from '../Wizard';
|
||||
import AdHocCredentialStep from './AdHocCredentialStep';
|
||||
import DetailsStep from './DetailsStep';
|
||||
|
||||
function AdHocCommandsWizard({
|
||||
onLaunch,
|
||||
i18n,
|
||||
moduleOptions,
|
||||
verbosityOptions,
|
||||
onCloseWizard,
|
||||
credentialTypeId,
|
||||
}) {
|
||||
const [currentStepId, setCurrentStepId] = useState(1);
|
||||
const [limitTypedValue, setLimitTypedValue] = useState('');
|
||||
const [enableLaunch, setEnableLaunch] = useState(false);
|
||||
|
||||
const { values } = useFormikContext();
|
||||
|
||||
const steps = [
|
||||
{
|
||||
id: 1,
|
||||
key: 1,
|
||||
name: i18n._(t`Details`),
|
||||
component: (
|
||||
<DetailsStep
|
||||
moduleOptions={moduleOptions}
|
||||
verbosityOptions={verbosityOptions}
|
||||
onLimitChange={value => setLimitTypedValue(value)}
|
||||
limitValue={limitTypedValue}
|
||||
/>
|
||||
),
|
||||
enableNext: values.module_args && values.arguments && values.verbosity,
|
||||
nextButtonText: i18n._(t`Next`),
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
key: 2,
|
||||
name: i18n._(t`Machine Credential`),
|
||||
component: (
|
||||
<AdHocCredentialStep
|
||||
credentialTypeId={credentialTypeId}
|
||||
onEnableLaunch={() => setEnableLaunch(true)}
|
||||
/>
|
||||
),
|
||||
enableNext: enableLaunch,
|
||||
nextButtonText: i18n._(t`Launch`),
|
||||
canJumpTo: currentStepId >= 2,
|
||||
},
|
||||
];
|
||||
|
||||
const currentStep = steps.find(step => step.id === currentStepId);
|
||||
|
||||
const submit = () => {
|
||||
onLaunch(values, limitTypedValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<Wizard
|
||||
style={{ overflow: 'scroll' }}
|
||||
isOpen
|
||||
onNext={step => setCurrentStepId(step.id)}
|
||||
onClose={() => onCloseWizard()}
|
||||
onSave={submit}
|
||||
steps={steps}
|
||||
title={i18n._(t`Ad Hoc Commands`)}
|
||||
nextButtonText={currentStep.nextButtonText || undefined}
|
||||
backButtonText={i18n._(t`Back`)}
|
||||
cancelButtonText={i18n._(t`Cancel`)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const FormikApp = withFormik({
|
||||
mapPropsToValues({ adHocItems }) {
|
||||
const adHocItemStrings = adHocItems.map(item => item.name);
|
||||
return {
|
||||
limit: adHocItemStrings || [],
|
||||
credential: [],
|
||||
module_args: '',
|
||||
arguments: '',
|
||||
verbosity: '',
|
||||
forks: 0,
|
||||
changes: false,
|
||||
escalation: false,
|
||||
extra_vars: '---',
|
||||
};
|
||||
},
|
||||
})(AdHocCommandsWizard);
|
||||
|
||||
export default withI18n()(FormikApp);
|
||||
@ -0,0 +1,176 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import {
|
||||
mountWithContexts,
|
||||
waitForElement,
|
||||
} from '../../../testUtils/enzymeHelpers';
|
||||
import { CredentialsAPI } from '../../api';
|
||||
import AdHocCommandsWizard from './AdHocCommandsWizard';
|
||||
|
||||
jest.mock('../../api/models/CredentialTypes');
|
||||
jest.mock('../../api/models/Inventories');
|
||||
jest.mock('../../api/models/Credentials');
|
||||
|
||||
const adHocItems = [
|
||||
{ name: 'Inventory 1' },
|
||||
{ name: 'Inventory 2' },
|
||||
{ name: 'inventory 3' },
|
||||
];
|
||||
describe('<AdHocCommandsWizard/>', () => {
|
||||
let wrapper;
|
||||
const onLaunch = jest.fn();
|
||||
beforeEach(async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<AdHocCommandsWizard
|
||||
adHocItems={adHocItems}
|
||||
onLaunch={onLaunch}
|
||||
moduleOptions={[]}
|
||||
verbosityOptions={[]}
|
||||
onCloseWizard={() => {}}
|
||||
credentialTypeId={1}
|
||||
/>
|
||||
);
|
||||
});
|
||||
});
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('should mount properly', async () => {
|
||||
// wrapper.update();
|
||||
expect(wrapper.find('AdHocCommandsWizard').length).toBe(1);
|
||||
});
|
||||
|
||||
test('next and nav item should be disabled', async () => {
|
||||
// wrapper.update();
|
||||
await waitForElement(wrapper, 'WizardNavItem', el => el.length > 0);
|
||||
|
||||
expect(
|
||||
wrapper.find('WizardNavItem[content="Details"]').prop('isCurrent')
|
||||
).toBe(true);
|
||||
expect(
|
||||
wrapper.find('WizardNavItem[content="Details"]').prop('isDisabled')
|
||||
).toBe(false);
|
||||
expect(
|
||||
wrapper
|
||||
.find('WizardNavItem[content="Machine Credential"]')
|
||||
.prop('isDisabled')
|
||||
).toBe(true);
|
||||
expect(
|
||||
wrapper
|
||||
.find('WizardNavItem[content="Machine Credential"]')
|
||||
.prop('isCurrent')
|
||||
).toBe(false);
|
||||
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(true);
|
||||
});
|
||||
|
||||
test('next button should become active, and should navigate to the next step', async () => {
|
||||
await waitForElement(wrapper, 'WizardNavItem', el => el.length > 0);
|
||||
|
||||
act(() => {
|
||||
wrapper.find('AnsibleSelect[name="module_args"]').prop('onChange')(
|
||||
{},
|
||||
'command'
|
||||
);
|
||||
wrapper.find('input#arguments').simulate('change', {
|
||||
target: { value: 'foo', name: 'arguments' },
|
||||
});
|
||||
wrapper.find('AnsibleSelect[name="verbosity"]').prop('onChange')({}, 1);
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(
|
||||
false
|
||||
);
|
||||
wrapper.find('Button[type="submit"]').prop('onClick')();
|
||||
wrapper.update();
|
||||
});
|
||||
test('launch button should become active', async () => {
|
||||
CredentialsAPI.read.mockResolvedValue({
|
||||
data: {
|
||||
results: [
|
||||
{ id: 1, name: 'Cred 1' },
|
||||
{ id: 2, name: 'Cred2' },
|
||||
],
|
||||
count: 2,
|
||||
},
|
||||
});
|
||||
await waitForElement(wrapper, 'WizardNavItem', el => el.length > 0);
|
||||
|
||||
act(() => {
|
||||
wrapper.find('AnsibleSelect[name="module_args"]').prop('onChange')(
|
||||
{},
|
||||
'command'
|
||||
);
|
||||
wrapper.find('input#arguments').simulate('change', {
|
||||
target: { value: 'foo', name: 'arguments' },
|
||||
});
|
||||
wrapper.find('AnsibleSelect[name="verbosity"]').prop('onChange')({}, 1);
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(
|
||||
false
|
||||
);
|
||||
wrapper.find('Button[type="submit"]').prop('onClick')();
|
||||
|
||||
wrapper.update();
|
||||
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"]')
|
||||
.simulate('change', { target: { checked: true } });
|
||||
});
|
||||
|
||||
wrapper.update();
|
||||
|
||||
expect(
|
||||
wrapper.find('CheckboxListItem[label="Cred 1"]').prop('isSelected')
|
||||
).toBe(true);
|
||||
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(
|
||||
false
|
||||
);
|
||||
wrapper.find('Button[type="submit"]').prop('onClick')();
|
||||
expect(onLaunch).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('expect credential step to throw error', async () => {
|
||||
CredentialsAPI.read.mockRejectedValue(
|
||||
new Error({
|
||||
response: {
|
||||
config: {
|
||||
method: 'get',
|
||||
url: '/api/v2/credentals',
|
||||
},
|
||||
data: 'An error occurred',
|
||||
status: 403,
|
||||
},
|
||||
})
|
||||
);
|
||||
await waitForElement(wrapper, 'WizardNavItem', el => el.length > 0);
|
||||
|
||||
act(() => {
|
||||
wrapper.find('AnsibleSelect[name="module_args"]').prop('onChange')(
|
||||
{},
|
||||
'command'
|
||||
);
|
||||
wrapper.find('input#arguments').simulate('change', {
|
||||
target: { value: 'foo', name: 'arguments' },
|
||||
});
|
||||
wrapper.find('AnsibleSelect[name="verbosity"]').prop('onChange')({}, 1);
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(
|
||||
false
|
||||
);
|
||||
wrapper.find('Button[type="submit"]').prop('onClick')();
|
||||
|
||||
wrapper.update();
|
||||
expect(wrapper.find('ContentLoading').length).toBe(1);
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
expect(wrapper.find('ContentError').length).toBe(1);
|
||||
});
|
||||
});
|
||||
114
awx/ui_next/src/components/AdHocCommands/AdHocCredentialStep.jsx
Normal file
114
awx/ui_next/src/components/AdHocCommands/AdHocCredentialStep.jsx
Normal file
@ -0,0 +1,114 @@
|
||||
import React, { useEffect, useCallback } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { useField } from 'formik';
|
||||
import { Form, FormGroup } from '@patternfly/react-core';
|
||||
import { CredentialsAPI } from '../../api';
|
||||
|
||||
import { getQSConfig, parseQueryString, mergeParams } from '../../util/qs';
|
||||
import useRequest from '../../util/useRequest';
|
||||
import ContentError from '../ContentError';
|
||||
import ContentLoading from '../ContentLoading';
|
||||
import { required } from '../../util/validators';
|
||||
import OptionsList from '../OptionsList';
|
||||
|
||||
const QS_CONFIG = getQSConfig('credentials', {
|
||||
page: 1,
|
||||
page_size: 5,
|
||||
order_by: 'name',
|
||||
});
|
||||
|
||||
function AdHocCredentialStep({ i18n, credentialTypeId, onEnableLaunch }) {
|
||||
const history = useHistory();
|
||||
const {
|
||||
error,
|
||||
isLoading,
|
||||
request: fetchCredentials,
|
||||
result: { credentials, credentialCount },
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
const params = parseQueryString(QS_CONFIG, history.location.search);
|
||||
|
||||
const {
|
||||
data: { results, count },
|
||||
} = await CredentialsAPI.read(
|
||||
mergeParams(params, { credential_type: credentialTypeId })
|
||||
);
|
||||
|
||||
return {
|
||||
credentials: results,
|
||||
credentialCount: count,
|
||||
};
|
||||
}, [credentialTypeId, history.location.search]),
|
||||
{ credentials: [], credentialCount: 0 }
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetchCredentials();
|
||||
}, [fetchCredentials]);
|
||||
|
||||
const [credentialField, credentialMeta, credentialHelpers] = useField({
|
||||
name: 'credential',
|
||||
validate: required(null, i18n),
|
||||
});
|
||||
if (error) {
|
||||
return <ContentError error={error} />;
|
||||
}
|
||||
if (isLoading) {
|
||||
return <ContentLoading error={error} />;
|
||||
}
|
||||
return (
|
||||
<Form>
|
||||
<FormGroup
|
||||
fieldId="credential"
|
||||
label={i18n._(t`Machine Credential`)}
|
||||
isRequired
|
||||
validated={
|
||||
!credentialMeta.touched || !credentialMeta.error ? 'default' : 'error'
|
||||
}
|
||||
helperTextInvalid={credentialMeta.error}
|
||||
>
|
||||
<OptionsList
|
||||
value={credentialField.value || []}
|
||||
options={credentials}
|
||||
optionCount={credentialCount}
|
||||
header={i18n._(t`Machine Credential`)}
|
||||
readOnly
|
||||
qsConfig={QS_CONFIG}
|
||||
searchColumns={[
|
||||
{
|
||||
name: i18n._(t`Name`),
|
||||
key: 'name',
|
||||
isDefault: true,
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Created By (Username)`),
|
||||
key: 'created_by__username',
|
||||
},
|
||||
{
|
||||
name: i18n._(t`Modified By (Username)`),
|
||||
key: 'modified_by__username',
|
||||
},
|
||||
]}
|
||||
sortColumns={[
|
||||
{
|
||||
name: i18n._(t`Name`),
|
||||
key: 'name',
|
||||
},
|
||||
]}
|
||||
name="credential"
|
||||
selectItem={value => {
|
||||
credentialHelpers.setValue([value]);
|
||||
onEnableLaunch();
|
||||
}}
|
||||
deselectItem={() => {
|
||||
credentialHelpers.setValue([]);
|
||||
}}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default withI18n()(AdHocCredentialStep);
|
||||
@ -0,0 +1,51 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { Formik } from 'formik';
|
||||
import {
|
||||
mountWithContexts,
|
||||
waitForElement,
|
||||
} from '../../../testUtils/enzymeHelpers';
|
||||
import { CredentialsAPI } from '../../api';
|
||||
import AdHocCredentialStep from './AdHocCredentialStep';
|
||||
|
||||
jest.mock('../../api/models/Credentials');
|
||||
|
||||
describe('<AdHocCredentialStep />', () => {
|
||||
const onEnableLaunch = jest.fn();
|
||||
let wrapper;
|
||||
beforeEach(async () => {
|
||||
CredentialsAPI.read.mockResolvedValue({
|
||||
data: {
|
||||
results: [
|
||||
{ id: 1, name: 'Cred 1', url: 'wwww.google.com' },
|
||||
{ id: 2, name: 'Cred2', url: 'wwww.google.com' },
|
||||
],
|
||||
count: 2,
|
||||
},
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<Formik>
|
||||
<AdHocCredentialStep
|
||||
credentialTypeId={1}
|
||||
onEnableLaunch={onEnableLaunch}
|
||||
/>
|
||||
</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(CredentialsAPI.read).toHaveBeenCalled();
|
||||
expect(wrapper.find('CheckboxListItem').length).toBe(2);
|
||||
});
|
||||
});
|
||||
300
awx/ui_next/src/components/AdHocCommands/DetailsStep.jsx
Normal file
300
awx/ui_next/src/components/AdHocCommands/DetailsStep.jsx
Normal file
@ -0,0 +1,300 @@
|
||||
/* eslint-disable react/no-unescaped-entities */
|
||||
import React from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
|
||||
import { useField } from 'formik';
|
||||
import {
|
||||
Form,
|
||||
FormGroup,
|
||||
InputGroup,
|
||||
TextInput,
|
||||
Label,
|
||||
InputGroupText,
|
||||
Switch,
|
||||
Checkbox,
|
||||
} from '@patternfly/react-core';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import AnsibleSelect from '../AnsibleSelect';
|
||||
import FormField, { FieldTooltip } from '../FormField';
|
||||
import { VariablesField } from '../CodeMirrorInput';
|
||||
import {
|
||||
FormColumnLayout,
|
||||
FormFullWidthLayout,
|
||||
FormCheckboxLayout,
|
||||
} from '../FormLayout';
|
||||
import { required } from '../../util/validators';
|
||||
|
||||
const TooltipWrapper = styled.div`
|
||||
text-align: left;
|
||||
`;
|
||||
|
||||
function CredentialStep({
|
||||
i18n,
|
||||
verbosityOptions,
|
||||
moduleOptions,
|
||||
onLimitChange,
|
||||
limitValue,
|
||||
}) {
|
||||
const [moduleField, moduleMeta, moduleHelpers] = useField({
|
||||
name: 'module_args',
|
||||
validate: required(null, i18n),
|
||||
});
|
||||
const [limitField, , limitHelpers] = useField('limit');
|
||||
const [variablesField] = useField('extra_vars');
|
||||
const [changesField, , changesHelpers] = useField('changes');
|
||||
const [escalationField, , escalationHelpers] = useField('escalation');
|
||||
const [verbosityField, verbosityMeta, verbosityHelpers] = useField({
|
||||
name: 'verbosity',
|
||||
validate: required(null, i18n),
|
||||
});
|
||||
|
||||
return (
|
||||
<Form>
|
||||
<FormColumnLayout>
|
||||
<FormFullWidthLayout>
|
||||
<FormGroup
|
||||
fieldId="module"
|
||||
label={i18n._(t`Module`)}
|
||||
isRequired
|
||||
helperTextInvalid={moduleMeta.error}
|
||||
validated={
|
||||
!moduleMeta.touched || !moduleMeta.error ? 'default' : 'error'
|
||||
}
|
||||
labelIcon={
|
||||
<FieldTooltip
|
||||
content={i18n._(
|
||||
t`These are the modules that AWX supports running commands against.`
|
||||
)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<AnsibleSelect
|
||||
{...moduleField}
|
||||
isValid={!moduleMeta.touched || !moduleMeta.error}
|
||||
id="module"
|
||||
data={moduleOptions || []}
|
||||
onChange={(event, value) => {
|
||||
moduleHelpers.setValue(value);
|
||||
}}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormField
|
||||
id="arguments"
|
||||
name="arguments"
|
||||
type="text"
|
||||
label={i18n._(t`Arguments`)}
|
||||
isRequired
|
||||
tooltip={i18n._(
|
||||
t`These arguments are used with the specified module.`
|
||||
)}
|
||||
/>
|
||||
<FormGroup
|
||||
fieldId="verbosity"
|
||||
label={i18n._(t`Verbosity`)}
|
||||
isRequired
|
||||
validated={
|
||||
!verbosityMeta.touched || !verbosityMeta.error
|
||||
? 'default'
|
||||
: 'error'
|
||||
}
|
||||
helperTextInvalid={verbosityMeta.error}
|
||||
labelIcon={
|
||||
<FieldTooltip
|
||||
content={i18n._(
|
||||
t`These are the verbosity levels for standard out of the command run that are supported.`
|
||||
)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<AnsibleSelect
|
||||
{...verbosityField}
|
||||
isValid={!verbosityMeta.touched || !verbosityMeta.error}
|
||||
id="verbosity"
|
||||
data={verbosityOptions || []}
|
||||
onChange={(event, value) => {
|
||||
verbosityHelpers.setValue(value);
|
||||
}}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
label={i18n._(t`Limit`)}
|
||||
labelIcon={
|
||||
<FieldTooltip
|
||||
content={
|
||||
<span>
|
||||
{i18n._(
|
||||
t`The pattern used to target hosts in the inventory. Leaving the field blank, all, and * will all target all hosts in the inventory. You can find more information about Ansible's host patterns`
|
||||
)}{' '}
|
||||
<a
|
||||
href="https://docs.ansible.com/ansible/latest/user_guide/intro_patterns.html"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{i18n._(`here`)}
|
||||
</a>
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<InputGroup>
|
||||
<InputGroupText>
|
||||
{limitField.value.map((item, index) => (
|
||||
<Label
|
||||
onClose={() => {
|
||||
limitField.value.splice(index, 1);
|
||||
limitHelpers.setValue(limitField.value);
|
||||
}}
|
||||
>
|
||||
{item}
|
||||
</Label>
|
||||
))}
|
||||
</InputGroupText>
|
||||
<TextInput
|
||||
id="limit"
|
||||
name="limit"
|
||||
type="text"
|
||||
label={i18n._(t`Limit`)}
|
||||
value={limitValue}
|
||||
isRequired
|
||||
onChange={value => {
|
||||
onLimitChange(value);
|
||||
}}
|
||||
/>
|
||||
</InputGroup>
|
||||
</FormGroup>
|
||||
<FormField
|
||||
id="template-forks"
|
||||
name="forks"
|
||||
type="number"
|
||||
min="0"
|
||||
label={i18n._(t`Forks`)}
|
||||
tooltip={
|
||||
<span>
|
||||
{i18n._(
|
||||
t`The number of parallel or simultaneous processes to use while executing the playbook. Inputting no value will use the default value from the `
|
||||
)}{' '}
|
||||
<a
|
||||
href="https://docs.ansible.com/ansible/latest/installation_guide/intro_configuration.html#the-ansible-configuration-file"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{i18n._(t`ansible configuration file.`)}
|
||||
</a>
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<FormColumnLayout>
|
||||
<FormGroup
|
||||
label={i18n._(t`Show changes`)}
|
||||
labelIcon={
|
||||
<FieldTooltip
|
||||
content={i18n._(
|
||||
t`If enabled, show the changes made by Ansible tasks, where supported. This is equivalent to Ansible’s --diff mode.`
|
||||
)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Switch
|
||||
css="display: inline-flex;"
|
||||
id="changes"
|
||||
label={i18n._(t`On`)}
|
||||
labelOff={i18n._(t`Off`)}
|
||||
isChecked={changesField.value}
|
||||
onChange={() => {
|
||||
changesHelpers.setValue(!changesField.value);
|
||||
}}
|
||||
aria-label={i18n._(t`toggle changes`)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
name={i18n._(t`enable privilege escalation`)}
|
||||
fieldId="escalation"
|
||||
>
|
||||
<FormCheckboxLayout>
|
||||
<Checkbox
|
||||
aria-label={i18n._(t`Enable privilege escalation`)}
|
||||
label={
|
||||
<span>
|
||||
{i18n._(t`Enable privilege escalation`)}
|
||||
|
||||
<FieldTooltip
|
||||
content={
|
||||
<p>
|
||||
{i18n._(t`Enables creation of a provisioning
|
||||
callback URL. Using the URL a host can contact BRAND_NAME
|
||||
and request a configuration update using this job
|
||||
template`)}
|
||||
|
||||
<code>--{i18n._(t`become`)} </code>
|
||||
{i18n._(t`option to the`)}
|
||||
<code>{i18n._(t`ansible`)} </code>
|
||||
{i18n._(t`command`)}
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
id="escalation"
|
||||
isChecked={escalationField.value}
|
||||
onChange={checked => {
|
||||
escalationHelpers.setValue(checked);
|
||||
}}
|
||||
/>
|
||||
</FormCheckboxLayout>
|
||||
</FormGroup>
|
||||
</FormColumnLayout>
|
||||
|
||||
<VariablesField
|
||||
css="margin: 20px 0"
|
||||
id="extra_vars"
|
||||
name="extra_vars"
|
||||
value={JSON.stringify(variablesField.value)}
|
||||
rows={4}
|
||||
labelIcon
|
||||
tooltip={
|
||||
<TooltipWrapper>
|
||||
<p>
|
||||
{i18n._(
|
||||
t`Pass extra command line changes. There are two ansible command line parameters: `
|
||||
)}
|
||||
<br />
|
||||
<code>-e</code>, <code>--extra-vars </code>
|
||||
<br />
|
||||
{i18n._(t`Provide key/value pairs using either
|
||||
YAML or JSON.`)}
|
||||
</p>
|
||||
JSON:
|
||||
<br />
|
||||
<code>
|
||||
<pre>
|
||||
{'{'}
|
||||
{'\n '}"somevar": "somevalue",
|
||||
{'\n '}"password": "magic"
|
||||
{'\n'}
|
||||
{'}'}
|
||||
</pre>
|
||||
</code>
|
||||
YAML:
|
||||
<br />
|
||||
<code>
|
||||
<pre>
|
||||
---
|
||||
{'\n'}somevar: somevalue
|
||||
{'\n'}password: magic
|
||||
</pre>
|
||||
</code>
|
||||
</TooltipWrapper>
|
||||
}
|
||||
label={i18n._(t`Extra variables`)}
|
||||
/>
|
||||
</FormFullWidthLayout>
|
||||
</FormColumnLayout>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default withI18n()(CredentialStep);
|
||||
150
awx/ui_next/src/components/AdHocCommands/DetailsStep.test.jsx
Normal file
150
awx/ui_next/src/components/AdHocCommands/DetailsStep.test.jsx
Normal file
@ -0,0 +1,150 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { Formik } from 'formik';
|
||||
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
||||
import DetailsStep from './DetailsStep';
|
||||
|
||||
jest.mock('../../api/models/Credentials');
|
||||
|
||||
const verbosityOptions = [
|
||||
{ key: -1, value: '', label: '', isDisabled: false },
|
||||
{ key: 0, value: 0, label: '0', isDisabled: false },
|
||||
{ key: 1, value: 1, label: '1', isDisabled: false },
|
||||
];
|
||||
const moduleOptions = [
|
||||
{ key: -1, value: '', label: '', isDisabled: false },
|
||||
{ key: 0, value: 'command', label: 'command', isDisabled: false },
|
||||
{ key: 1, value: 'shell', label: 'shell', isDisabled: false },
|
||||
];
|
||||
const onLimitChange = jest.fn();
|
||||
const limitValue = '';
|
||||
const initialValues = {
|
||||
limit: ['Inventory 1', 'inventory 2'],
|
||||
credential: [],
|
||||
module_args: '',
|
||||
arguments: '',
|
||||
verbosity: '',
|
||||
forks: 0,
|
||||
changes: false,
|
||||
escalation: false,
|
||||
extra_vars: '---',
|
||||
};
|
||||
|
||||
describe('<DetailsStep />', () => {
|
||||
let wrapper;
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
test('should mount properly', async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<Formik initialValues={initialValues}>
|
||||
<DetailsStep
|
||||
verbosityOptions={verbosityOptions}
|
||||
moduleOptions={moduleOptions}
|
||||
onLimitChange={onLimitChange}
|
||||
limitValue={limitValue}
|
||||
/>
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('should show all the fields', async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<Formik initialValues={initialValues}>
|
||||
<DetailsStep
|
||||
verbosityOptions={verbosityOptions}
|
||||
moduleOptions={moduleOptions}
|
||||
onLimitChange={onLimitChange}
|
||||
limitValue={limitValue}
|
||||
/>
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
expect(wrapper.find('FormGroup[label="Module"]').length).toBe(1);
|
||||
expect(wrapper.find('FormField[name="arguments"]').length).toBe(1);
|
||||
expect(wrapper.find('FormGroup[label="Verbosity"]').length).toBe(1);
|
||||
expect(wrapper.find('FormGroup[label="Limit"]').length).toBe(1);
|
||||
expect(wrapper.find('FormField[name="forks"]').length).toBe(1);
|
||||
expect(wrapper.find('FormGroup[label="Show changes"]').length).toBe(1);
|
||||
expect(
|
||||
wrapper.find('FormGroup[name="enable privilege escalation"]').length
|
||||
).toBe(1);
|
||||
expect(wrapper.find('VariablesField').length).toBe(1);
|
||||
});
|
||||
|
||||
test('shold update form values', async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<Formik initialValues={initialValues}>
|
||||
<DetailsStep
|
||||
verbosityOptions={verbosityOptions}
|
||||
moduleOptions={moduleOptions}
|
||||
onLimitChange={onLimitChange}
|
||||
limitValue={limitValue}
|
||||
/>
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
wrapper.find('AnsibleSelect[name="module_args"]').prop('onChange')(
|
||||
{},
|
||||
'command'
|
||||
);
|
||||
wrapper.find('input#arguments').simulate('change', {
|
||||
target: { value: 'foo', name: 'arguments' },
|
||||
});
|
||||
wrapper.find('AnsibleSelect[name="verbosity"]').prop('onChange')({}, 1);
|
||||
|
||||
wrapper.find('TextInputBase[name="forks"]').simulate('change', {
|
||||
target: { value: 10, name: 'forks' },
|
||||
});
|
||||
wrapper.find('Switch').invoke('onChange')();
|
||||
wrapper
|
||||
.find('Checkbox[aria-label="Enable privilege escalation"]')
|
||||
.invoke('onChange')(true, {
|
||||
currentTarget: { value: true, type: 'change', checked: true },
|
||||
});
|
||||
});
|
||||
wrapper.update();
|
||||
expect(
|
||||
wrapper.find('AnsibleSelect[name="module_args"]').prop('value')
|
||||
).toBe('command');
|
||||
expect(wrapper.find('input#arguments').prop('value')).toBe('foo');
|
||||
expect(wrapper.find('AnsibleSelect[name="verbosity"]').prop('value')).toBe(
|
||||
1
|
||||
);
|
||||
expect(wrapper.find('TextInputBase[name="forks"]').prop('value')).toBe(10);
|
||||
expect(wrapper.find('TextInputBase[label="Limit"]').prop('value')).toBe('');
|
||||
expect(wrapper.find('Switch').prop('isChecked')).toBe(true);
|
||||
expect(
|
||||
wrapper
|
||||
.find('Checkbox[aria-label="Enable privilege escalation"]')
|
||||
.prop('isChecked')
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('should mount with proper limit value', async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<Formik initialValues={initialValues}>
|
||||
<DetailsStep
|
||||
verbosityOptions={verbosityOptions}
|
||||
moduleOptions={moduleOptions}
|
||||
onLimitChange={onLimitChange}
|
||||
limitValue="foo value"
|
||||
/>
|
||||
</Formik>
|
||||
);
|
||||
});
|
||||
expect(wrapper.find('TextInputBase[label="Limit"]').prop('value')).toBe(
|
||||
'foo value'
|
||||
);
|
||||
});
|
||||
});
|
||||
1
awx/ui_next/src/components/AdHocCommands/index.js
Normal file
1
awx/ui_next/src/components/AdHocCommands/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './AdHocCommands';
|
||||
@ -13,8 +13,10 @@ import DataListToolbar from '../../../components/DataListToolbar';
|
||||
import PaginatedDataList, {
|
||||
ToolbarAddButton,
|
||||
} from '../../../components/PaginatedDataList';
|
||||
|
||||
import InventoryGroupItem from './InventoryGroupItem';
|
||||
import InventoryGroupsDeleteModal from '../shared/InventoryGroupsDeleteModal';
|
||||
import AdHocCommandsButton from '../../../components/AdHocCommands/AdHocCommands';
|
||||
|
||||
const QS_CONFIG = getQSConfig('group', {
|
||||
page: 1,
|
||||
@ -225,6 +227,34 @@ function InventoryGroupsList({ i18n }) {
|
||||
</Button>
|
||||
</div>
|
||||
</Tooltip>,
|
||||
[
|
||||
<Tooltip
|
||||
content={i18n._(
|
||||
t`Select an inventory source by clicking the check box beside it. The inventory source can be a single group or a selection of multiple groups.`
|
||||
)}
|
||||
position="top"
|
||||
key="adhoc"
|
||||
>
|
||||
<AdHocCommandsButton
|
||||
adHocItems={selected}
|
||||
apiModule={InventoriesAPI}
|
||||
itemId={inventoryId}
|
||||
>
|
||||
{({ openAdHocCommands }) => (
|
||||
<Button
|
||||
variant="secondary"
|
||||
aria-label={i18n._(t`Run command`)}
|
||||
onClick={openAdHocCommands}
|
||||
isDisabled={
|
||||
selected.length === 0 || selected.some(cannotDelete)
|
||||
}
|
||||
>
|
||||
{i18n._(t`Run command`)}
|
||||
</Button>
|
||||
)}
|
||||
</AdHocCommandsButton>
|
||||
</Tooltip>,
|
||||
],
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user