diff --git a/awx/ui_next/src/components/Lookup/OrganizationLookup.jsx b/awx/ui_next/src/components/Lookup/OrganizationLookup.jsx index 3fb443426e..8252c9035c 100644 --- a/awx/ui_next/src/components/Lookup/OrganizationLookup.jsx +++ b/awx/ui_next/src/components/Lookup/OrganizationLookup.jsx @@ -30,6 +30,7 @@ function OrganizationLookup({ history, autoPopulate, isDisabled, + helperText, }) { const autoPopulateLookup = useAutoPopulateLookup(onChange); @@ -79,6 +80,7 @@ function OrganizationLookup({ isRequired={required} validated={isValid ? 'default' : 'error'} label={i18n._(t`Organization`)} + helperText={helperText} > - + + {({ me }) => ( + + )} + diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentAdd/ExecutionEnvironmentAdd.test.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentAdd/ExecutionEnvironmentAdd.test.jsx index 781501b19e..5396746223 100644 --- a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentAdd/ExecutionEnvironmentAdd.test.jsx +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentAdd/ExecutionEnvironmentAdd.test.jsx @@ -8,6 +8,11 @@ import ExecutionEnvironmentAdd from './ExecutionEnvironmentAdd'; jest.mock('../../../api'); +const mockMe = { + is_superuser: true, + is_system_auditor: false, +}; + const executionEnvironmentData = { credential: 4, description: 'A simple EE', @@ -29,7 +34,7 @@ describe('', () => { initialEntries: ['/execution_environments'], }); await act(async () => { - wrapper = mountWithContexts(, { + wrapper = mountWithContexts(, { context: { router: { history } }, }); }); diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentEdit/ExecutionEnvironmentEdit.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentEdit/ExecutionEnvironmentEdit.jsx index 6d8cbc9520..ea4943b2da 100644 --- a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentEdit/ExecutionEnvironmentEdit.jsx +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentEdit/ExecutionEnvironmentEdit.jsx @@ -4,6 +4,7 @@ import { useHistory } from 'react-router-dom'; import { CardBody } from '../../../components/Card'; import { ExecutionEnvironmentsAPI } from '../../../api'; import ExecutionEnvironmentForm from '../shared/ExecutionEnvironmentForm'; +import { Config } from '../../../contexts/Config'; function ExecutionEnvironmentEdit({ executionEnvironment }) { const history = useHistory(); @@ -15,6 +16,7 @@ function ExecutionEnvironmentEdit({ executionEnvironment }) { await ExecutionEnvironmentsAPI.update(executionEnvironment.id, { ...values, credential: values.credential ? values.credential.id : null, + organization: values.organization ? values.organization.id : null, }); history.push(detailsUrl); } catch (error) { @@ -27,12 +29,17 @@ function ExecutionEnvironmentEdit({ executionEnvironment }) { }; return ( - + + {({ me }) => ( + + )} + ); } diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentEdit/ExecutionEnvironmentEdit.test.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentEdit/ExecutionEnvironmentEdit.test.jsx index 2d4f916aba..94eff7616f 100644 --- a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentEdit/ExecutionEnvironmentEdit.test.jsx +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentEdit/ExecutionEnvironmentEdit.test.jsx @@ -9,6 +9,11 @@ import ExecutionEnvironmentEdit from './ExecutionEnvironmentEdit'; jest.mock('../../../api'); +const mockMe = { + is_superuser: true, + is_system_auditor: false, +}; + const executionEnvironmentData = { id: 42, credential: { id: 4 }, @@ -31,6 +36,7 @@ describe('', () => { wrapper = mountWithContexts( , { context: { router: { history } }, @@ -53,6 +59,7 @@ describe('', () => { expect(ExecutionEnvironmentsAPI.update).toHaveBeenCalledWith(42, { ...updateExecutionEnvironmentData, credential: null, + organization: null, }); }); diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/shared/ExecutionEnvironmentForm.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/shared/ExecutionEnvironmentForm.jsx index a5170ae3bb..5d7a16d217 100644 --- a/awx/ui_next/src/screens/ExecutionEnvironment/shared/ExecutionEnvironmentForm.jsx +++ b/awx/ui_next/src/screens/ExecutionEnvironment/shared/ExecutionEnvironmentForm.jsx @@ -1,18 +1,42 @@ -import React from 'react'; +import React, { useCallback } from 'react'; import { func, shape } from 'prop-types'; -import { Formik, useField } from 'formik'; +import { Formik, useField, useFormikContext } from 'formik'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; - import { Form } from '@patternfly/react-core'; -import FormField, { FormSubmitError } from '../../../components/FormField'; -import FormActionGroup from '../../../components/FormActionGroup'; -import CredentialLookup from '../../../components/Lookup/CredentialLookup'; -import { url } from '../../../util/validators'; -import { FormColumnLayout } from '../../../components/FormLayout'; -function ExecutionEnvironmentFormFields({ i18n }) { - const [credentialField, , credentialHelpers] = useField('credential'); +import CredentialLookup from '../../../components/Lookup/CredentialLookup'; +import FormActionGroup from '../../../components/FormActionGroup'; +import FormField, { FormSubmitError } from '../../../components/FormField'; +import { FormColumnLayout } from '../../../components/FormLayout'; +import { OrganizationLookup } from '../../../components/Lookup'; +import { required, url } from '../../../util/validators'; + +function ExecutionEnvironmentFormFields({ i18n, me, executionEnvironment }) { + const [credentialField] = useField('credential'); + const [organizationField, organizationMeta, organizationHelpers] = useField({ + name: 'organization', + validate: + !me?.is_superuser && + required(i18n._(t`Select a value for this field`), i18n), + }); + + const { setFieldValue } = useFormikContext(); + + const onCredentialChange = useCallback( + value => { + setFieldValue('credential', value); + }, + [setFieldValue] + ); + + const onOrganizationChange = useCallback( + value => { + setFieldValue('organization', value); + }, + [setFieldValue] + ); + return ( <> + organizationHelpers.setTouched()} + onChange={onOrganizationChange} + value={organizationField.value} + required={!me.is_superuser} + helperText={ + me?.is_superuser + ? i18n._( + t`Leave this field blank to make the execution environment globally available.` + ) + : null + } + autoPopulate={!me?.is_superuser ? !executionEnvironment?.id : null} + /> + credentialHelpers.setValue(value)} + onChange={onCredentialChange} value={credentialField.value} /> @@ -46,19 +87,21 @@ function ExecutionEnvironmentForm({ onSubmit, onCancel, submitError, + me, ...rest }) { const initialValues = { image: executionEnvironment.image || '', description: executionEnvironment.description || '', - credential: executionEnvironment?.summary_fields?.credential || null, + credential: executionEnvironment.summary_fields?.credential || null, + organization: executionEnvironment.summary_fields?.organization || null, }; return ( onSubmit(values)}> {formik => (
- + {submitError && } ', () => { onCancel={onCancel} onSubmit={onSubmit} executionEnvironment={executionEnvironment} + me={mockMe} /> ); }); @@ -75,8 +81,8 @@ describe('', () => { expect(onSubmit).toHaveBeenCalledTimes(1); }); - test('should update form values', () => { - act(() => { + test('should update form values', async () => { + await act(async () => { wrapper.find('input#execution-environment-image').simulate('change', { target: { value: 'https://registry.com/image/container2', @@ -93,8 +99,19 @@ describe('', () => { id: 99, name: 'credential', }); + + wrapper.find('OrganizationLookup').invoke('onBlur')(); + wrapper.find('OrganizationLookup').invoke('onChange')({ + id: 3, + name: 'organization', + }); }); + wrapper.update(); + expect(wrapper.find('OrganizationLookup').prop('value')).toEqual({ + id: 3, + name: 'organization', + }); expect( wrapper.find('input#execution-environment-image').prop('value') ).toEqual('https://registry.com/image/container2');