Clear inv src subform values when source value changes

* Test that inv file field resets when project value changes
* Remove project and inv file path from API request when type is SCM
* Update checkbox tooltip to accept node proptypes
* Format option field tooltips
This commit is contained in:
Marliana Lara
2020-05-07 08:57:08 -04:00
parent b717aabcc9
commit 4b53875a71
7 changed files with 116 additions and 27 deletions

View File

@@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { string, func } from 'prop-types'; import { string, func, node } from 'prop-types';
import { useField } from 'formik'; import { useField } from 'formik';
import { Checkbox, Tooltip } from '@patternfly/react-core'; import { Checkbox, Tooltip } from '@patternfly/react-core';
import { QuestionCircleIcon as PFQuestionCircleIcon } from '@patternfly/react-icons'; import { QuestionCircleIcon as PFQuestionCircleIcon } from '@patternfly/react-icons';
@@ -40,7 +40,7 @@ CheckboxField.propTypes = {
name: string.isRequired, name: string.isRequired,
label: string.isRequired, label: string.isRequired,
validate: func, validate: func,
tooltip: string, tooltip: node,
}; };
CheckboxField.defaultProps = { CheckboxField.defaultProps = {
validate: () => {}, validate: () => {},

View File

@@ -26,12 +26,21 @@ function InventorySourceAdd() {
}, [result, history]); }, [result, history]);
const handleSubmit = async form => { const handleSubmit = async form => {
const { credential, source_project, ...remainingForm } = form; const { credential, source_path, source_project, ...remainingForm } = form;
const sourcePath = {};
const sourceProject = {};
if (form.source === 'scm') {
sourcePath.source_path =
source_path === '/ (project root)' ? '' : source_path;
sourceProject.source_project = source_project.id;
}
await request({ await request({
credential: credential?.id || null, credential: credential?.id || null,
source_project: source_project?.id || null,
inventory: id, inventory: id,
...sourcePath,
...sourceProject,
...remainingForm, ...remainingForm,
}); });
}; };

View File

@@ -1,5 +1,6 @@
import React, { useEffect, useCallback, useContext } from 'react'; import React, { useEffect, useCallback, useContext } from 'react';
import { Formik, useField } from 'formik'; import { Formik, useField, useFormikContext } from 'formik';
import { func, shape } from 'prop-types';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { InventorySourcesAPI } from '@api'; import { InventorySourcesAPI } from '@api';
@@ -21,7 +22,8 @@ import { FormColumnLayout, SubFormLayout } from '@components/FormLayout';
import SCMSubForm from './InventorySourceSubForms'; import SCMSubForm from './InventorySourceSubForms';
const InventorySourceFormFields = ({ sourceOptions, i18n }) => { const InventorySourceFormFields = ({ sourceOptions, i18n }) => {
const [sourceField, sourceMeta, sourceHelpers] = useField({ const { values, initialValues, resetForm } = useFormikContext();
const [sourceField, sourceMeta] = useField({
name: 'source', name: 'source',
validate: required(i18n._(t`Set a value for this field`), i18n), validate: required(i18n._(t`Set a value for this field`), i18n),
}); });
@@ -33,6 +35,18 @@ const InventorySourceFormFields = ({ sourceOptions, i18n }) => {
key: 'default', key: 'default',
}; };
const resetSubFormFields = sourceType => {
resetForm({
values: {
...initialValues,
name: values.name,
description: values.description,
custom_virtualenv: values.custom_virtualenv,
source: sourceType,
},
});
};
return ( return (
<> <>
<FormField <FormField
@@ -69,7 +83,7 @@ const InventorySourceFormFields = ({ sourceOptions, i18n }) => {
...sourceOptions, ...sourceOptions,
]} ]}
onChange={(event, value) => { onChange={(event, value) => {
sourceHelpers.setValue(value); resetSubFormFields(value);
}} }}
/> />
</FormGroup> </FormGroup>
@@ -197,4 +211,14 @@ const InventorySourceForm = ({
); );
}; };
InventorySourceForm.propTypes = {
onCancel: func.isRequired,
onSubmit: func.isRequired,
submitError: shape({}),
};
InventorySourceForm.defaultProps = {
submitError: null,
};
export default withI18n()(InventorySourceForm); export default withI18n()(InventorySourceForm);

View File

