diff --git a/awx/ui_next/src/components/CodeMirrorInput/VariablesField.jsx b/awx/ui_next/src/components/CodeMirrorInput/VariablesField.jsx index 34d6e8608e..734b68b53a 100644 --- a/awx/ui_next/src/components/CodeMirrorInput/VariablesField.jsx +++ b/awx/ui_next/src/components/CodeMirrorInput/VariablesField.jsx @@ -3,6 +3,7 @@ import { string, bool } from 'prop-types'; import { useField } from 'formik'; import { Split, SplitItem } from '@patternfly/react-core'; import { yamlToJson, jsonToYaml, isJson } from '@util/yaml'; +import { FormFullWidthLayout } from '@components/FormLayout'; import CodeMirrorInput from './CodeMirrorInput'; import YamlJsonToggle from './YamlJsonToggle'; import { JSON_MODE, YAML_MODE } from './constants'; @@ -12,50 +13,46 @@ function VariablesField({ id, name, label, readOnly }) { const [mode, setMode] = useState(isJson(field.value) ? JSON_MODE : YAML_MODE); return ( - - {({ field, form }) => ( - - - - - {label} - - - - { - try { - const newVal = - newMode === YAML_MODE - ? jsonToYaml(field.value) - : yamlToJson(field.value); - helpers.setValue(newVal); - setMode(newMode); - } catch (err) { - helpers.setError(err.message); - } - }} - /> - - - + + + + {label} + + + + { - helpers.setValue(newVal); + onChange={newMode => { + try { + const newVal = + newMode === YAML_MODE + ? jsonToYaml(field.value) + : yamlToJson(field.value); + helpers.setValue(newVal); + setMode(newMode); + } catch (err) { + helpers.setError(err.message); + } }} - hasErrors={!!meta.error} /> + + + { + helpers.setValue(newVal); + }} + hasErrors={!!meta.error} + /> {meta.error ? ( {meta.error} - - ) : null} - )} - + ) : null} + ); } VariablesField.propTypes = { diff --git a/awx/ui_next/src/components/FormActionGroup/FormActionGroup.jsx b/awx/ui_next/src/components/FormActionGroup/FormActionGroup.jsx index bae4fe0772..71adc9c2f7 100644 --- a/awx/ui_next/src/components/FormActionGroup/FormActionGroup.jsx +++ b/awx/ui_next/src/components/FormActionGroup/FormActionGroup.jsx @@ -9,6 +9,7 @@ const ActionGroup = styled(PFActionGroup)` display: flex; justify-content: flex-end; --pf-c-form__group--m-action--MarginTop: 0; + grid-column: 1 / -1; .pf-c-form__actions { & > button { diff --git a/awx/ui_next/src/components/FormLayout/FormLayout.jsx b/awx/ui_next/src/components/FormLayout/FormLayout.jsx new file mode 100644 index 0000000000..aeab53a3ef --- /dev/null +++ b/awx/ui_next/src/components/FormLayout/FormLayout.jsx @@ -0,0 +1,44 @@ +import styled from 'styled-components'; + +export const FormColumnLayout = styled.div` + width: 100%; + grid-column: 1 / 4; + display: grid; + grid-gap: 20px; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + + @media (min-width: 1210px) { + grid-template-columns: repeat(3, 1fr); + } +`; + +export const FormFullWidthLayout = styled.div` + grid-column: 1 / -1; + display: grid; + grid-template-columns: 1fr; + grid-gap: 20px; +`; + +export const FormCheckboxLayout = styled.div` + display: flex; + justify-content: flex-start; + flex-wrap: wrap; + + & > * { + margin-right: 30px; + margin-bottom: 10px; + } +`; + +export const SubFormLayout = styled.div` + grid-column: 1 / -1; + background-color: #f5f5f5; + margin: 0 -24px; + padding: 24px; + + & > .pf-c-title { + --pf-c-title--m-md--FontWeight: 700; + grid-column: 1 / -1; + margin-bottom: 20px; + } +`; diff --git a/awx/ui_next/src/components/FormLayout/index.js b/awx/ui_next/src/components/FormLayout/index.js new file mode 100644 index 0000000000..208a99d993 --- /dev/null +++ b/awx/ui_next/src/components/FormLayout/index.js @@ -0,0 +1,6 @@ +export { + FormColumnLayout, + FormFullWidthLayout, + FormCheckboxLayout, + SubFormLayout, +} from './FormLayout'; diff --git a/awx/ui_next/src/components/FormRow/FormRow.jsx b/awx/ui_next/src/components/FormRow/FormRow.jsx deleted file mode 100644 index a28913dd37..0000000000 --- a/awx/ui_next/src/components/FormRow/FormRow.jsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; -import styled from 'styled-components'; - -const Row = styled.div` - display: grid; - grid-gap: 20px; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); -`; -export default function FormRow({ children, className }) { - return {children}; -} diff --git a/awx/ui_next/src/components/FormRow/FormRow.test.jsx b/awx/ui_next/src/components/FormRow/FormRow.test.jsx deleted file mode 100644 index f913ef8c3c..0000000000 --- a/awx/ui_next/src/components/FormRow/FormRow.test.jsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; -import { mount } from 'enzyme'; - -import FormRow from './FormRow'; - -describe('FormRow', () => { - test('renders the expected content', () => { - const wrapper = mount(); - expect(wrapper).toHaveLength(1); - }); -}); diff --git a/awx/ui_next/src/components/FormRow/index.js b/awx/ui_next/src/components/FormRow/index.js deleted file mode 100644 index 8f063b5ff2..0000000000 --- a/awx/ui_next/src/components/FormRow/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from './FormRow'; diff --git a/awx/ui_next/src/screens/Host/shared/HostForm.jsx b/awx/ui_next/src/screens/Host/shared/HostForm.jsx index fd947f839d..66ffe6759f 100644 --- a/awx/ui_next/src/screens/Host/shared/HostForm.jsx +++ b/awx/ui_next/src/screens/Host/shared/HostForm.jsx @@ -8,12 +8,12 @@ import { t } from '@lingui/macro'; import { Form } from '@patternfly/react-core'; -import FormRow from '@components/FormRow'; import FormField, { FormSubmitError } from '@components/FormField'; import FormActionGroup from '@components/FormActionGroup/FormActionGroup'; import { VariablesField } from '@components/CodeMirrorInput'; import { required } from '@util/validators'; import { InventoryLookup } from '@components/Lookup'; +import { FormColumnLayout } from '@components/FormLayout'; function HostFormFields({ host, i18n }) { const [inventory, setInventory] = useState( @@ -30,43 +30,43 @@ function HostFormFields({ host, i18n }) { return ( <> - - - {hostAddMatch && ( - + + {hostAddMatch && ( + inventoryHelpers.setTouched()} - tooltip={i18n._( - t`Select the inventory that this host will belong to.` - )} + tooltip={i18n._( + t`Select the inventory that this host will belong to.` + )} isValid={!inventoryMeta.touched || !inventoryMeta.error} helperTextInvalid={inventoryMeta.error} - onChange={value => { + onChange={value => { inventoryHelpers.setValuealue(value.id); - setInventory(value); - }} - required + setInventory(value); + }} + required touched={inventoryMeta.touched} error={inventoryMeta.error} - /> - )} - + /> + )} + > ); } @@ -84,12 +84,14 @@ function HostForm({ handleSubmit, host, submitError, handleCancel, ...rest }) { > {formik => ( + - - + + + )} diff --git a/awx/ui_next/src/screens/Inventory/shared/InventoryForm.jsx b/awx/ui_next/src/screens/Inventory/shared/InventoryForm.jsx index ee6d4667c9..883b151044 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventoryForm.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventoryForm.jsx @@ -12,6 +12,7 @@ import { required } from '@util/validators'; import InstanceGroupsLookup from '@components/Lookup/InstanceGroupsLookup'; import OrganizationLookup from '@components/Lookup/OrganizationLookup'; import CredentialLookup from '@components/Lookup/CredentialLookup'; +import { FormColumnLayout } from '@components/FormLayout'; function InventoryFormFields({ i18n, credentialTypeId }) { const [organizationField, organizationMeta, organizationHelpers] = useField({ @@ -27,52 +28,52 @@ function InventoryFormFields({ i18n, credentialTypeId }) { const insightsCredentialHelpers = insightsCredentialFieldArr[2]; return ( <> - - - + + organizationHelpers.setTouched()} - onChange={value => { + onChange={value => { organizationHelpers.setValue(value); - }} + }} value={organizationField.value} touched={organizationMeta.touched} error={organizationMeta.error} - required - /> - + insightsCredentialHelpers.setValue(value)} value={insightsCredentialField.value} - /> - + { + onChange={value => { instanceGroupsHelpers.setValue(value); - }} - /> - + }} + /> + > ); } @@ -108,13 +109,14 @@ function InventoryForm({ > {formik => ( + - + )} diff --git a/awx/ui_next/src/screens/Inventory/shared/InventoryGroupForm.jsx b/awx/ui_next/src/screens/Inventory/shared/InventoryGroupForm.jsx index ccec0375ac..09b461a4be 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventoryGroupForm.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventoryGroupForm.jsx @@ -6,11 +6,11 @@ import { Form, Card } from '@patternfly/react-core'; import { t } from '@lingui/macro'; import { CardBody } from '@components/Card'; -import FormRow from '@components/FormRow'; import FormField from '@components/FormField'; import FormActionGroup from '@components/FormActionGroup/FormActionGroup'; import { VariablesField } from '@components/CodeMirrorInput'; import { required } from '@util/validators'; +import { FormColumnLayout } from '@components/FormLayout'; function InventoryGroupForm({ i18n, @@ -31,7 +31,7 @@ function InventoryGroupForm({ {formik => ( - + - - - - - {error ? error : null} + + {error ? error : null} + )} diff --git a/awx/ui_next/src/screens/Organization/shared/OrganizationForm.jsx b/awx/ui_next/src/screens/Organization/shared/OrganizationForm.jsx index d7b32f32cc..f8a4b02205 100644 --- a/awx/ui_next/src/screens/Organization/shared/OrganizationForm.jsx +++ b/awx/ui_next/src/screens/Organization/shared/OrganizationForm.jsx @@ -11,12 +11,12 @@ import { ConfigContext } from '@contexts/Config'; import AnsibleSelect from '@components/AnsibleSelect'; import ContentError from '@components/ContentError'; import ContentLoading from '@components/ContentLoading'; -import FormRow from '@components/FormRow'; import FormField, { FormSubmitError } from '@components/FormField'; import FormActionGroup from '@components/FormActionGroup/FormActionGroup'; import { InstanceGroupsLookup } from '@components/Lookup/'; import { getAddedAndRemoved } from '@util/lists'; import { required, minMaxValue } from '@util/validators'; +import { FormColumnLayout } from '@components/FormLayout'; function OrganizationFormFields({ i18n, @@ -166,6 +166,7 @@ function OrganizationForm({ > {formik => ( + + )} diff --git a/awx/ui_next/src/screens/Project/shared/ProjectForm.jsx b/awx/ui_next/src/screens/Project/shared/ProjectForm.jsx index e18eb7fa3a..220462f98f 100644 --- a/awx/ui_next/src/screens/Project/shared/ProjectForm.jsx +++ b/awx/ui_next/src/screens/Project/shared/ProjectForm.jsx @@ -14,11 +14,10 @@ import FormField, { FieldTooltip, FormSubmitError, } from '@components/FormField'; -import FormRow from '@components/FormRow'; import OrganizationLookup from '@components/Lookup/OrganizationLookup'; import { CredentialTypesAPI, ProjectsAPI } from '@api'; import { required } from '@util/validators'; -import styled from 'styled-components'; +import { FormColumnLayout, SubFormLayout } from '@components/FormLayout'; import { GitSubForm, HgSubForm, @@ -146,147 +145,145 @@ function ProjectFormFields({ return ( <> - - - + + organizationHelpers.setTouched()} - onChange={value => { + onChange={value => { organizationHelpers.setValue(value.id); - setOrganization(value); - }} - value={organization} - required - /> - + - + { - if (label === 'Manual') { - value = 'manual'; - } - return { - label, - value, - key: value, - }; - }), - ]} - onChange={(event, value) => { + id="scm_type" + data={[ + { + value: '', + key: '', + label: i18n._(t`Choose an SCM Type`), + isDisabled: true, + }, + ...scmTypeOptions.map(([value, label]) => { + if (label === 'Manual') { + value = 'manual'; + } + return { + label, + value, + key: value, + }; + }), + ]} + onChange={(event, value) => { scmTypeHelpers.setValue(value); resetScmTypeFields(value, formik); - }} - /> - - {formik.values.scm_type !== '' && ( - - - {i18n._(t`Type Details`)} - - { - { - manual: ( - - ), - git: ( - + + {formik.values.scm_type !== '' && ( + + {i18n._(t`Type Details`)} + + { + { + manual: ( + + ), + git: ( + - ), - hg: ( - + ), + hg: ( + - ), - svn: ( - + ), + svn: ( + - ), - insights: ( - + ), + insights: ( + - ), - }[formik.values.scm_type] - } - - )} - - {({ custom_virtualenvs }) => - custom_virtualenvs && - custom_virtualenvs.length > 1 && ( - - {({ field }) => ( - - - + ), + }[formik.values.scm_type] + } + + + )} + + {({ custom_virtualenvs }) => + custom_virtualenvs && + custom_virtualenvs.length > 1 && ( + + + datum !== '/venv/ansible/') - .map(datum => ({ - label: datum, - value: datum, - key: datum, - })), - ]} + value: '/venv/ansible/', + key: 'default', + }, + ...custom_virtualenvs + .filter(datum => datum !== '/venv/ansible/') + .map(datum => ({ + label: datum, + value: datum, + key: datum, + })), + ]} {...venvField} - /> - - ) - } - + /> + + ) + } + > ); } @@ -373,6 +370,7 @@ function ProjectForm({ i18n, project, submitError, ...props }) { onSubmit={formik.handleSubmit} css="padding: 0 24px" > + - - + + + )} diff --git a/awx/ui_next/src/screens/Project/shared/ProjectSubForms/SharedFields.jsx b/awx/ui_next/src/screens/Project/shared/ProjectSubForms/SharedFields.jsx index 199c1b2d2a..dca5a88d96 100644 --- a/awx/ui_next/src/screens/Project/shared/ProjectSubForms/SharedFields.jsx +++ b/awx/ui_next/src/screens/Project/shared/ProjectSubForms/SharedFields.jsx @@ -6,12 +6,10 @@ import CredentialLookup from '@components/Lookup/CredentialLookup'; import FormField, { CheckboxField } from '@components/FormField'; import { required } from '@util/validators'; import { FormGroup, Title } from '@patternfly/react-core'; -import styled from 'styled-components'; - -export const SubFormTitle = styled(Title)` - --pf-c-title--m-md--FontWeight: 700; - grid-column: 1 / -1; -`; +import { + FormCheckboxLayout, + FormFullWidthLayout, +} from '@components/FormLayout'; export const UrlFormField = withI18n()(({ i18n, tooltip }) => ( { - onCredentialSelection('scm', value); + { + onCredentialSelection('scm', value); credHelpers.setValue(value ? value.id : ''); - }} - /> + }} + /> ); } ); export const ScmTypeOptions = withI18n()( ({ i18n, scmUpdateOnLaunch, hideAllowOverride }) => ( - <> - - + + + {!hideAllowOverride && ( @@ -101,15 +95,16 @@ export const ScmTypeOptions = withI18n()( label={i18n._(t`Allow Branch Override`)} tooltip={i18n._( t`Allow changing the SCM branch or revision in a job - template that uses this project.` + template that uses this project.` )} /> )} - + + {scmUpdateOnLaunch && ( <> - {i18n._(t`Option Details`)} + {i18n._(t`Option Details`)} > )} - > + ) ); diff --git a/awx/ui_next/src/screens/Project/shared/ProjectSubForms/index.js b/awx/ui_next/src/screens/Project/shared/ProjectSubForms/index.js index 51386fe017..022673187f 100644 --- a/awx/ui_next/src/screens/Project/shared/ProjectSubForms/index.js +++ b/awx/ui_next/src/screens/Project/shared/ProjectSubForms/index.js @@ -1,4 +1,3 @@ -export { SubFormTitle } from './SharedFields'; export { default as GitSubForm } from './GitSubForm'; export { default as HgSubForm } from './HgSubForm'; export { default as InsightsSubForm } from './InsightsSubForm'; diff --git a/awx/ui_next/src/screens/Team/shared/TeamForm.jsx b/awx/ui_next/src/screens/Team/shared/TeamForm.jsx index d5ba307fa2..f515074150 100644 --- a/awx/ui_next/src/screens/Team/shared/TeamForm.jsx +++ b/awx/ui_next/src/screens/Team/shared/TeamForm.jsx @@ -8,6 +8,7 @@ import FormActionGroup from '@components/FormActionGroup/FormActionGroup'; import FormField, { FormSubmitError } from '@components/FormField'; import OrganizationLookup from '@components/Lookup/OrganizationLookup'; import { required } from '@util/validators'; +import { FormColumnLayout } from '@components/FormLayout'; function TeamFormFields(props) { const { team, i18n } = props; @@ -23,31 +24,31 @@ function TeamFormFields(props) { return ( <> - - - + + orgHelpers.setTouched('organization')} - onChange={value => { + onChange={value => { orgHelpers.setValue(value.id); - setOrganization(value); - }} - value={organization} - required - /> + setOrganization(value); + }} + value={organization} + required + /> > ); } @@ -68,14 +69,15 @@ function TeamForm(props) { + - - + + + )} diff --git a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx index 1e3dd2caed..db1229cbe1 100644 --- a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx +++ b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx @@ -22,10 +22,13 @@ import FormField, { FormSubmitError, } from '@components/FormField'; import FieldWithPrompt from '@components/FieldWithPrompt'; -import FormRow from '@components/FormRow'; +import { + FormColumnLayout, + FormFullWidthLayout, + FormCheckboxLayout, +} from '@components/FormLayout'; import CollapsibleSection from '@components/CollapsibleSection'; import { required } from '@util/validators'; -import styled from 'styled-components'; import { JobTemplate } from '@types'; import { InventoryLookup, @@ -37,17 +40,6 @@ import { JobTemplatesAPI, ProjectsAPI } from '@api'; import LabelSelect from './LabelSelect'; import PlaybookSelect from './PlaybookSelect'; -const GridFormGroup = styled(FormGroup)` - & > label { - grid-column: 1 / -1; - } - - && { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); - } -`; - class JobTemplateForm extends Component { static propTypes = { template: JobTemplate, @@ -211,9 +203,10 @@ class JobTemplateForm extends Component { } const AdvancedFieldsWrapper = template.isNew ? CollapsibleSection : 'div'; + return ( - + - - - - {({ field }) => ( - - - setFieldValue('labels', labels)} - onError={this.setContentError} - /> - - )} - - - - - {({ field }) => ( - - setFieldValue('credentials', newCredentials) - } - onError={this.setContentError} - tooltip={i18n._( - t`Select credentials that allow Tower to access the nodes this job will be ran against. You can only select one credential of each type. For machine credentials (SSH), checking "Prompt on launch" without selecting credentials will require you to select a machine credential at run time. If you select credentials and check "Prompt on launch", the selected credential(s) become the defaults that can be updated at run time.` - )} - /> - )} - - - - - - {i18n._(t`The number of parallel or simultaneous - processes to use while executing the playbook. An empty value, - or a value less than 1 will use the Ansible default which is - usually 5. The default number of forks can be overwritten - with a change to`)}{' '} - ansible.cfg.{' '} - {i18n._(t`Refer to the Ansible documentation for details - about the configuration file.`)} - - } - /> - - + + {({ field }) => ( - + - setFieldValue('labels', labels)} + onError={this.setContentError} /> )} - - - - {({ field, form }) => ( - - - - - form.setFieldValue(field.name, checked) - } + + {({ field }) => ( + + setFieldValue('credentials', newCredentials) + } + onError={this.setContentError} + tooltip={i18n._( + t`Select credentials that allow Tower to access the nodes this job will be ran against. You can only select one credential of each type. For machine credentials (SSH), checking "Prompt on launch" without selecting credentials will require you to select a machine credential at run time. If you select credentials and check "Prompt on launch", the selected credential(s) become the defaults that can be updated at run time.` + )} + /> + )} + + + + + {i18n._(t`The number of parallel or simultaneous + processes to use while executing the playbook. An empty value, + or a value less than 1 will use the Ansible default which is + usually 5. The default number of forks can be overwritten + with a change to`)}{' '} + ansible.cfg.{' '} + {i18n._(t`Refer to the Ansible documentation for details + about the configuration file.`)} + + } + /> + + + {({ field }) => ( + + + + + )} + + + + + {({ field, form }) => ( + + + + + form.setFieldValue(field.name, checked) + } + /> + + + )} + + + + {({ field, form }) => ( + + form.setFieldValue(field.name, value) + } + tooltip={i18n._(t`Select the Instance Groups for this Organization + to run on.`)} + /> + )} + + + {({ field, form }) => ( + + + + form.setFieldValue(field.name, value) + } + /> + + )} + + + {({ field, form }) => ( + + + + form.setFieldValue(field.name, value) + } + /> + + )} + + + + + + {i18n._(t`Provisioning Callbacks`)} + + + + } + id="option-callbacks" + isChecked={allowCallbacks} + onChange={checked => { + this.setState({ allowCallbacks: checked }); + }} + /> + + + + + + {allowCallbacks && ( + <> + {callbackUrl && ( + + + + )} + - - - )} - - - - {({ field, form }) => ( - form.setFieldValue(field.name, value)} - tooltip={i18n._(t`Select the Instance Groups for this Organization - to run on.`)} - /> - )} - - - {({ field, form }) => ( - - - form.setFieldValue(field.name, value)} - /> - - )} - - - {({ field, form }) => ( - - - form.setFieldValue(field.name, value)} - /> - - )} - - - - - {i18n._(t`Provisioning Callbacks`)} - - - - } - id="option-callbacks" - isChecked={allowCallbacks} - onChange={checked => { - this.setState({ allowCallbacks: checked }); - }} - /> - - - - - - {callbackUrl && ( - - - - )} - - - - - - + > + )} + + + + + + ); } diff --git a/awx/ui_next/src/screens/User/shared/UserForm.jsx b/awx/ui_next/src/screens/User/shared/UserForm.jsx index 3a15e82fda..d51fc0cf9b 100644 --- a/awx/ui_next/src/screens/User/shared/UserForm.jsx +++ b/awx/ui_next/src/screens/User/shared/UserForm.jsx @@ -10,9 +10,9 @@ import FormField, { PasswordField, FormSubmitError, } from '@components/FormField'; -import FormRow from '@components/FormRow'; import OrganizationLookup from '@components/Lookup/OrganizationLookup'; import { required, requiredEmail } from '@util/validators'; +import { FormColumnLayout } from '@components/FormLayout'; function UserFormFields({ user, i18n }) { const [organization, setOrganization] = useState(null); @@ -41,7 +41,7 @@ function UserFormFields({ user, i18n }) { const organizationFieldArr = useField({ name: 'organization', validate: required(i18n._(t`Select a value for this field`), i18n), - }); + }); const organizationMeta = organizationFieldArr[1]; const organizationHelpers = organizationFieldArr[2]; @@ -99,6 +99,7 @@ function UserFormFields({ user, i18n }) { type="text" /> {!user.id && ( +<<<<<<< HEAD +======= + organizationHelpers.setTouched()} + onChange={value => { + organizationHelpers.setValue(value.id); + setOrganization(value); + }} + value={organization} + required + /> + )} + + + +>>>>>>> update forms from FormRow to using FormLayout components > - ); + ); } function UserForm({ user, handleCancel, handleSubmit, submitError, i18n }) { @@ -173,12 +202,14 @@ function UserForm({ user, handleCancel, handleSubmit, submitError, i18n }) { > {formik => ( + - - + + + )}
ansible.cfg