diff --git a/awx/api/serializers.py b/awx/api/serializers.py index eb13f150d3..941b465157 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1365,7 +1365,7 @@ class ExecutionEnvironmentSerializer(BaseSerializer): class Meta: model = ExecutionEnvironment - fields = ('*', 'organization', 'image', 'managed_by_tower', 'credential') + fields = ('*', 'organization', 'image', 'managed_by_tower', 'credential', 'container_options') def get_related(self, obj): res = super(ExecutionEnvironmentSerializer, self).get_related(obj) 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 5396746223..ede58e5d58 100644 --- a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentAdd/ExecutionEnvironmentAdd.test.jsx +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentAdd/ExecutionEnvironmentAdd.test.jsx @@ -2,7 +2,10 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { createMemoryHistory } from 'history'; -import { mountWithContexts } from '../../../../testUtils/enzymeHelpers'; +import { + mountWithContexts, + waitForElement, +} from '../../../../testUtils/enzymeHelpers'; import { ExecutionEnvironmentsAPI } from '../../../api'; import ExecutionEnvironmentAdd from './ExecutionEnvironmentAdd'; @@ -14,11 +17,30 @@ const mockMe = { }; const executionEnvironmentData = { + name: 'Test EE', credential: 4, description: 'A simple EE', image: 'https://registry.com/image/container', + container_options: 'one', }; +const mockOptions = { + data: { + actions: { + POST: { + container_options: { + choices: [ + ['one', 'One'], + ['two', 'Two'], + ['three', 'Three'], + ], + }, + }, + }, + }, +}; + +ExecutionEnvironmentsAPI.readOptions.mockResolvedValue(mockOptions); ExecutionEnvironmentsAPI.create.mockResolvedValue({ data: { id: 42, @@ -61,6 +83,8 @@ describe('', () => { }); test('handleCancel should return the user back to the execution environments list', async () => { + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); + wrapper.find('Button[aria-label="Cancel"]').simulate('click'); expect(history.location.pathname).toEqual('/execution_environments'); }); diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.jsx index abb6cf5ddc..64925df1cb 100644 --- a/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.jsx +++ b/awx/ui_next/src/screens/ExecutionEnvironment/ExecutionEnvironmentDetails/ExecutionEnvironmentDetails.jsx @@ -13,11 +13,18 @@ import { UserDateDetail, } from '../../../components/DetailList'; import useRequest, { useDismissableError } from '../../../util/useRequest'; +import { toTitleCase } from '../../../util/strings'; import { ExecutionEnvironmentsAPI } from '../../../api'; function ExecutionEnvironmentDetails({ executionEnvironment, i18n }) { const history = useHistory(); - const { id, image, description } = executionEnvironment; + const { + id, + name, + image, + description, + container_options, + } = executionEnvironment; const { request: deleteExecutionEnvironment, @@ -35,12 +42,25 @@ function ExecutionEnvironmentDetails({ executionEnvironment, i18n }) { return ( + + {executionEnvironment.summary_fields.credential && ( ', () => { let wrapper; let history; diff --git a/awx/ui_next/src/screens/ExecutionEnvironment/shared/ExecutionEnvironmentForm.jsx b/awx/ui_next/src/screens/ExecutionEnvironment/shared/ExecutionEnvironmentForm.jsx index 5d7a16d217..4dd1b695a4 100644 --- a/awx/ui_next/src/screens/ExecutionEnvironment/shared/ExecutionEnvironmentForm.jsx +++ b/awx/ui_next/src/screens/ExecutionEnvironment/shared/ExecutionEnvironmentForm.jsx @@ -1,18 +1,28 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, useEffect } from 'react'; import { func, shape } from 'prop-types'; import { Formik, useField, useFormikContext } from 'formik'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; -import { Form } from '@patternfly/react-core'; +import { Form, FormGroup } from '@patternfly/react-core'; +import { ExecutionEnvironmentsAPI } from '../../../api'; import CredentialLookup from '../../../components/Lookup/CredentialLookup'; import FormActionGroup from '../../../components/FormActionGroup'; import FormField, { FormSubmitError } from '../../../components/FormField'; +import AnsibleSelect from '../../../components/AnsibleSelect'; import { FormColumnLayout } from '../../../components/FormLayout'; import { OrganizationLookup } from '../../../components/Lookup'; +import ContentError from '../../../components/ContentError'; +import ContentLoading from '../../../components/ContentLoading'; import { required, url } from '../../../util/validators'; +import useRequest from '../../../util/useRequest'; -function ExecutionEnvironmentFormFields({ i18n, me, executionEnvironment }) { +function ExecutionEnvironmentFormFields({ + i18n, + me, + options, + executionEnvironment, +}) { const [credentialField] = useField('credential'); const [organizationField, organizationMeta, organizationHelpers] = useField({ name: 'organization', @@ -37,8 +47,28 @@ function ExecutionEnvironmentFormFields({ i18n, me, executionEnvironment }) { [setFieldValue] ); + const [ + containerOptionsField, + containerOptionsMeta, + containerOptionsHelpers, + ] = useField({ + name: 'container_options', + }); + + const containerPullChoices = options?.actions?.POST?.container_options?.choices.map( + ([value, label]) => ({ value, label, key: value }) + ); + return ( <> + + + { + containerOptionsHelpers.setValue(value); + }} + /> + { + const res = await ExecutionEnvironmentsAPI.readOptions(); + const { data } = res; + return data; + }, []), + null + ); + + useEffect(() => { + fetchOptions(); + }, [fetchOptions]); + + if (isLoading || !options) { + return ; + } + + if (error) { + return ; + } + const initialValues = { + name: executionEnvironment.name || '', image: executionEnvironment.image || '', + container_options: executionEnvironment?.container_options || '', description: executionEnvironment.description || '', credential: executionEnvironment.summary_fields?.credential || null, organization: executionEnvironment.summary_fields?.organization || null, @@ -101,7 +178,12 @@ function ExecutionEnvironmentForm({ {formik => (
- + {submitError && } ', () => { let wrapper; let onCancel; @@ -46,16 +68,19 @@ describe('', () => { beforeEach(async () => { onCancel = jest.fn(); onSubmit = jest.fn(); + ExecutionEnvironmentsAPI.readOptions.mockResolvedValue(mockOptions); await act(async () => { wrapper = mountWithContexts( ); }); + await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); }); afterEach(() => { @@ -83,6 +108,12 @@ describe('', () => { test('should update form values', async () => { await act(async () => { + wrapper.find('input#execution-environment-image').simulate('change', { + target: { + value: 'Updated EE Name', + name: 'name', + }, + }); wrapper.find('input#execution-environment-image').simulate('change', { target: { value: 'https://registry.com/image/container2',