@@ -14,7 +14,7 @@ describe('<InventorySourceForm />', () => {
data: { count: 0, results: [] }, data: { count: 0, results: [] },
}); });
ProjectsAPI.readInventories.mockResolvedValue({ ProjectsAPI.readInventories.mockResolvedValue({
data: ['foo'], data: ['foo', 'bar'],
}); });
InventorySourcesAPI.readOptions.mockResolvedValue({ InventorySourcesAPI.readOptions.mockResolvedValue({
data: { data: {

View File

@@ -31,8 +31,7 @@ const SCMSubForm = ({ i18n }) => {
} = useRequest( } = useRequest(
useCallback(async projectId => { useCallback(async projectId => {
const { data } = await ProjectsAPI.readInventories(projectId); const { data } = await ProjectsAPI.readInventories(projectId);
data.push('/ (project root)'); return [...data, '/ (project root)'];
return data;
}, []), }, []),
[] []
); );

View File

@@ -3,9 +3,10 @@ import { act } from 'react-dom/test-utils';
import { mountWithContexts } from '@testUtils/enzymeHelpers'; import { mountWithContexts } from '@testUtils/enzymeHelpers';
import { Formik } from 'formik'; import { Formik } from 'formik';
import SCMSubForm from './SCMSubForm'; import SCMSubForm from './SCMSubForm';
import { ProjectsAPI } from '@api'; import { ProjectsAPI, CredentialsAPI } from '@api';
jest.mock('@api/models/Projects'); jest.mock('@api/models/Projects');
jest.mock('@api/models/Credentials');
const initialValues = { const initialValues = {
credential: null, credential: null,
@@ -23,9 +24,26 @@ const initialValues = {
describe('<SCMSubForm />', () => { describe('<SCMSubForm />', () => {
let wrapper; let wrapper;
CredentialsAPI.read.mockResolvedValue({
data: { count: 0, results: [] },
});
ProjectsAPI.readInventories.mockResolvedValue({ ProjectsAPI.readInventories.mockResolvedValue({
data: ['foo'], data: ['foo', 'bar'],
});
ProjectsAPI.read.mockResolvedValue({
data: {
count: 2,
results: [
{
id: 1,
name: 'mock proj one',
},
{
id: 2,
name: 'mock proj two',
},
],
},
}); });
beforeAll(async () => { beforeAll(async () => {
@@ -59,10 +77,34 @@ describe('<SCMSubForm />', () => {
await act(async () => { await act(async () => {
wrapper.find('ProjectLookup').invoke('onChange')({ wrapper.find('ProjectLookup').invoke('onChange')({
id: 2, id: 2,
name: 'mock proj', name: 'mock proj two',
}); });
wrapper.find('ProjectLookup').invoke('onBlur')(); wrapper.find('ProjectLookup').invoke('onBlur')();
}); });
expect(ProjectsAPI.readInventories).toHaveBeenCalledWith(2); expect(ProjectsAPI.readInventories).toHaveBeenCalledWith(2);
}); });
test('changing source project should reset source path dropdown', async () => {
expect(wrapper.find('AnsibleSelect#source_path').prop('value')).toEqual('');
await act(async () => {
await wrapper.find('AnsibleSelect#source_path').prop('onChange')(
null,
'bar'
);
});
wrapper.update();
expect(wrapper.find('AnsibleSelect#source_path').prop('value')).toEqual(
'bar'
);
await act(async () => {
wrapper.find('ProjectLookup').invoke('onChange')({
id: 1,
name: 'mock proj one',
});
});
wrapper.update();
expect(wrapper.find('AnsibleSelect#source_path').prop('value')).toEqual('');
});
}); });

View File

@@ -63,24 +63,39 @@ export const OptionsField = withI18n()(({ i18n }) => {
id="overwrite" id="overwrite"
name="overwrite" name="overwrite"
label={i18n._(t`Overwrite`)} label={i18n._(t`Overwrite`)}
tooltip={i18n._(t`If checked, any hosts and groups that were tooltip={
previously present on the external source but are now removed <>
will be removed from the Tower inventory. Hosts and groups {i18n._(t`If checked, any hosts and groups that were
that were not managed by the inventory source will be promoted previously present on the external source but are now removed
to the next manually created group or if there is no manually will be removed from the Tower inventory. Hosts and groups
created group to promote them into, they will be left in the "all" that were not managed by the inventory source will be promoted
default group for the inventory. When not checked, local child to the next manually created group or if there is no manually
hosts and groups not found on the external source will remain created group to promote them into, they will be left in the "all"
untouched by the inventory update process.`)} default group for the inventory.`)}
<br />
<br />
{i18n._(t`When not checked, local child
hosts and groups not found on the external source will remain
untouched by the inventory update process.`)}
</>
}
/> />
<CheckboxField <CheckboxField
id="overwrite_vars" id="overwrite_vars"
name="overwrite_vars" name="overwrite_vars"
label={i18n._(t`Overwrite variables`)} label={i18n._(t`Overwrite variables`)}
tooltip={i18n._(t`If checked, all variables for child groups and hosts tooltip={
will be removed and replaced by those found on the external source. <>
When not checked, a merge will be performed, combining local {i18n._(t`If checked, all variables for child groups
variables with those found on the external source.`)} and hosts will be removed and replaced by those found
on the external source.`)}
<br />
<br />
{i18n._(t`When not checked, a merge will be performed,
combining local variables with those found on the
external source.`)}
</>
}
/> />
<CheckboxField <CheckboxField
id="update_on_launch" id="update_on_launch"