diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentEdit/ExecutionEnvironmentEdit.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentEdit/ExecutionEnvironmentEdit.jsx index ea4943b2da..6c66f84170 100644 --- a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentEdit/ExecutionEnvironmentEdit.jsx +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentEdit/ExecutionEnvironmentEdit.jsx @@ -37,6 +37,7 @@ function ExecutionEnvironmentEdit({ executionEnvironment }) { submitError={submitError} onCancel={handleCancel} me={me || {}} + isOrgLookupDisabled /> )} diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/shared/ExecutionEnvironmentForm.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/shared/ExecutionEnvironmentForm.jsx index bb2a8977fa..30cc2bb350 100644 --- a/awx/ui_next/src/screens/ExecutionEnvironment/shared/ExecutionEnvironmentForm.jsx +++ b/awx/ui_next/src/screens/ExecutionEnvironment/shared/ExecutionEnvironmentForm.jsx @@ -1,9 +1,9 @@ -import React, { useCallback, useEffect } from 'react'; -import { func, shape } from 'prop-types'; +import React, { useCallback, useEffect, useRef } from 'react'; +import { func, shape, bool } from 'prop-types'; import { Formik, useField, useFormikContext } from 'formik'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; -import { Form, FormGroup } from '@patternfly/react-core'; +import { Form, FormGroup, Tooltip } from '@patternfly/react-core'; import { ExecutionEnvironmentsAPI } from '../../../api'; import CredentialLookup from '../../../components/Lookup/CredentialLookup'; @@ -22,6 +22,7 @@ function ExecutionEnvironmentFormFields({ me, options, executionEnvironment, + isOrgLookupDisabled, }) { const [credentialField, credentialMeta, credentialHelpers] = useField( 'credential' @@ -33,6 +34,8 @@ function ExecutionEnvironmentFormFields({ required(i18n._(t`Select a value for this field`), i18n), }); + const isGloballyAvailable = useRef(!organizationField.value); + const { setFieldValue } = useFormikContext(); const onCredentialChange = useCallback( @@ -61,6 +64,30 @@ function ExecutionEnvironmentFormFields({ ([value, label]) => ({ value, label, key: value }) ); + const renderOrganizationLookup = () => { + return ( + organizationHelpers.setTouched()} + onChange={onOrganizationChange} + value={organizationField.value} + required={!me.is_superuser} + helperText={ + me?.is_superuser && + ((!isOrgLookupDisabled && isGloballyAvailable) || + organizationField.value === null) + ? i18n._( + t`Leave this field blank to make the execution environment globally available.` + ) + : null + } + autoPopulate={!me?.is_superuser ? !executionEnvironment?.id : null} + isDisabled={!!isOrgLookupDisabled && isGloballyAvailable.current} + /> + ); + }; + 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} - /> + {isOrgLookupDisabled && isGloballyAvailable.current ? ( + + {renderOrganizationLookup()} + + ) : ( + renderOrganizationLookup() + )} {submitError && } @@ -210,11 +234,13 @@ ExecutionEnvironmentForm.propTypes = { onCancel: func.isRequired, onSubmit: func.isRequired, submitError: shape({}), + isOrgLookupDisabled: bool, }; ExecutionEnvironmentForm.defaultProps = { executionEnvironment: {}, submitError: null, + isOrgLookupDisabled: false, }; export default withI18n()(ExecutionEnvironmentForm); diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/shared/ExecutionEnvironmentForm.test.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/shared/ExecutionEnvironmentForm.test.jsx index e31390cd1b..eab62ce089 100644 --- a/awx/ui_next/src/screens/ExecutionEnvironment/shared/ExecutionEnvironmentForm.test.jsx +++ b/awx/ui_next/src/screens/ExecutionEnvironment/shared/ExecutionEnvironmentForm.test.jsx @@ -29,6 +29,45 @@ const executionEnvironment = { '/api/v2/execution_environments/16/unified_job_templates/', credential: '/api/v2/credentials/4/', }, + summary_fields: { + organization: { + id: 1, + name: 'Default', + description: '', + }, + credential: { + id: 4, + name: 'Container Registry', + description: '', + kind: 'registry', + cloud: false, + kubernetes: false, + credential_type_id: 17, + }, + }, + created: '2020-09-17T16:06:57.346128Z', + modified: '2020-09-17T16:06:57.346147Z', + description: 'A simple EE', + organization: 1, + image: 'https://registry.com/image/container', + managed_by_tower: false, + credential: 4, +}; + +const globallyAvailableEE = { + id: 17, + name: 'GEE', + type: 'execution_environment', + pull: 'one', + url: '/api/v2/execution_environments/17/', + related: { + created_by: '/api/v2/users/1/', + modified_by: '/api/v2/users/1/', + activity_stream: '/api/v2/execution_environments/16/activity_stream/', + unified_job_templates: + '/api/v2/execution_environments/16/unified_job_templates/', + credential: '/api/v2/credentials/4/', + }, summary_fields: { credential: { id: 4, @@ -180,4 +219,55 @@ describe('', () => { wrapper.find('button[aria-label="Cancel"]').invoke('onClick')(); expect(onCancel).toBeCalled(); }); + + test('globally available EE can not have organization reassigned', async () => { + let newWrapper; + await act(async () => { + newWrapper = mountWithContexts( + + ); + }); + await waitForElement(newWrapper, 'ContentLoading', el => el.length === 0); + expect(newWrapper.find('OrganizationLookup').prop('isDisabled')).toEqual( + true + ); + expect(newWrapper.find('Tooltip').prop('content')).toEqual( + 'Globally available execution environment can not be reassigned to a specific Organization' + ); + }); + + test('should allow an organization to be re-assigned as globally available EE', async () => { + let newWrapper; + await act(async () => { + newWrapper = mountWithContexts( + + ); + }); + await waitForElement(newWrapper, 'ContentLoading', el => el.length === 0); + expect(newWrapper.find('OrganizationLookup').prop('isDisabled')).toEqual( + false + ); + expect(newWrapper.find('Tooltip').length).toEqual(0); + + await act(async () => { + newWrapper.find('OrganizationLookup').invoke('onBlur')(); + newWrapper.find('OrganizationLookup').invoke('onChange')(null); + }); + newWrapper.update(); + expect(newWrapper.find('OrganizationLookup').prop('value')).toEqual(null); + }); });