mirror of
https://github.com/ansible/awx.git
synced 2026-04-14 06:29:25 -02:30
Adds Proptypes and updates tooltips to make them more translatable
This commit is contained in:
@@ -2,23 +2,30 @@ import React, { useState, Fragment, useCallback, useEffect } from 'react';
|
|||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import useRequest, { useDismissableError } from '../../util/useRequest';
|
import useRequest, { useDismissableError } from '../../util/useRequest';
|
||||||
import AlertModal from '../AlertModal';
|
import AlertModal from '../AlertModal';
|
||||||
import { CredentialTypesAPI } from '../../api';
|
import { CredentialTypesAPI } from '../../api';
|
||||||
import ErrorDetail from '../ErrorDetail';
|
import ErrorDetail from '../ErrorDetail';
|
||||||
import AdHocCommandsForm from './AdHocCommandsWizard';
|
import AdHocCommandsWizard from './AdHocCommandsWizard';
|
||||||
import ContentLoading from '../ContentLoading';
|
import ContentLoading from '../ContentLoading';
|
||||||
import ContentError from '../ContentError';
|
import ContentError from '../ContentError';
|
||||||
|
|
||||||
function AdHocCommands({ children, apiModule, adHocItems, itemId, i18n }) {
|
function AdHocCommands({ children, apiModule, adHocItems, itemId, i18n }) {
|
||||||
const [isWizardOpen, setIsWizardOpen] = useState(false);
|
const [isWizardOpen, setIsWizardOpen] = useState(false);
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
const verbosityOptions = [
|
||||||
|
{ value: '0', key: '0', label: i18n._(t`0 (Normal)`) },
|
||||||
|
{ value: '1', key: '1', label: i18n._(t`1 (Verbose)`) },
|
||||||
|
{ value: '2', key: '2', label: i18n._(t`2 (More Verbose)`) },
|
||||||
|
{ value: '3', key: '3', label: i18n._(t`3 (Debug)`) },
|
||||||
|
{ value: '4', key: '4', label: i18n._(t`4 (Connection Debug)`) },
|
||||||
|
];
|
||||||
const {
|
const {
|
||||||
error: fetchError,
|
error: fetchError,
|
||||||
request: fetchModuleOptions,
|
request: fetchModuleOptions,
|
||||||
result: { moduleOptions, verbosityOptions, credentialTypeId },
|
result: { moduleOptions, credentialTypeId },
|
||||||
} = useRequest(
|
} = useRequest(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
const [choices, credId] = await Promise.all([
|
const [choices, credId] = await Promise.all([
|
||||||
@@ -38,13 +45,8 @@ function AdHocCommands({ children, apiModule, adHocItems, itemId, i18n }) {
|
|||||||
(choice, index) => itemObject(choice[0], index)
|
(choice, index) => itemObject(choice[0], index)
|
||||||
);
|
);
|
||||||
|
|
||||||
const verbosityItems = choices.data.actions.GET.verbosity.choices.map(
|
|
||||||
(choice, index) => itemObject(choice[0], index)
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
moduleOptions: [itemObject('', -1), ...options],
|
moduleOptions: [itemObject('', -1), ...options],
|
||||||
verbosityOptions: [itemObject('', -1), ...verbosityItems],
|
|
||||||
credentialTypeId: credId.data.results[0].id,
|
credentialTypeId: credId.data.results[0].id,
|
||||||
};
|
};
|
||||||
}, [itemId, apiModule]),
|
}, [itemId, apiModule]),
|
||||||
@@ -65,6 +67,7 @@ function AdHocCommands({ children, apiModule, adHocItems, itemId, i18n }) {
|
|||||||
const { data } = await apiModule.launchAdHocCommands(itemId, values);
|
const { data } = await apiModule.launchAdHocCommands(itemId, values);
|
||||||
history.push(`/jobs/${data.module_name}/${data.id}/output`);
|
history.push(`/jobs/${data.module_name}/${data.id}/output`);
|
||||||
},
|
},
|
||||||
|
|
||||||
[apiModule, itemId, history]
|
[apiModule, itemId, history]
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -73,16 +76,11 @@ function AdHocCommands({ children, apiModule, adHocItems, itemId, i18n }) {
|
|||||||
launchError || fetchError
|
launchError || fetchError
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSubmit = async (values, limitTypedValue) => {
|
const handleSubmit = async values => {
|
||||||
const { credential, limit, ...remainingValues } = values;
|
const { credential, ...remainingValues } = values;
|
||||||
const newCredential = credential[0].id;
|
const newCredential = credential[0].id;
|
||||||
if (limitTypedValue) {
|
|
||||||
values.limit = limit.concat(limitTypedValue);
|
|
||||||
}
|
|
||||||
const stringifyLimit = values.limit.join(', ').trim();
|
|
||||||
|
|
||||||
const manipulatedValues = {
|
const manipulatedValues = {
|
||||||
limit: stringifyLimit[0],
|
|
||||||
credential: newCredential,
|
credential: newCredential,
|
||||||
...remainingValues,
|
...remainingValues,
|
||||||
};
|
};
|
||||||
@@ -105,7 +103,14 @@ function AdHocCommands({ children, apiModule, adHocItems, itemId, i18n }) {
|
|||||||
setIsWizardOpen(false);
|
setIsWizardOpen(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ContentError error={error} />
|
{launchError ? (
|
||||||
|
<>
|
||||||
|
{i18n._(t`Failed to launch job.`)}
|
||||||
|
<ErrorDetail error={error} />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<ContentError error={error} />
|
||||||
|
)}
|
||||||
</AlertModal>
|
</AlertModal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -116,30 +121,24 @@ function AdHocCommands({ children, apiModule, adHocItems, itemId, i18n }) {
|
|||||||
})}
|
})}
|
||||||
|
|
||||||
{isWizardOpen && (
|
{isWizardOpen && (
|
||||||
<AdHocCommandsForm
|
<AdHocCommandsWizard
|
||||||
adHocItems={adHocItems}
|
adHocItems={adHocItems}
|
||||||
moduleOptions={moduleOptions}
|
moduleOptions={moduleOptions}
|
||||||
verbosityOptions={verbosityOptions}
|
verbosityOptions={verbosityOptions}
|
||||||
credentialTypeId={credentialTypeId}
|
credentialTypeId={credentialTypeId}
|
||||||
onCloseWizard={() => setIsWizardOpen(false)}
|
onCloseWizard={() => setIsWizardOpen(false)}
|
||||||
onLaunch={handleSubmit}
|
onLaunch={handleSubmit}
|
||||||
error={error}
|
|
||||||
onDismissError={() => dismissError()}
|
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>
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AdHocCommands.propTypes = {
|
||||||
|
children: PropTypes.func.isRequired,
|
||||||
|
adHocItems: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
itemId: PropTypes.number.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
export default withI18n()(AdHocCommands);
|
export default withI18n()(AdHocCommands);
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const credentials = [
|
|||||||
];
|
];
|
||||||
const adHocItems = [
|
const adHocItems = [
|
||||||
{
|
{
|
||||||
name: ' Inventory 1 Org 0',
|
name: 'Inventory 1 Org 0',
|
||||||
},
|
},
|
||||||
{ name: 'Inventory 2 Org 0' },
|
{ name: 'Inventory 2 Org 0' },
|
||||||
];
|
];
|
||||||
@@ -43,6 +43,7 @@ describe('<AdHocCOmmands />', () => {
|
|||||||
apiModule={InventoriesAPI}
|
apiModule={InventoriesAPI}
|
||||||
adHocItems={adHocItems}
|
adHocItems={adHocItems}
|
||||||
itemId={1}
|
itemId={1}
|
||||||
|
credentialTypeId={1}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</AdHocCommands>
|
</AdHocCommands>
|
||||||
@@ -57,6 +58,7 @@ describe('<AdHocCOmmands />', () => {
|
|||||||
apiModule={InventoriesAPI}
|
apiModule={InventoriesAPI}
|
||||||
adHocItems={adHocItems}
|
adHocItems={adHocItems}
|
||||||
itemId={1}
|
itemId={1}
|
||||||
|
credentialTypeId={1}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</AdHocCommands>
|
</AdHocCommands>
|
||||||
@@ -91,12 +93,13 @@ describe('<AdHocCOmmands />', () => {
|
|||||||
apiModule={InventoriesAPI}
|
apiModule={InventoriesAPI}
|
||||||
adHocItems={adHocItems}
|
adHocItems={adHocItems}
|
||||||
itemId={1}
|
itemId={1}
|
||||||
|
credentialTypeId={1}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</AdHocCommands>
|
</AdHocCommands>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
wrapper.find('button').prop('onClick')();
|
await act(async () => wrapper.find('button').prop('onClick')());
|
||||||
|
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
|
|
||||||
@@ -128,29 +131,32 @@ describe('<AdHocCOmmands />', () => {
|
|||||||
count: 5,
|
count: 5,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
InventoriesAPI.launchAdHocCommands.mockResolvedValue({ data: { id: 1 } });
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(
|
wrapper = mountWithContexts(
|
||||||
<AdHocCommands
|
<AdHocCommands
|
||||||
apiModule={InventoriesAPI}
|
apiModule={InventoriesAPI}
|
||||||
adHocItems={adHocItems}
|
adHocItems={adHocItems}
|
||||||
itemId={1}
|
itemId={1}
|
||||||
|
credentialTypeId={1}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</AdHocCommands>
|
</AdHocCommands>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
wrapper.find('button').prop('onClick')();
|
await act(async () => wrapper.find('button').prop('onClick')());
|
||||||
|
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
|
|
||||||
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(true);
|
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(true);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
wrapper
|
wrapper
|
||||||
.find('WizardNavItem[content="Machine Credential"]')
|
.find('WizardNavItem[content="Machine credential"]')
|
||||||
.prop('isDisabled')
|
.prop('isDisabled')
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
|
|
||||||
act(() => {
|
await act(async () => {
|
||||||
wrapper.find('AnsibleSelect[name="module_args"]').prop('onChange')(
|
wrapper.find('AnsibleSelect[name="module_args"]').prop('onChange')(
|
||||||
{},
|
{},
|
||||||
'command'
|
'command'
|
||||||
@@ -194,7 +200,7 @@ describe('<AdHocCOmmands />', () => {
|
|||||||
escalation: false,
|
escalation: false,
|
||||||
extra_vars: '---',
|
extra_vars: '---',
|
||||||
forks: 0,
|
forks: 0,
|
||||||
limit: 'I',
|
limit: 'Inventory 1 Org 0, Inventory 2 Org 0',
|
||||||
module_args: 'command',
|
module_args: 'command',
|
||||||
verbosity: 1,
|
verbosity: 1,
|
||||||
});
|
});
|
||||||
@@ -203,6 +209,7 @@ describe('<AdHocCOmmands />', () => {
|
|||||||
|
|
||||||
expect(wrapper.find('AdHocCommandsWizard').length).toBe(0);
|
expect(wrapper.find('AdHocCommandsWizard').length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should throw error on submission properly', async () => {
|
test('should throw error on submission properly', async () => {
|
||||||
InventoriesAPI.launchAdHocCommands.mockRejectedValue(
|
InventoriesAPI.launchAdHocCommands.mockRejectedValue(
|
||||||
new Error({
|
new Error({
|
||||||
@@ -245,24 +252,25 @@ describe('<AdHocCOmmands />', () => {
|
|||||||
<AdHocCommands
|
<AdHocCommands
|
||||||
apiModule={InventoriesAPI}
|
apiModule={InventoriesAPI}
|
||||||
adHocItems={adHocItems}
|
adHocItems={adHocItems}
|
||||||
itemId="a"
|
itemId={1}
|
||||||
|
credentialTypeId={1}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</AdHocCommands>
|
</AdHocCommands>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
wrapper.find('button').prop('onClick')();
|
await act(async () => wrapper.find('button').prop('onClick')());
|
||||||
|
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
|
|
||||||
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(true);
|
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(true);
|
||||||
expect(
|
expect(
|
||||||
wrapper
|
wrapper
|
||||||
.find('WizardNavItem[content="Machine Credential"]')
|
.find('WizardNavItem[content="Machine credential"]')
|
||||||
.prop('isDisabled')
|
.prop('isDisabled')
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
|
|
||||||
act(() => {
|
await act(async () => {
|
||||||
wrapper.find('AnsibleSelect[name="module_args"]').prop('onChange')(
|
wrapper.find('AnsibleSelect[name="module_args"]').prop('onChange')(
|
||||||
{},
|
{},
|
||||||
'command'
|
'command'
|
||||||
@@ -327,12 +335,13 @@ describe('<AdHocCOmmands />', () => {
|
|||||||
apiModule={InventoriesAPI}
|
apiModule={InventoriesAPI}
|
||||||
adHocItems={adHocItems}
|
adHocItems={adHocItems}
|
||||||
itemId={1}
|
itemId={1}
|
||||||
|
credentialTypeId={1}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</AdHocCommands>
|
</AdHocCommands>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
wrapper.find('button').prop('onClick')();
|
await act(async () => wrapper.find('button').prop('onClick')());
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
expect(wrapper.find('ErrorDetail').length).toBe(1);
|
expect(wrapper.find('ErrorDetail').length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ import React, { useState } from 'react';
|
|||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { withFormik, useFormikContext } from 'formik';
|
import { withFormik, useFormikContext } from 'formik';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import Wizard from '../Wizard';
|
import Wizard from '../Wizard';
|
||||||
import AdHocCredentialStep from './AdHocCredentialStep';
|
import AdHocCredentialStep from './AdHocCredentialStep';
|
||||||
import DetailsStep from './DetailsStep';
|
import AdHocDetailsStep from './AdHocDetailsStep';
|
||||||
|
|
||||||
function AdHocCommandsWizard({
|
function AdHocCommandsWizard({
|
||||||
onLaunch,
|
onLaunch,
|
||||||
@@ -16,31 +17,43 @@ function AdHocCommandsWizard({
|
|||||||
credentialTypeId,
|
credentialTypeId,
|
||||||
}) {
|
}) {
|
||||||
const [currentStepId, setCurrentStepId] = useState(1);
|
const [currentStepId, setCurrentStepId] = useState(1);
|
||||||
const [limitTypedValue, setLimitTypedValue] = useState('');
|
|
||||||
const [enableLaunch, setEnableLaunch] = useState(false);
|
const [enableLaunch, setEnableLaunch] = useState(false);
|
||||||
|
|
||||||
const { values } = useFormikContext();
|
const { values } = useFormikContext();
|
||||||
|
|
||||||
|
const enabledNextOnDetailsStep = () => {
|
||||||
|
if (!values.module_args) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (values.module_args === 'shell' || values.module_args === 'command') {
|
||||||
|
if (values.arguments) {
|
||||||
|
return true;
|
||||||
|
// eslint-disable-next-line no-else-return
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined; // makes the linter happy;
|
||||||
|
};
|
||||||
const steps = [
|
const steps = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
key: 1,
|
key: 1,
|
||||||
name: i18n._(t`Details`),
|
name: i18n._(t`Details`),
|
||||||
component: (
|
component: (
|
||||||
<DetailsStep
|
<AdHocDetailsStep
|
||||||
moduleOptions={moduleOptions}
|
moduleOptions={moduleOptions}
|
||||||
verbosityOptions={verbosityOptions}
|
verbosityOptions={verbosityOptions}
|
||||||
onLimitChange={value => setLimitTypedValue(value)}
|
|
||||||
limitValue={limitTypedValue}
|
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
enableNext: values.module_args && values.arguments && values.verbosity,
|
enableNext: enabledNextOnDetailsStep(),
|
||||||
nextButtonText: i18n._(t`Next`),
|
nextButtonText: i18n._(t`Next`),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
key: 2,
|
key: 2,
|
||||||
name: i18n._(t`Machine Credential`),
|
name: i18n._(t`Machine credential`),
|
||||||
component: (
|
component: (
|
||||||
<AdHocCredentialStep
|
<AdHocCredentialStep
|
||||||
credentialTypeId={credentialTypeId}
|
credentialTypeId={credentialTypeId}
|
||||||
@@ -55,19 +68,17 @@ function AdHocCommandsWizard({
|
|||||||
|
|
||||||
const currentStep = steps.find(step => step.id === currentStepId);
|
const currentStep = steps.find(step => step.id === currentStepId);
|
||||||
|
|
||||||
const submit = () => {
|
|
||||||
onLaunch(values, limitTypedValue);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wizard
|
<Wizard
|
||||||
style={{ overflow: 'scroll' }}
|
style={{ overflow: 'scroll' }}
|
||||||
isOpen
|
isOpen
|
||||||
onNext={step => setCurrentStepId(step.id)}
|
onNext={step => setCurrentStepId(step.id)}
|
||||||
onClose={() => onCloseWizard()}
|
onClose={() => onCloseWizard()}
|
||||||
onSave={submit}
|
onSave={() => {
|
||||||
|
onLaunch(values);
|
||||||
|
}}
|
||||||
steps={steps}
|
steps={steps}
|
||||||
title={i18n._(t`Ad Hoc Commands`)}
|
title={i18n._(t`Run command`)}
|
||||||
nextButtonText={currentStep.nextButtonText || undefined}
|
nextButtonText={currentStep.nextButtonText || undefined}
|
||||||
backButtonText={i18n._(t`Back`)}
|
backButtonText={i18n._(t`Back`)}
|
||||||
cancelButtonText={i18n._(t`Cancel`)}
|
cancelButtonText={i18n._(t`Cancel`)}
|
||||||
@@ -76,14 +87,14 @@ function AdHocCommandsWizard({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const FormikApp = withFormik({
|
const FormikApp = withFormik({
|
||||||
mapPropsToValues({ adHocItems }) {
|
mapPropsToValues({ adHocItems, verbosityOptions }) {
|
||||||
const adHocItemStrings = adHocItems.map(item => item.name);
|
const adHocItemStrings = adHocItems.map(item => item.name).join(', ');
|
||||||
return {
|
return {
|
||||||
limit: adHocItemStrings || [],
|
limit: adHocItemStrings || [],
|
||||||
credential: [],
|
credential: [],
|
||||||
module_args: '',
|
module_args: '',
|
||||||
arguments: '',
|
arguments: '',
|
||||||
verbosity: '',
|
verbosity: verbosityOptions[0].value,
|
||||||
forks: 0,
|
forks: 0,
|
||||||
changes: false,
|
changes: false,
|
||||||
escalation: false,
|
escalation: false,
|
||||||
@@ -92,4 +103,11 @@ const FormikApp = withFormik({
|
|||||||
},
|
},
|
||||||
})(AdHocCommandsWizard);
|
})(AdHocCommandsWizard);
|
||||||
|
|
||||||
|
FormikApp.propTypes = {
|
||||||
|
onLaunch: PropTypes.func.isRequired,
|
||||||
|
moduleOptions: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
verbosityOptions: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
onCloseWizard: PropTypes.func.isRequired,
|
||||||
|
credentialTypeId: PropTypes.number.isRequired,
|
||||||
|
};
|
||||||
export default withI18n()(FormikApp);
|
export default withI18n()(FormikApp);
|
||||||
|
|||||||
@@ -10,7 +10,13 @@ import AdHocCommandsWizard from './AdHocCommandsWizard';
|
|||||||
jest.mock('../../api/models/CredentialTypes');
|
jest.mock('../../api/models/CredentialTypes');
|
||||||
jest.mock('../../api/models/Inventories');
|
jest.mock('../../api/models/Inventories');
|
||||||
jest.mock('../../api/models/Credentials');
|
jest.mock('../../api/models/Credentials');
|
||||||
|
const verbosityOptions = [
|
||||||
|
{ value: '0', key: '0', label: '0 (Normal)' },
|
||||||
|
{ value: '1', key: '1', label: '1 (Verbose)' },
|
||||||
|
{ value: '2', key: '2', label: '2 (More Verbose)' },
|
||||||
|
{ value: '3', key: '3', label: '3 (Debug)' },
|
||||||
|
{ value: '4', key: '4', label: '4 (Connection Debug)' },
|
||||||
|
];
|
||||||
const adHocItems = [
|
const adHocItems = [
|
||||||
{ name: 'Inventory 1' },
|
{ name: 'Inventory 1' },
|
||||||
{ name: 'Inventory 2' },
|
{ name: 'Inventory 2' },
|
||||||
@@ -26,7 +32,7 @@ describe('<AdHocCommandsWizard/>', () => {
|
|||||||
adHocItems={adHocItems}
|
adHocItems={adHocItems}
|
||||||
onLaunch={onLaunch}
|
onLaunch={onLaunch}
|
||||||
moduleOptions={[]}
|
moduleOptions={[]}
|
||||||
verbosityOptions={[]}
|
verbosityOptions={verbosityOptions}
|
||||||
onCloseWizard={() => {}}
|
onCloseWizard={() => {}}
|
||||||
credentialTypeId={1}
|
credentialTypeId={1}
|
||||||
/>
|
/>
|
||||||
@@ -39,14 +45,11 @@ describe('<AdHocCommandsWizard/>', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should mount properly', async () => {
|
test('should mount properly', async () => {
|
||||||
// wrapper.update();
|
|
||||||
expect(wrapper.find('AdHocCommandsWizard').length).toBe(1);
|
expect(wrapper.find('AdHocCommandsWizard').length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('next and nav item should be disabled', async () => {
|
test('next and nav item should be disabled', async () => {
|
||||||
// wrapper.update();
|
|
||||||
await waitForElement(wrapper, 'WizardNavItem', el => el.length > 0);
|
await waitForElement(wrapper, 'WizardNavItem', el => el.length > 0);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
wrapper.find('WizardNavItem[content="Details"]').prop('isCurrent')
|
wrapper.find('WizardNavItem[content="Details"]').prop('isCurrent')
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
@@ -55,12 +58,12 @@ describe('<AdHocCommandsWizard/>', () => {
|
|||||||
).toBe(false);
|
).toBe(false);
|
||||||
expect(
|
expect(
|
||||||
wrapper
|
wrapper
|
||||||
.find('WizardNavItem[content="Machine Credential"]')
|
.find('WizardNavItem[content="Machine credential"]')
|
||||||
.prop('isDisabled')
|
.prop('isDisabled')
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
expect(
|
expect(
|
||||||
wrapper
|
wrapper
|
||||||
.find('WizardNavItem[content="Machine Credential"]')
|
.find('WizardNavItem[content="Machine credential"]')
|
||||||
.prop('isCurrent')
|
.prop('isCurrent')
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(true);
|
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(true);
|
||||||
@@ -69,7 +72,7 @@ describe('<AdHocCommandsWizard/>', () => {
|
|||||||
test('next button should become active, and should navigate to the next step', async () => {
|
test('next button should become active, and should navigate to the next step', async () => {
|
||||||
await waitForElement(wrapper, 'WizardNavItem', el => el.length > 0);
|
await waitForElement(wrapper, 'WizardNavItem', el => el.length > 0);
|
||||||
|
|
||||||
act(() => {
|
await act(async () => {
|
||||||
wrapper.find('AnsibleSelect[name="module_args"]').prop('onChange')(
|
wrapper.find('AnsibleSelect[name="module_args"]').prop('onChange')(
|
||||||
{},
|
{},
|
||||||
'command'
|
'command'
|
||||||
@@ -83,22 +86,25 @@ describe('<AdHocCommandsWizard/>', () => {
|
|||||||
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(
|
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
wrapper.find('Button[type="submit"]').prop('onClick')();
|
await act(async () =>
|
||||||
|
wrapper.find('Button[type="submit"]').prop('onClick')()
|
||||||
|
);
|
||||||
|
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
});
|
});
|
||||||
test('launch button should become active', async () => {
|
test('launch button should become active', async () => {
|
||||||
CredentialsAPI.read.mockResolvedValue({
|
CredentialsAPI.read.mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
results: [
|
results: [
|
||||||
{ id: 1, name: 'Cred 1' },
|
{ id: 1, name: 'Cred 1', url: '' },
|
||||||
{ id: 2, name: 'Cred2' },
|
{ id: 2, name: 'Cred2', url: '' },
|
||||||
],
|
],
|
||||||
count: 2,
|
count: 2,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
await waitForElement(wrapper, 'WizardNavItem', el => el.length > 0);
|
await waitForElement(wrapper, 'WizardNavItem', el => el.length > 0);
|
||||||
|
|
||||||
act(() => {
|
await act(async () => {
|
||||||
wrapper.find('AnsibleSelect[name="module_args"]').prop('onChange')(
|
wrapper.find('AnsibleSelect[name="module_args"]').prop('onChange')(
|
||||||
{},
|
{},
|
||||||
'command'
|
'command'
|
||||||
@@ -112,7 +118,9 @@ describe('<AdHocCommandsWizard/>', () => {
|
|||||||
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(
|
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
wrapper.find('Button[type="submit"]').prop('onClick')();
|
await act(async () =>
|
||||||
|
wrapper.find('Button[type="submit"]').prop('onClick')()
|
||||||
|
);
|
||||||
|
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
await waitForElement(wrapper, 'OptionsList', el => el.length > 0);
|
await waitForElement(wrapper, 'OptionsList', el => el.length > 0);
|
||||||
@@ -133,7 +141,11 @@ describe('<AdHocCommandsWizard/>', () => {
|
|||||||
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(
|
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
wrapper.find('Button[type="submit"]').prop('onClick')();
|
|
||||||
|
await act(async () =>
|
||||||
|
wrapper.find('Button[type="submit"]').prop('onClick')()
|
||||||
|
);
|
||||||
|
|
||||||
expect(onLaunch).toHaveBeenCalled();
|
expect(onLaunch).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -152,7 +164,7 @@ describe('<AdHocCommandsWizard/>', () => {
|
|||||||
);
|
);
|
||||||
await waitForElement(wrapper, 'WizardNavItem', el => el.length > 0);
|
await waitForElement(wrapper, 'WizardNavItem', el => el.length > 0);
|
||||||
|
|
||||||
act(() => {
|
await act(async () => {
|
||||||
wrapper.find('AnsibleSelect[name="module_args"]').prop('onChange')(
|
wrapper.find('AnsibleSelect[name="module_args"]').prop('onChange')(
|
||||||
{},
|
{},
|
||||||
'command'
|
'command'
|
||||||
@@ -166,11 +178,12 @@ describe('<AdHocCommandsWizard/>', () => {
|
|||||||
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(
|
expect(wrapper.find('Button[type="submit"]').prop('isDisabled')).toBe(
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
wrapper.find('Button[type="submit"]').prop('onClick')();
|
|
||||||
|
await act(async () =>
|
||||||
|
wrapper.find('Button[type="submit"]').prop('onClick')()
|
||||||
|
);
|
||||||
|
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
expect(wrapper.find('ContentLoading').length).toBe(1);
|
|
||||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
|
||||||
expect(wrapper.find('ContentError').length).toBe(1);
|
expect(wrapper.find('ContentError').length).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ import React, { useEffect, useCallback } from 'react';
|
|||||||
import { useHistory } from 'react-router-dom';
|
import { useHistory } from 'react-router-dom';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import { useField } from 'formik';
|
import { useField } from 'formik';
|
||||||
import { Form, FormGroup } from '@patternfly/react-core';
|
import { Form, FormGroup } from '@patternfly/react-core';
|
||||||
import { CredentialsAPI } from '../../api';
|
import { CredentialsAPI } from '../../api';
|
||||||
|
import { FieldTooltip } from '../FormField';
|
||||||
|
|
||||||
import { getQSConfig, parseQueryString, mergeParams } from '../../util/qs';
|
import { getQSConfig, parseQueryString, mergeParams } from '../../util/qs';
|
||||||
import useRequest from '../../util/useRequest';
|
import useRequest from '../../util/useRequest';
|
||||||
@@ -68,6 +70,13 @@ function AdHocCredentialStep({ i18n, credentialTypeId, onEnableLaunch }) {
|
|||||||
!credentialMeta.touched || !credentialMeta.error ? 'default' : 'error'
|
!credentialMeta.touched || !credentialMeta.error ? 'default' : 'error'
|
||||||
}
|
}
|
||||||
helperTextInvalid={credentialMeta.error}
|
helperTextInvalid={credentialMeta.error}
|
||||||
|
labelIcon={
|
||||||
|
<FieldTooltip
|
||||||
|
content={i18n._(
|
||||||
|
t`Select the credential you want to use when accessing the remote hosts to run the command. Choose the credential containing the username and SSH key or password that Ansible will need to log into the remote hosts.`
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<OptionsList
|
<OptionsList
|
||||||
value={credentialField.value || []}
|
value={credentialField.value || []}
|
||||||
@@ -111,4 +120,8 @@ function AdHocCredentialStep({ i18n, credentialTypeId, onEnableLaunch }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AdHocCredentialStep.propTypes = {
|
||||||
|
credentialTypeId: PropTypes.number.isRequired,
|
||||||
|
onEnableLaunch: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
export default withI18n()(AdHocCredentialStep);
|
export default withI18n()(AdHocCredentialStep);
|
||||||
|
|||||||
@@ -2,20 +2,12 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import { useField } from 'formik';
|
import { useField } from 'formik';
|
||||||
import {
|
import { Form, FormGroup, Switch, Checkbox } from '@patternfly/react-core';
|
||||||
Form,
|
|
||||||
FormGroup,
|
|
||||||
InputGroup,
|
|
||||||
TextInput,
|
|
||||||
Label,
|
|
||||||
InputGroupText,
|
|
||||||
Switch,
|
|
||||||
Checkbox,
|
|
||||||
} from '@patternfly/react-core';
|
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
|
import { BrandName } from '../../variables';
|
||||||
import AnsibleSelect from '../AnsibleSelect';
|
import AnsibleSelect from '../AnsibleSelect';
|
||||||
import FormField, { FieldTooltip } from '../FormField';
|
import FormField, { FieldTooltip } from '../FormField';
|
||||||
import { VariablesField } from '../CodeMirrorInput';
|
import { VariablesField } from '../CodeMirrorInput';
|
||||||
@@ -30,18 +22,16 @@ const TooltipWrapper = styled.div`
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function CredentialStep({
|
// Setting BrandName to a variable here is necessary to get the jest tests
|
||||||
i18n,
|
// passing. Attempting to use BrandName in the template literal results
|
||||||
verbosityOptions,
|
// in failing tests.
|
||||||
moduleOptions,
|
const brandName = BrandName;
|
||||||
onLimitChange,
|
|
||||||
limitValue,
|
function CredentialStep({ i18n, verbosityOptions, moduleOptions }) {
|
||||||
}) {
|
|
||||||
const [moduleField, moduleMeta, moduleHelpers] = useField({
|
const [moduleField, moduleMeta, moduleHelpers] = useField({
|
||||||
name: 'module_args',
|
name: 'module_args',
|
||||||
validate: required(null, i18n),
|
validate: required(null, i18n),
|
||||||
});
|
});
|
||||||
const [limitField, , limitHelpers] = useField('limit');
|
|
||||||
const [variablesField] = useField('extra_vars');
|
const [variablesField] = useField('extra_vars');
|
||||||
const [changesField, , changesHelpers] = useField('changes');
|
const [changesField, , changesHelpers] = useField('changes');
|
||||||
const [escalationField, , escalationHelpers] = useField('escalation');
|
const [escalationField, , escalationHelpers] = useField('escalation');
|
||||||
@@ -65,7 +55,7 @@ function CredentialStep({
|
|||||||
labelIcon={
|
labelIcon={
|
||||||
<FieldTooltip
|
<FieldTooltip
|
||||||
content={i18n._(
|
content={i18n._(
|
||||||
t`These are the modules that AWX supports running commands against.`
|
t`These are the modules that ${brandName} supports running commands against.`
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -85,7 +75,9 @@ function CredentialStep({
|
|||||||
name="arguments"
|
name="arguments"
|
||||||
type="text"
|
type="text"
|
||||||
label={i18n._(t`Arguments`)}
|
label={i18n._(t`Arguments`)}
|
||||||
isRequired
|
isRequired={
|
||||||
|
moduleField.value === 'command' || moduleField.value === 'shell'
|
||||||
|
}
|
||||||
tooltip={i18n._(
|
tooltip={i18n._(
|
||||||
t`These arguments are used with the specified module.`
|
t`These arguments are used with the specified module.`
|
||||||
)}
|
)}
|
||||||
@@ -118,54 +110,26 @@ function CredentialStep({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
<FormField
|
||||||
<FormGroup
|
id="limit"
|
||||||
|
name="limit"
|
||||||
|
type="text"
|
||||||
label={i18n._(t`Limit`)}
|
label={i18n._(t`Limit`)}
|
||||||
labelIcon={
|
tooltip={
|
||||||
<FieldTooltip
|
<span>
|
||||||
content={
|
{i18n._(
|
||||||
<span>
|
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`
|
||||||
{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"
|
||||||
<a
|
target="_blank"
|
||||||
href="https://docs.ansible.com/ansible/latest/user_guide/intro_patterns.html"
|
rel="noopener noreferrer"
|
||||||
target="_blank"
|
>
|
||||||
rel="noopener noreferrer"
|
{i18n._(`here`)}
|
||||||
>
|
</a>
|
||||||
{i18n._(`here`)}
|
</span>
|
||||||
</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
|
<FormField
|
||||||
id="template-forks"
|
id="template-forks"
|
||||||
name="forks"
|
name="forks"
|
||||||
@@ -175,14 +139,14 @@ function CredentialStep({
|
|||||||
tooltip={
|
tooltip={
|
||||||
<span>
|
<span>
|
||||||
{i18n._(
|
{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 `
|
t`The number of parallel or simultaneous processes to use while executing the playbook. Inputting no value will use the default value from the ansible configuration file. You can find more information`
|
||||||
)}{' '}
|
)}{' '}
|
||||||
<a
|
<a
|
||||||
href="https://docs.ansible.com/ansible/latest/installation_guide/intro_configuration.html#the-ansible-configuration-file"
|
href="https://docs.ansible.com/ansible/latest/installation_guide/intro_configuration.html#the-ansible-configuration-file"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
{i18n._(t`ansible configuration file.`)}
|
{i18n._(t`here.`)}
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
@@ -225,13 +189,13 @@ function CredentialStep({
|
|||||||
content={
|
content={
|
||||||
<p>
|
<p>
|
||||||
{i18n._(t`Enables creation of a provisioning
|
{i18n._(t`Enables creation of a provisioning
|
||||||
callback URL. Using the URL a host can contact BRAND_NAME
|
callback URL. Using the URL a host can contact ${brandName}
|
||||||
and request a configuration update using this job
|
and request a configuration update using this job
|
||||||
template`)}
|
template`)}
|
||||||
|
|
||||||
<code>--{i18n._(t`become`)} </code>
|
<code>--become </code>
|
||||||
{i18n._(t`option to the`)}
|
{i18n._(t`option to the`)}
|
||||||
<code>{i18n._(t`ansible`)} </code>
|
<code>ansible </code>
|
||||||
{i18n._(t`command`)}
|
{i18n._(t`command`)}
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
@@ -297,4 +261,9 @@ function CredentialStep({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CredentialStep.propTypes = {
|
||||||
|
moduleOptions: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
verbosityOptions: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
export default withI18n()(CredentialStep);
|
export default withI18n()(CredentialStep);
|
||||||
@@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import { act } from 'react-dom/test-utils';
|
import { act } from 'react-dom/test-utils';
|
||||||
import { Formik } from 'formik';
|
import { Formik } from 'formik';
|
||||||
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
||||||
import DetailsStep from './DetailsStep';
|
import DetailsStep from './AdHocDetailsStep';
|
||||||
|
|
||||||
jest.mock('../../api/models/Credentials');
|
jest.mock('../../api/models/Credentials');
|
||||||
|
|
||||||
@@ -17,7 +17,6 @@ const moduleOptions = [
|
|||||||
{ key: 1, value: 'shell', label: 'shell', isDisabled: false },
|
{ key: 1, value: 'shell', label: 'shell', isDisabled: false },
|
||||||
];
|
];
|
||||||
const onLimitChange = jest.fn();
|
const onLimitChange = jest.fn();
|
||||||
const limitValue = '';
|
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
limit: ['Inventory 1', 'inventory 2'],
|
limit: ['Inventory 1', 'inventory 2'],
|
||||||
credential: [],
|
credential: [],
|
||||||
@@ -46,7 +45,6 @@ describe('<DetailsStep />', () => {
|
|||||||
verbosityOptions={verbosityOptions}
|
verbosityOptions={verbosityOptions}
|
||||||
moduleOptions={moduleOptions}
|
moduleOptions={moduleOptions}
|
||||||
onLimitChange={onLimitChange}
|
onLimitChange={onLimitChange}
|
||||||
limitValue={limitValue}
|
|
||||||
/>
|
/>
|
||||||
</Formik>
|
</Formik>
|
||||||
);
|
);
|
||||||
@@ -61,7 +59,6 @@ describe('<DetailsStep />', () => {
|
|||||||
verbosityOptions={verbosityOptions}
|
verbosityOptions={verbosityOptions}
|
||||||
moduleOptions={moduleOptions}
|
moduleOptions={moduleOptions}
|
||||||
onLimitChange={onLimitChange}
|
onLimitChange={onLimitChange}
|
||||||
limitValue={limitValue}
|
|
||||||
/>
|
/>
|
||||||
</Formik>
|
</Formik>
|
||||||
);
|
);
|
||||||
@@ -69,7 +66,7 @@ describe('<DetailsStep />', () => {
|
|||||||
expect(wrapper.find('FormGroup[label="Module"]').length).toBe(1);
|
expect(wrapper.find('FormGroup[label="Module"]').length).toBe(1);
|
||||||
expect(wrapper.find('FormField[name="arguments"]').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="Verbosity"]').length).toBe(1);
|
||||||
expect(wrapper.find('FormGroup[label="Limit"]').length).toBe(1);
|
expect(wrapper.find('FormField[label="Limit"]').length).toBe(1);
|
||||||
expect(wrapper.find('FormField[name="forks"]').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[label="Show changes"]').length).toBe(1);
|
||||||
expect(
|
expect(
|
||||||
@@ -86,7 +83,6 @@ describe('<DetailsStep />', () => {
|
|||||||
verbosityOptions={verbosityOptions}
|
verbosityOptions={verbosityOptions}
|
||||||
moduleOptions={moduleOptions}
|
moduleOptions={moduleOptions}
|
||||||
onLimitChange={onLimitChange}
|
onLimitChange={onLimitChange}
|
||||||
limitValue={limitValue}
|
|
||||||
/>
|
/>
|
||||||
</Formik>
|
</Formik>
|
||||||
);
|
);
|
||||||
@@ -100,6 +96,12 @@ describe('<DetailsStep />', () => {
|
|||||||
wrapper.find('input#arguments').simulate('change', {
|
wrapper.find('input#arguments').simulate('change', {
|
||||||
target: { value: 'foo', name: 'arguments' },
|
target: { value: 'foo', name: 'arguments' },
|
||||||
});
|
});
|
||||||
|
wrapper.find('input#limit').simulate('change', {
|
||||||
|
target: {
|
||||||
|
value: 'Inventory 1, inventory 2, new inventory',
|
||||||
|
name: 'limit',
|
||||||
|
},
|
||||||
|
});
|
||||||
wrapper.find('AnsibleSelect[name="verbosity"]').prop('onChange')({}, 1);
|
wrapper.find('AnsibleSelect[name="verbosity"]').prop('onChange')({}, 1);
|
||||||
|
|
||||||
wrapper.find('TextInputBase[name="forks"]').simulate('change', {
|
wrapper.find('TextInputBase[name="forks"]').simulate('change', {
|
||||||
@@ -121,7 +123,9 @@ describe('<DetailsStep />', () => {
|
|||||||
1
|
1
|
||||||
);
|
);
|
||||||
expect(wrapper.find('TextInputBase[name="forks"]').prop('value')).toBe(10);
|
expect(wrapper.find('TextInputBase[name="forks"]').prop('value')).toBe(10);
|
||||||
expect(wrapper.find('TextInputBase[label="Limit"]').prop('value')).toBe('');
|
expect(wrapper.find('TextInputBase[name="limit"]').prop('value')).toBe(
|
||||||
|
'Inventory 1, inventory 2, new inventory'
|
||||||
|
);
|
||||||
expect(wrapper.find('Switch').prop('isChecked')).toBe(true);
|
expect(wrapper.find('Switch').prop('isChecked')).toBe(true);
|
||||||
expect(
|
expect(
|
||||||
wrapper
|
wrapper
|
||||||
@@ -129,22 +133,4 @@ describe('<DetailsStep />', () => {
|
|||||||
.prop('isChecked')
|
.prop('isChecked')
|
||||||
).toBe(true);
|
).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'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
@@ -2,7 +2,13 @@ import React, { useCallback, useState, useEffect } from 'react';
|
|||||||
import { useParams, useLocation } from 'react-router-dom';
|
import { useParams, useLocation } from 'react-router-dom';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Button, Tooltip } from '@patternfly/react-core';
|
import {
|
||||||
|
Button,
|
||||||
|
Tooltip,
|
||||||
|
DropdownItem,
|
||||||
|
ToolbarGroup,
|
||||||
|
ToolbarItem,
|
||||||
|
} from '@patternfly/react-core';
|
||||||
import { getQSConfig, parseQueryString } from '../../../util/qs';
|
import { getQSConfig, parseQueryString } from '../../../util/qs';
|
||||||
import useSelected from '../../../util/useSelected';
|
import useSelected from '../../../util/useSelected';
|
||||||
import useRequest from '../../../util/useRequest';
|
import useRequest from '../../../util/useRequest';
|
||||||
@@ -17,6 +23,7 @@ import PaginatedDataList, {
|
|||||||
import InventoryGroupItem from './InventoryGroupItem';
|
import InventoryGroupItem from './InventoryGroupItem';
|
||||||
import InventoryGroupsDeleteModal from '../shared/InventoryGroupsDeleteModal';
|
import InventoryGroupsDeleteModal from '../shared/InventoryGroupsDeleteModal';
|
||||||
import AdHocCommandsButton from '../../../components/AdHocCommands/AdHocCommands';
|
import AdHocCommandsButton from '../../../components/AdHocCommands/AdHocCommands';
|
||||||
|
import { Kebabified } from '../../../contexts/Kebabified';
|
||||||
|
|
||||||
const QS_CONFIG = getQSConfig('group', {
|
const QS_CONFIG = getQSConfig('group', {
|
||||||
page: 1,
|
page: 1,
|
||||||
@@ -141,9 +148,37 @@ function InventoryGroupsList({ i18n }) {
|
|||||||
setSelected([]);
|
setSelected([]);
|
||||||
setIsDeleteLoading(false);
|
setIsDeleteLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const canAdd =
|
const canAdd =
|
||||||
actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
|
actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
|
||||||
|
const kebabedAdditionalControls = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AdHocCommandsButton
|
||||||
|
adHocItems={selected}
|
||||||
|
apiModule={InventoriesAPI}
|
||||||
|
itemId={parseInt(inventoryId, 10)}
|
||||||
|
>
|
||||||
|
{({ openAdHocCommands }) => (
|
||||||
|
<DropdownItem
|
||||||
|
key="add"
|
||||||
|
onClick={openAdHocCommands}
|
||||||
|
isDisabled={groupCount === 0}
|
||||||
|
>
|
||||||
|
{i18n._(t`Run command`)}
|
||||||
|
</DropdownItem>
|
||||||
|
)}
|
||||||
|
</AdHocCommandsButton>
|
||||||
|
<DropdownItem
|
||||||
|
variant="danger"
|
||||||
|
aria-label={i18n._(t`Delete`)}
|
||||||
|
onClick={toggleModal}
|
||||||
|
isDisabled={selected.length === 0 || selected.some(cannotDelete)}
|
||||||
|
>
|
||||||
|
{i18n._(t`Delete`)}
|
||||||
|
</DropdownItem>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -213,48 +248,66 @@ function InventoryGroupsList({ i18n }) {
|
|||||||
/>,
|
/>,
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
<Tooltip content={renderTooltip()} position="top" key="delete">
|
<Kebabified>
|
||||||
<div>
|
{({ isKebabified }) => (
|
||||||
<Button
|
<>
|
||||||
variant="secondary"
|
{isKebabified ? (
|
||||||
aria-label={i18n._(t`Delete`)}
|
kebabedAdditionalControls()
|
||||||
onClick={toggleModal}
|
) : (
|
||||||
isDisabled={
|
<ToolbarGroup>
|
||||||
selected.length === 0 || selected.some(cannotDelete)
|
<ToolbarItem>
|
||||||
}
|
<Tooltip
|
||||||
>
|
content={i18n._(
|
||||||
{i18n._(t`Delete`)}
|
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.`
|
||||||
</Button>
|
)}
|
||||||
</div>
|
position="top"
|
||||||
</Tooltip>,
|
key="adhoc"
|
||||||
[
|
>
|
||||||
<Tooltip
|
<AdHocCommandsButton
|
||||||
content={i18n._(
|
css="margin-right: 20px"
|
||||||
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.`
|
adHocItems={selected}
|
||||||
)}
|
apiModule={InventoriesAPI}
|
||||||
position="top"
|
itemId={parseInt(inventoryId, 10)}
|
||||||
key="adhoc"
|
>
|
||||||
>
|
{({ openAdHocCommands }) => (
|
||||||
<AdHocCommandsButton
|
<Button
|
||||||
adHocItems={selected}
|
variant="secondary"
|
||||||
apiModule={InventoriesAPI}
|
aria-label={i18n._(t`Run command`)}
|
||||||
itemId={inventoryId}
|
onClick={openAdHocCommands}
|
||||||
>
|
isDisabled={groupCount === 0}
|
||||||
{({ openAdHocCommands }) => (
|
>
|
||||||
<Button
|
{i18n._(t`Run command`)}
|
||||||
variant="secondary"
|
</Button>
|
||||||
aria-label={i18n._(t`Run command`)}
|
)}
|
||||||
onClick={openAdHocCommands}
|
</AdHocCommandsButton>
|
||||||
isDisabled={
|
</Tooltip>
|
||||||
selected.length === 0 || selected.some(cannotDelete)
|
</ToolbarItem>
|
||||||
}
|
<ToolbarItem>
|
||||||
>
|
<Tooltip
|
||||||
{i18n._(t`Run command`)}
|
content={renderTooltip()}
|
||||||
</Button>
|
position="top"
|
||||||
|
key="delete"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
variant="danger"
|
||||||
|
aria-label={i18n._(t`Delete`)}
|
||||||
|
onClick={toggleModal}
|
||||||
|
isDisabled={
|
||||||
|
selected.length === 0 ||
|
||||||
|
selected.some(cannotDelete)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{i18n._(t`Delete`)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</ToolbarItem>
|
||||||
|
</ToolbarGroup>
|
||||||
)}
|
)}
|
||||||
</AdHocCommandsButton>
|
</>
|
||||||
</Tooltip>,
|
)}
|
||||||
],
|
</Kebabified>,
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -271,6 +324,7 @@ function InventoryGroupsList({ i18n }) {
|
|||||||
<AlertModal
|
<AlertModal
|
||||||
isOpen={deletionError}
|
isOpen={deletionError}
|
||||||
variant="error"
|
variant="error"
|
||||||
|
aria-label={i18n._(t`deletion error`)}
|
||||||
title={i18n._(t`Error!`)}
|
title={i18n._(t`Error!`)}
|
||||||
onClose={() => setDeletionError(null)}
|
onClose={() => setDeletionError(null)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -88,6 +88,10 @@ describe('<InventoryGroupsList />', () => {
|
|||||||
});
|
});
|
||||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
});
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
test('initially renders successfully', () => {
|
test('initially renders successfully', () => {
|
||||||
expect(wrapper.find('InventoryGroupsList').length).toBe(1);
|
expect(wrapper.find('InventoryGroupsList').length).toBe(1);
|
||||||
@@ -143,15 +147,17 @@ describe('<InventoryGroupsList />', () => {
|
|||||||
expect(el.props().checked).toBe(false);
|
expect(el.props().checked).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
describe('<InventoryGroupsList/> error handling', () => {
|
||||||
|
let wrapper;
|
||||||
test('should show content error when api throws error on initial render', async () => {
|
test('should show content error when api throws error on initial render', async () => {
|
||||||
InventoriesAPI.readGroupsOptions.mockImplementation(() =>
|
InventoriesAPI.readGroupsOptions.mockImplementationOnce(() =>
|
||||||
Promise.reject(new Error())
|
Promise.reject(new Error())
|
||||||
);
|
);
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(<InventoryGroupsList />);
|
wrapper = mountWithContexts(<InventoryGroupsList />);
|
||||||
});
|
});
|
||||||
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
|
await waitForElement(wrapper, 'ContentError', el => el.length > 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should show content error if groups are not successfully fetched from api', async () => {
|
test('should show content error if groups are not successfully fetched from api', async () => {
|
||||||
@@ -159,26 +165,27 @@ describe('<InventoryGroupsList />', () => {
|
|||||||
Promise.reject(new Error())
|
Promise.reject(new Error())
|
||||||
);
|
);
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper.find('DataListCheck[id="select-group-1"]').invoke('onChange')();
|
wrapper = mountWithContexts(<InventoryGroupsList />);
|
||||||
});
|
});
|
||||||
wrapper.update();
|
|
||||||
await act(async () => {
|
await waitForElement(wrapper, 'ContentError', el => el.length > 0);
|
||||||
wrapper.find('Toolbar Button[aria-label="Delete"]').invoke('onClick')();
|
|
||||||
});
|
|
||||||
await waitForElement(
|
|
||||||
wrapper,
|
|
||||||
'InventoryGroupsDeleteModal',
|
|
||||||
el => el.props().isModalOpen === true
|
|
||||||
);
|
|
||||||
await act(async () => {
|
|
||||||
wrapper
|
|
||||||
.find('ModalBoxFooter Button[aria-label="Delete"]')
|
|
||||||
.invoke('onClick')();
|
|
||||||
});
|
|
||||||
await waitForElement(wrapper, 'ContentError', el => el.length === 1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should show error modal when group is not successfully deleted from api', async () => {
|
test('should show error modal when group is not successfully deleted from api', async () => {
|
||||||
|
InventoriesAPI.readGroups.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
count: mockGroups.length,
|
||||||
|
results: mockGroups,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
InventoriesAPI.readGroupsOptions.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
actions: {
|
||||||
|
GET: {},
|
||||||
|
POST: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
GroupsAPI.destroy.mockRejectedValue(
|
GroupsAPI.destroy.mockRejectedValue(
|
||||||
new Error({
|
new Error({
|
||||||
response: {
|
response: {
|
||||||
@@ -190,6 +197,25 @@ describe('<InventoryGroupsList />', () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/inventories/inventory/3/groups'],
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<Route path="/inventories/inventory/:id/groups">
|
||||||
|
<InventoryGroupsList />
|
||||||
|
</Route>,
|
||||||
|
{
|
||||||
|
context: {
|
||||||
|
router: { history, route: { location: history.location } },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
waitForElement(wrapper, 'ContentEmpty', el => el.length === 0);
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper.find('DataListCheck[id="select-group-1"]').invoke('onChange')();
|
wrapper.find('DataListCheck[id="select-group-1"]').invoke('onChange')();
|
||||||
});
|
});
|
||||||
@@ -213,11 +239,14 @@ describe('<InventoryGroupsList />', () => {
|
|||||||
});
|
});
|
||||||
await waitForElement(
|
await waitForElement(
|
||||||
wrapper,
|
wrapper,
|
||||||
'AlertModal[title="Error!"] Modal',
|
'AlertModal[aria-label="deletion error"] Modal',
|
||||||
el => el.props().isOpen === true && el.props().title === 'Error!'
|
el => el.props().isOpen === true && el.props().title === 'Error!'
|
||||||
);
|
);
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper.find('ModalBoxCloseButton').invoke('onClose')();
|
wrapper
|
||||||
|
.find('AlertModal[aria-label="deletion error"]')
|
||||||
|
.invoke('onClose')();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user