From 99c7beb7c20cb3656513fc914e88eaffbcf4e744 Mon Sep 17 00:00:00 2001 From: nixocio Date: Tue, 2 Mar 2021 16:32:31 -0500 Subject: [PATCH] Remove custom virtual env Remove custom virtual from the UI. Also, surface missing-resource warnings on list items for UJTs that were using custom virtualenvs. Fix some uni-tests warnings. See: https://github.com/ansible/awx/issues/9190 Also: https://github.com/ansible/awx/issues/9207 --- .../AppContainer/AppContainer.test.jsx | 2 - .../ExecutionEnvironmentDetail.jsx | 82 +++++++++++++++++++ .../ExecutionEnvironmentDetail.test.jsx | 64 +++++++++++++++ .../ExecutionEnvironmentDetail/index.js | 1 + .../PromptInventorySourceDetail.jsx | 9 +- .../PromptDetail/PromptJobTemplateDetail.jsx | 6 ++ .../PromptDetail/PromptProjectDetail.jsx | 10 ++- .../PromptDetail/PromptProjectDetail.test.jsx | 9 +- .../PromptDetail/data.job_template.json | 9 +- .../components/PromptDetail/data.project.json | 9 +- .../TemplateList/TemplateListItem.jsx | 24 ++++++ .../InventorySourceAdd.test.jsx | 10 +-- .../InventorySourceDetail.jsx | 19 +---- .../InventorySourceDetail.test.jsx | 10 ++- .../InventorySourceListItem.jsx | 28 ++++++- .../Inventory/shared/InventorySourceForm.jsx | 37 +-------- .../shared/InventorySourceForm.test.jsx | 10 +-- .../AzureSubForm.test.jsx | 1 - .../GCESubForm.test.jsx | 1 - .../OpenStackSubForm.test.jsx | 1 - .../SCMSubForm.test.jsx | 2 - .../VMwareSubForm.test.jsx | 1 - .../VirtualizationSubForm.test.jsx | 1 - .../shared/data.inventory_source.json | 9 +- .../src/screens/Job/JobDetail/JobDetail.jsx | 7 +- .../screens/Job/JobDetail/JobDetail.test.jsx | 10 ++- .../src/screens/Job/shared/data.job.json | 9 +- .../NotificationTemplateAdd.jsx | 5 -- .../NotificationTemplateEdit.jsx | 4 - .../OrganizationAdd/OrganizationAdd.jsx | 5 -- .../OrganizationAdd/OrganizationAdd.test.jsx | 41 +--------- .../OrganizationDetail/OrganizationDetail.jsx | 20 ++--- .../OrganizationDetail.test.jsx | 1 - .../OrganizationEdit/OrganizationEdit.jsx | 4 - .../OrganizationEdit.test.jsx | 4 +- .../OrganizationList/OrganizationListItem.jsx | 36 ++++++-- .../Organization/shared/OrganizationForm.jsx | 41 +--------- .../shared/OrganizationForm.test.jsx | 46 ++--------- .../Project/ProjectDetail/ProjectDetail.jsx | 20 ++--- .../ProjectDetail/ProjectDetail.test.jsx | 10 ++- .../ProjectJobTemplatesListItem.jsx | 22 +++++ .../Project/ProjectList/ProjectListItem.jsx | 31 ++++++- .../src/screens/Project/data.project.json | 9 +- .../screens/Project/shared/ProjectForm.jsx | 39 --------- .../Project/shared/ProjectForm.test.jsx | 10 +-- .../src/screens/Team/TeamEdit/TeamEdit.jsx | 4 - .../JobTemplateEdit/JobTemplateEdit.test.jsx | 5 +- .../WorkflowJobTemplateEdit.test.jsx | 5 +- .../Template/shared/JobTemplateForm.jsx | 2 +- .../shared/WorkflowJobTemplateForm.jsx | 2 +- awx/ui_next/src/types.js | 3 +- 51 files changed, 418 insertions(+), 332 deletions(-) create mode 100644 awx/ui_next/src/components/ExecutionEnvironmentDetail/ExecutionEnvironmentDetail.jsx create mode 100644 awx/ui_next/src/components/ExecutionEnvironmentDetail/ExecutionEnvironmentDetail.test.jsx create mode 100644 awx/ui_next/src/components/ExecutionEnvironmentDetail/index.js diff --git a/awx/ui_next/src/components/AppContainer/AppContainer.test.jsx b/awx/ui_next/src/components/AppContainer/AppContainer.test.jsx index bdb67db490e6..8e6bb3507b58 100644 --- a/awx/ui_next/src/components/AppContainer/AppContainer.test.jsx +++ b/awx/ui_next/src/components/AppContainer/AppContainer.test.jsx @@ -11,14 +11,12 @@ jest.mock('../../api'); describe('', () => { const ansible_version = '111'; - const custom_virtualenvs = []; const version = '222'; beforeEach(() => { ConfigAPI.read.mockResolvedValue({ data: { ansible_version, - custom_virtualenvs, version, }, }); diff --git a/awx/ui_next/src/components/ExecutionEnvironmentDetail/ExecutionEnvironmentDetail.jsx b/awx/ui_next/src/components/ExecutionEnvironmentDetail/ExecutionEnvironmentDetail.jsx new file mode 100644 index 000000000000..593812c2d5ba --- /dev/null +++ b/awx/ui_next/src/components/ExecutionEnvironmentDetail/ExecutionEnvironmentDetail.jsx @@ -0,0 +1,82 @@ +import React from 'react'; +import { bool, string } from 'prop-types'; +import { Link } from 'react-router-dom'; +import { withI18n } from '@lingui/react'; +import { t } from '@lingui/macro'; +import { Tooltip } from '@patternfly/react-core'; +import styled from 'styled-components'; + +import { ExclamationTriangleIcon as PFExclamationTriangleIcon } from '@patternfly/react-icons'; + +import { Detail } from '../DetailList'; +import { ExecutionEnvironment } from '../../types'; + +const ExclamationTriangleIcon = styled(PFExclamationTriangleIcon)` + color: var(--pf-global--warning-color--100); + margin-left: 18px; +`; + +function ExecutionEnvironmentDetail({ + virtualEnvironment, + executionEnvironment, + isDefaultEnvironment, + i18n, +}) { + const label = isDefaultEnvironment + ? i18n._(t`Default Execution Environment`) + : i18n._(t`Execution Environment`); + + if (executionEnvironment) { + return ( + + {executionEnvironment.name} + + } + dataCy="execution-environment-detail" + /> + ); + } + if (virtualEnvironment && !executionEnvironment) { + return ( + + {i18n._(t`Missing resource`)} + + + + + + + } + dataCy="missing-execution-environment-detail" + /> + ); + } + return null; +} + +ExecutionEnvironmentDetail.propTypes = { + executionEnvironment: ExecutionEnvironment, + isDefaultEnvironment: bool, + virtualEnvironment: string, +}; + +ExecutionEnvironmentDetail.defaultProps = { + isDefaultEnvironment: false, + executionEnvironment: null, + virtualEnvironment: '', +}; + +export default withI18n()(ExecutionEnvironmentDetail); diff --git a/awx/ui_next/src/components/ExecutionEnvironmentDetail/ExecutionEnvironmentDetail.test.jsx b/awx/ui_next/src/components/ExecutionEnvironmentDetail/ExecutionEnvironmentDetail.test.jsx new file mode 100644 index 000000000000..136114b0f3a2 --- /dev/null +++ b/awx/ui_next/src/components/ExecutionEnvironmentDetail/ExecutionEnvironmentDetail.test.jsx @@ -0,0 +1,64 @@ +import React from 'react'; +import { mountWithContexts } from '../../../testUtils/enzymeHelpers'; + +import ExecutionEnvironmentDetail from './ExecutionEnvironmentDetail'; + +const mockExecutionEnvironment = { + id: 2, + name: 'Foo', + image: 'quay.io/ansible/awx-ee', + pull: 'missing', + description: '', +}; + +const virtualEnvironment = 'var/lib/awx/custom_env'; + +describe('', () => { + test('should display execution environment detail', async () => { + const wrapper = mountWithContexts( + + ); + const executionEnvironment = wrapper.find('ExecutionEnvironmentDetail'); + expect(executionEnvironment).toHaveLength(1); + expect(executionEnvironment.find('dt').text()).toEqual( + 'Execution Environment' + ); + expect(executionEnvironment.find('dd').text()).toEqual( + mockExecutionEnvironment.name + ); + }); + + test('should display execution environment detail even with a previous virtual env present', async () => { + const wrapper = mountWithContexts( + + ); + const executionEnvironment = wrapper.find('ExecutionEnvironmentDetail'); + expect(executionEnvironment).toHaveLength(1); + expect(executionEnvironment.find('dt').text()).toEqual( + 'Execution Environment' + ); + expect(executionEnvironment.find('dd').text()).toEqual( + mockExecutionEnvironment.name + ); + }); + + test('should display warning missing execution environment', async () => { + const wrapper = mountWithContexts( + + ); + const executionEnvironment = wrapper.find('ExecutionEnvironmentDetail'); + expect(executionEnvironment).toHaveLength(1); + expect(executionEnvironment.find('dt').text()).toEqual( + 'Execution Environment' + ); + expect(executionEnvironment.find('dd').text()).toEqual('Missing resource'); + expect(wrapper.find('Tooltip').prop('content')).toEqual( + `Custom virtual environment ${virtualEnvironment} must be replaced by an execution environment.` + ); + }); +}); diff --git a/awx/ui_next/src/components/ExecutionEnvironmentDetail/index.js b/awx/ui_next/src/components/ExecutionEnvironmentDetail/index.js new file mode 100644 index 000000000000..7c5efd15b45d --- /dev/null +++ b/awx/ui_next/src/components/ExecutionEnvironmentDetail/index.js @@ -0,0 +1 @@ +export { default } from './ExecutionEnvironmentDetail'; diff --git a/awx/ui_next/src/components/PromptDetail/PromptInventorySourceDetail.jsx b/awx/ui_next/src/components/PromptDetail/PromptInventorySourceDetail.jsx index 8db476f7d1fc..587c399a1ec1 100644 --- a/awx/ui_next/src/components/PromptDetail/PromptInventorySourceDetail.jsx +++ b/awx/ui_next/src/components/PromptDetail/PromptInventorySourceDetail.jsx @@ -8,6 +8,7 @@ import { Detail, DeletedDetail } from '../DetailList'; import { VariablesDetail } from '../CodeEditor'; import CredentialChip from '../CredentialChip'; import ChipGroup from '../ChipGroup'; +import ExecutionEnvironmentDetail from '../ExecutionEnvironmentDetail'; function PromptInventorySourceDetail({ i18n, resource }) { const { @@ -83,10 +84,6 @@ function PromptInventorySourceDetail({ i18n, resource }) { /> )} - {summary_fields?.source_project && ( )} + )} + diff --git a/awx/ui_next/src/components/PromptDetail/PromptProjectDetail.jsx b/awx/ui_next/src/components/PromptDetail/PromptProjectDetail.jsx index 5c04b53d1a51..0ab2d6d31f66 100644 --- a/awx/ui_next/src/components/PromptDetail/PromptProjectDetail.jsx +++ b/awx/ui_next/src/components/PromptDetail/PromptProjectDetail.jsx @@ -8,6 +8,7 @@ import { Config } from '../../contexts/Config'; import { Detail, DeletedDetail } from '../DetailList'; import CredentialChip from '../CredentialChip'; import { toTitleCase } from '../../util/strings'; +import ExecutionEnvironmentDetail from '../ExecutionEnvironmentDetail'; function PromptProjectDetail({ i18n, resource }) { const { @@ -64,6 +65,11 @@ function PromptProjectDetail({ i18n, resource }) { ) : ( )} + - {({ project_base_dir }) => ( { assertDetail(wrapper, 'Source Control Branch', 'foo'); assertDetail(wrapper, 'Source Control Refspec', 'refs/'); assertDetail(wrapper, 'Cache Timeout', '3 Seconds'); - assertDetail(wrapper, 'Ansible Environment', 'mock virtual env'); assertDetail(wrapper, 'Project Base Path', 'dir/foo/bar'); assertDetail(wrapper, 'Playbook Directory', '_6__demo_project'); assertDetail(wrapper, 'Source Control Credential', 'Scm: mock scm'); + const executionEnvironment = wrapper.find('ExecutionEnvironmentDetail'); + expect(executionEnvironment).toHaveLength(1); + expect(executionEnvironment.find('dt').text()).toEqual( + 'Default Execution Environment' + ); + expect(executionEnvironment.find('dd').text()).toEqual( + mockProject.summary_fields.default_environment.name + ); expect( wrapper .find('Detail[label="Options"]') diff --git a/awx/ui_next/src/components/PromptDetail/data.job_template.json b/awx/ui_next/src/components/PromptDetail/data.job_template.json index 4a3653f842fa..5bbc5b26a80d 100644 --- a/awx/ui_next/src/components/PromptDetail/data.job_template.json +++ b/awx/ui_next/src/components/PromptDetail/data.job_template.json @@ -45,6 +45,12 @@ "organization_id": 1, "kind": "" }, + "execution_environment": { + "id": 1, + "name": "Default EE", + "description": "", + "image": "quay.io/ansible/awx-ee" + }, "project": { "id": 6, "name": "Mock Project", @@ -192,5 +198,6 @@ "custom_virtualenv": null, "job_slice_count": 1, "webhook_service": "github", - "webhook_credential": 8 + "webhook_credential": 8, + "execution_environment": 1 } diff --git a/awx/ui_next/src/components/PromptDetail/data.project.json b/awx/ui_next/src/components/PromptDetail/data.project.json index b7ac2271a1a9..24ed7ff1a62d 100644 --- a/awx/ui_next/src/components/PromptDetail/data.project.json +++ b/awx/ui_next/src/components/PromptDetail/data.project.json @@ -28,6 +28,12 @@ "name":"Default", "description":"" }, + "default_environment": { + "id": 1, + "name": "Default EE", + "description": "", + "image": "quay.io/ansible/awx-ee" + }, "credential": { "id": 9, "name": "mock scm", @@ -103,5 +109,6 @@ "allow_override":true, "custom_virtualenv": "mock virtual env", "last_update_failed":false, - "last_updated":"2020-03-11T20:18:14Z" + "last_updated":"2020-03-11T20:18:14Z", + "default_environment": 1 } \ No newline at end of file diff --git a/awx/ui_next/src/components/TemplateList/TemplateListItem.jsx b/awx/ui_next/src/components/TemplateList/TemplateListItem.jsx index a4ba4c360bec..effde59bc97b 100644 --- a/awx/ui_next/src/components/TemplateList/TemplateListItem.jsx +++ b/awx/ui_next/src/components/TemplateList/TemplateListItem.jsx @@ -11,6 +11,8 @@ import { ProjectDiagramIcon, RocketIcon, } from '@patternfly/react-icons'; +import styled from 'styled-components'; + import { ActionsTd, ActionItem } from '../PaginatedTable'; import { DetailList, Detail, DeletedDetail } from '../DetailList'; import ChipGroup from '../ChipGroup'; @@ -23,6 +25,11 @@ import Sparkline from '../Sparkline'; import { toTitleCase } from '../../util/strings'; import CopyButton from '../CopyButton'; +const ExclamationTriangleIconWarning = styled(ExclamationTriangleIcon)` + color: var(--pf-global--warning-color--100); + margin-left: 18px; +`; + function TemplateListItem({ i18n, template, @@ -67,6 +74,11 @@ function TemplateListItem({ (!summaryFields.project || (!summaryFields.inventory && !askInventoryOnLaunch)); + const missingExecutionEnvironment = + template.type === 'job_template' && + template.custom_virtualenv && + !template.execution_environment; + const inventoryValue = (kind, id) => { const inventorykind = kind === 'smart' ? 'smart_inventory' : 'inventory'; @@ -125,6 +137,18 @@ function TemplateListItem({ )} + {missingExecutionEnvironment && ( + + + + + + )} {toTitleCase(template.type)} {lastRun} diff --git a/awx/ui_next/src/screens/Inventory/InventorySourceAdd/InventorySourceAdd.test.jsx b/awx/ui_next/src/screens/Inventory/InventorySourceAdd/InventorySourceAdd.test.jsx index c186d4dcb7d3..a0439ad16439 100644 --- a/awx/ui_next/src/screens/Inventory/InventorySourceAdd/InventorySourceAdd.test.jsx +++ b/awx/ui_next/src/screens/Inventory/InventorySourceAdd/InventorySourceAdd.test.jsx @@ -74,15 +74,9 @@ describe('', () => { }); test('new form displays primary form fields', async () => { - const config = { - custom_virtualenvs: ['venv/foo', 'venv/bar'], - }; await act(async () => { wrapper = mountWithContexts( - , - { - context: { config }, - } + ); }); await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); @@ -90,7 +84,7 @@ describe('', () => { expect(wrapper.find('FormGroup[label="Description"]')).toHaveLength(1); expect(wrapper.find('FormGroup[label="Source"]')).toHaveLength(1); expect(wrapper.find('FormGroup[label="Ansible Environment"]')).toHaveLength( - 1 + 0 ); }); diff --git a/awx/ui_next/src/screens/Inventory/InventorySourceDetail/InventorySourceDetail.jsx b/awx/ui_next/src/screens/Inventory/InventorySourceDetail/InventorySourceDetail.jsx index efc2a8ff73b8..11a59aeb443a 100644 --- a/awx/ui_next/src/screens/Inventory/InventorySourceDetail/InventorySourceDetail.jsx +++ b/awx/ui_next/src/screens/Inventory/InventorySourceDetail/InventorySourceDetail.jsx @@ -11,6 +11,7 @@ import ContentError from '../../../components/ContentError'; import ContentLoading from '../../../components/ContentLoading'; import CredentialChip from '../../../components/CredentialChip'; import DeleteButton from '../../../components/DeleteButton'; +import ExecutionEnvironmentDetail from '../../../components/ExecutionEnvironmentDetail'; import InventorySourceSyncButton from '../shared/InventorySourceSyncButton'; import { DetailList, @@ -201,9 +202,9 @@ function InventorySourceDetail({ inventorySource, i18n }) { } /> )} - {source_project && ( )} - {execution_environment?.name && ( - - {execution_environment.name} - - } - /> - )} {source === 'scm' ? ( { assertDetail(wrapper, 'Description', 'mock description'); assertDetail(wrapper, 'Source', 'Sourced from a Project'); assertDetail(wrapper, 'Organization', 'Mock Org'); - assertDetail(wrapper, 'Ansible environment', '/var/lib/awx/venv/custom'); assertDetail(wrapper, 'Project', 'Mock Project'); assertDetail(wrapper, 'Inventory file', 'foo'); assertDetail(wrapper, 'Verbosity', '2 (Debug)'); assertDetail(wrapper, 'Cache timeout', '2 seconds'); + const executionEnvironment = wrapper.find('ExecutionEnvironmentDetail'); + expect(executionEnvironment).toHaveLength(1); + expect(executionEnvironment.find('dt').text()).toEqual( + 'Execution Environment' + ); + expect(executionEnvironment.find('dd').text()).toEqual( + mockInvSource.summary_fields.execution_environment.name + ); + expect(wrapper.find('CredentialChip').text()).toBe('Cloud: mock cred'); expect(wrapper.find('VariablesDetail').prop('value')).toEqual( '---\nfoo: bar' diff --git a/awx/ui_next/src/screens/Inventory/InventorySources/InventorySourceListItem.jsx b/awx/ui_next/src/screens/Inventory/InventorySources/InventorySourceListItem.jsx index 94c21eef73bc..b5c1ec756142 100644 --- a/awx/ui_next/src/screens/Inventory/InventorySources/InventorySourceListItem.jsx +++ b/awx/ui_next/src/screens/Inventory/InventorySources/InventorySourceListItem.jsx @@ -12,10 +12,20 @@ import { DataListAction, Tooltip, } from '@patternfly/react-core'; -import { PencilAltIcon } from '@patternfly/react-icons'; +import { + ExclamationTriangleIcon as PFExclamationTriangleIcon, + PencilAltIcon, +} from '@patternfly/react-icons'; +import styled from 'styled-components'; + import StatusIcon from '../../../components/StatusIcon'; import InventorySourceSyncButton from '../shared/InventorySourceSyncButton'; +const ExclamationTriangleIcon = styled(PFExclamationTriangleIcon)` + color: var(--pf-global--warning-color--100); + margin-left: 18px; +`; + function InventorySourceListItem({ source, isSelected, @@ -42,6 +52,10 @@ function InventorySourceListItem({ ); }; + + const missingExecutionEnvironment = + source.custom_virtualenv && !source.execution_environment; + return ( <> @@ -79,6 +93,18 @@ function InventorySourceListItem({ {source.name} + {missingExecutionEnvironment && ( + + + + + + )} , {label} diff --git a/awx/ui_next/src/screens/Inventory/shared/InventorySourceForm.jsx b/awx/ui_next/src/screens/Inventory/shared/InventorySourceForm.jsx index 05bbd4e370be..3049d2ccb470 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventorySourceForm.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventorySourceForm.jsx @@ -1,11 +1,10 @@ -import React, { useEffect, useCallback, useContext } from 'react'; +import React, { useEffect, useCallback } from 'react'; import { Formik, useField, useFormikContext } from 'formik'; import { func, shape } from 'prop-types'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; import { Form, FormGroup, Title } from '@patternfly/react-core'; import { InventorySourcesAPI } from '../../../api'; -import { ConfigContext } from '../../../contexts/Config'; import useRequest from '../../../util/useRequest'; import { required } from '../../../util/validators'; @@ -18,7 +17,6 @@ import { FormColumnLayout, SubFormLayout, } from '../../../components/FormLayout'; -import Popover from '../../../components/Popover'; import { AzureSubForm, @@ -64,13 +62,6 @@ const InventorySourceFormFields = ({ ] = useField({ name: 'execution_environment', }); - const { custom_virtualenvs } = useContext(ConfigContext); - const [venvField] = useField('custom_virtualenv'); - const defaultVenv = { - label: i18n._(t`Use Default Ansible Environment`), - value: '/var/lib/awx/venv/ansible/', - key: 'default', - }; const resetSubFormFields = sourceType => { if (sourceType === initialValues.source) { @@ -79,7 +70,6 @@ const InventorySourceFormFields = ({ ...initialValues, name: values.name, description: values.description, - custom_virtualenv: values.custom_virtualenv, source: sourceType, }, }); @@ -161,30 +151,6 @@ const InventorySourceFormFields = ({ }} /> - {custom_virtualenvs && custom_virtualenvs.length > 1 && ( - - } - > - value !== defaultVenv.value) - .map(value => ({ value, label: value, key: value })), - ]} - {...venvField} - /> - - )} {!['', 'custom'].includes(sourceField.value) && ( @@ -272,7 +238,6 @@ const InventorySourceForm = ({ }) => { const initialValues = { credential: source?.summary_fields?.credential || null, - custom_virtualenv: source?.custom_virtualenv || '', description: source?.description || '', name: source?.name || '', overwrite: source?.overwrite || false, diff --git a/awx/ui_next/src/screens/Inventory/shared/InventorySourceForm.test.jsx b/awx/ui_next/src/screens/Inventory/shared/InventorySourceForm.test.jsx index 86cb2bbd26f3..ab165d8ca4fb 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventorySourceForm.test.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventorySourceForm.test.jsx @@ -46,15 +46,9 @@ describe('<InventorySourceForm />', () => { const onSubmit = jest.fn(); beforeAll(async () => { - const config = { - custom_virtualenvs: ['venv/foo', 'venv/bar'], - }; await act(async () => { wrapper = mountWithContexts( - <InventorySourceForm onCancel={() => {}} onSubmit={onSubmit} />, - { - context: { config }, - } + <InventorySourceForm onCancel={() => {}} onSubmit={onSubmit} /> ); }); await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); @@ -71,7 +65,7 @@ describe('<InventorySourceForm />', () => { expect(wrapper.find('FormGroup[label="Source"]')).toHaveLength(1); expect( wrapper.find('FormGroup[label="Ansible Environment"]') - ).toHaveLength(1); + ).toHaveLength(0); expect(wrapper.find('ExecutionEnvironmentLookup')).toHaveLength(1); }); diff --git a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/AzureSubForm.test.jsx b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/AzureSubForm.test.jsx index b363f7f42b50..a05c2b19d441 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/AzureSubForm.test.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/AzureSubForm.test.jsx @@ -9,7 +9,6 @@ jest.mock('../../../../api/models/Credentials'); const initialValues = { credential: null, - custom_virtualenv: '', overwrite: false, overwrite_vars: false, source_path: '', diff --git a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/GCESubForm.test.jsx b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/GCESubForm.test.jsx index fdbd1aca5e67..e1e7004c2d35 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/GCESubForm.test.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/GCESubForm.test.jsx @@ -9,7 +9,6 @@ jest.mock('../../../../api/models/Credentials'); const initialValues = { credential: null, - custom_virtualenv: '', overwrite: false, overwrite_vars: false, source_path: '', diff --git a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/OpenStackSubForm.test.jsx b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/OpenStackSubForm.test.jsx index f912186816ad..14e6262f237e 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/OpenStackSubForm.test.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/OpenStackSubForm.test.jsx @@ -9,7 +9,6 @@ jest.mock('../../../../api/models/Credentials'); const initialValues = { credential: null, - custom_virtualenv: '', overwrite: false, overwrite_vars: false, source_path: '', diff --git a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SCMSubForm.test.jsx b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SCMSubForm.test.jsx index 9edd61b7e4b2..2a355081206b 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SCMSubForm.test.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/SCMSubForm.test.jsx @@ -10,7 +10,6 @@ jest.mock('../../../../api/models/Credentials'); const initialValues = { credential: null, - custom_virtualenv: '', overwrite: false, overwrite_vars: false, source_path: '', @@ -115,7 +114,6 @@ describe('<SCMSubForm />', () => { test('should be able to create custom source path', async () => { const customInitialValues = { credential: { id: 1, name: 'Credential' }, - custom_virtualenv: '', overwrite: false, overwrite_vars: false, source_path: '/path', diff --git a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/VMwareSubForm.test.jsx b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/VMwareSubForm.test.jsx index e86bc49d5092..d8c447731cf5 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/VMwareSubForm.test.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/VMwareSubForm.test.jsx @@ -9,7 +9,6 @@ jest.mock('../../../../api/models/Credentials'); const initialValues = { credential: null, - custom_virtualenv: '', overwrite: false, overwrite_vars: false, source_path: '', diff --git a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/VirtualizationSubForm.test.jsx b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/VirtualizationSubForm.test.jsx index b6c238735cb5..7c7f0f4ff4f4 100644 --- a/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/VirtualizationSubForm.test.jsx +++ b/awx/ui_next/src/screens/Inventory/shared/InventorySourceSubForms/VirtualizationSubForm.test.jsx @@ -9,7 +9,6 @@ jest.mock('../../../../api/models/Credentials'); const initialValues = { credential: null, - custom_virtualenv: '', overwrite: false, overwrite_vars: false, source_path: '', diff --git a/awx/ui_next/src/screens/Inventory/shared/data.inventory_source.json b/awx/ui_next/src/screens/Inventory/shared/data.inventory_source.json index 550cb8138e79..a1eeaf270cfe 100644 --- a/awx/ui_next/src/screens/Inventory/shared/data.inventory_source.json +++ b/awx/ui_next/src/screens/Inventory/shared/data.inventory_source.json @@ -39,6 +39,12 @@ "organization_id":1, "kind":"" }, + "execution_environment": { + "id": 1, + "name": "Default EE", + "description": "", + "image": "quay.io/ansible/awx-ee" + }, "source_project":{ "id":8, "name":"Mock Project", @@ -111,5 +117,6 @@ "source_project":8, "update_on_project_update":true, "last_update_failed": true, - "last_updated":null + "last_updated":null, + "execution_environment": 1 } \ No newline at end of file diff --git a/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx b/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx index 176ca95556de..b81d77451209 100644 --- a/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx +++ b/awx/ui_next/src/screens/Job/JobDetail/JobDetail.jsx @@ -24,6 +24,7 @@ import { ReLaunchDropDown, } from '../../../components/LaunchButton'; import StatusIcon from '../../../components/StatusIcon'; +import ExecutionEnvironmentDetail from '../../../components/ExecutionEnvironmentDetail'; import { toTitleCase } from '../../../util/strings'; import { formatDateString } from '../../../util/dates'; import { Job } from '../../../types'; @@ -71,6 +72,7 @@ function JobDetail({ job, i18n }) { labels, project, source_workflow_job, + execution_environment: executionEnvironment, } = job.summary_fields; const [errorMsg, setErrorMsg] = useState(); const history = useHistory(); @@ -250,7 +252,10 @@ function JobDetail({ job, i18n }) { <Detail label={i18n._(t`Playbook`)} value={job.playbook} /> <Detail label={i18n._(t`Limit`)} value={job.limit} /> <Detail label={i18n._(t`Verbosity`)} value={VERBOSITY[job.verbosity]} /> - <Detail label={i18n._(t`Environment`)} value={job.custom_virtualenv} /> + <ExecutionEnvironmentDetail + virtualEnvironment={job.custom_virtualenv} + executionEnvironment={executionEnvironment} + /> <Detail label={i18n._(t`Execution Node`)} value={job.execution_node} /> {instanceGroup && !instanceGroup?.is_container_group && ( <Detail diff --git a/awx/ui_next/src/screens/Job/JobDetail/JobDetail.test.jsx b/awx/ui_next/src/screens/Job/JobDetail/JobDetail.test.jsx index d509ebd17670..5b2538436e1f 100644 --- a/awx/ui_next/src/screens/Job/JobDetail/JobDetail.test.jsx +++ b/awx/ui_next/src/screens/Job/JobDetail/JobDetail.test.jsx @@ -60,7 +60,6 @@ describe('<JobDetail />', () => { assertDetail('Revision', mockJobData.scm_revision); assertDetail('Playbook', mockJobData.playbook); assertDetail('Verbosity', '0 (Normal)'); - assertDetail('Environment', mockJobData.custom_virtualenv); assertDetail('Execution Node', mockJobData.execution_node); assertDetail( 'Instance Group', @@ -70,6 +69,15 @@ describe('<JobDetail />', () => { assertDetail('Credentials', 'SSH: Demo Credential'); assertDetail('Machine Credential', 'SSH: Machine cred'); + const executionEnvironment = wrapper.find('ExecutionEnvironmentDetail'); + expect(executionEnvironment).toHaveLength(1); + expect(executionEnvironment.find('dt').text()).toEqual( + 'Execution Environment' + ); + expect(executionEnvironment.find('dd').text()).toEqual( + mockJobData.summary_fields.execution_environment.name + ); + const credentialChip = wrapper.find( `Detail[label="Credentials"] CredentialChip` ); diff --git a/awx/ui_next/src/screens/Job/shared/data.job.json b/awx/ui_next/src/screens/Job/shared/data.job.json index 778c2fcc847e..614e7d6aad80 100644 --- a/awx/ui_next/src/screens/Job/shared/data.job.json +++ b/awx/ui_next/src/screens/Job/shared/data.job.json @@ -36,6 +36,12 @@ "organization_id": 1, "kind": "" }, + "execution_environment": { + "id": 1, + "name": "Default EE", + "description": "", + "image": "quay.io/ansible/awx-ee" + }, "project": { "id": 6, "name": "Demo Project", @@ -184,5 +190,6 @@ "play_count": 1, "task_count": 1 }, - "custom_virtualenv": "/var/lib/awx/venv/ansible" + "custom_virtualenv": "/var/lib/awx/venv/ansible", + "execution_environment": 1 } diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateAdd.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateAdd.jsx index b5f464db1707..51148d64894b 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateAdd.jsx +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateAdd.jsx @@ -1,5 +1,4 @@ import React, { useState, useEffect, useCallback } from 'react'; -import PropTypes from 'prop-types'; import { useHistory, Link } from 'react-router-dom'; import { t } from '@lingui/macro'; import { withI18n } from '@lingui/react'; @@ -78,9 +77,5 @@ function NotificationTemplateAdd({ i18n }) { ); } -NotificationTemplateAdd.contextTypes = { - custom_virtualenvs: PropTypes.arrayOf(PropTypes.string), -}; - export { NotificationTemplateAdd as _NotificationTemplateAdd }; export default withI18n()(NotificationTemplateAdd); diff --git a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateEdit/NotificationTemplateEdit.jsx b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateEdit/NotificationTemplateEdit.jsx index 3cff9b08333a..4daa7e67bb94 100644 --- a/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateEdit/NotificationTemplateEdit.jsx +++ b/awx/ui_next/src/screens/NotificationTemplate/NotificationTemplateEdit/NotificationTemplateEdit.jsx @@ -40,9 +40,5 @@ NotificationTemplateEdit.propTypes = { template: PropTypes.shape().isRequired, }; -NotificationTemplateEdit.contextTypes = { - custom_virtualenvs: PropTypes.arrayOf(PropTypes.string), -}; - export { NotificationTemplateEdit as _NotificationTemplateEdit }; export default NotificationTemplateEdit; diff --git a/awx/ui_next/src/screens/Organization/OrganizationAdd/OrganizationAdd.jsx b/awx/ui_next/src/screens/Organization/OrganizationAdd/OrganizationAdd.jsx index adbe04820ddd..11f5be4f2577 100644 --- a/awx/ui_next/src/screens/Organization/OrganizationAdd/OrganizationAdd.jsx +++ b/awx/ui_next/src/screens/Organization/OrganizationAdd/OrganizationAdd.jsx @@ -1,5 +1,4 @@ import React, { useState } from 'react'; -import PropTypes from 'prop-types'; import { useHistory } from 'react-router-dom'; import { PageSection, Card } from '@patternfly/react-core'; @@ -51,9 +50,5 @@ function OrganizationAdd() { ); } -OrganizationAdd.contextTypes = { - custom_virtualenvs: PropTypes.arrayOf(PropTypes.string), -}; - export { OrganizationAdd as _OrganizationAdd }; export default OrganizationAdd; diff --git a/awx/ui_next/src/screens/Organization/OrganizationAdd/OrganizationAdd.test.jsx b/awx/ui_next/src/screens/Organization/OrganizationAdd/OrganizationAdd.test.jsx index d99634ea09a4..c05564801c44 100644 --- a/awx/ui_next/src/screens/Organization/OrganizationAdd/OrganizationAdd.test.jsx +++ b/awx/ui_next/src/screens/Organization/OrganizationAdd/OrganizationAdd.test.jsx @@ -15,7 +15,6 @@ describe('<OrganizationAdd />', () => { const updatedOrgData = { name: 'new name', description: 'new description', - custom_virtualenv: 'Buzz', galaxy_credentials: [], default_environment: { id: 1, name: 'Foo' }, }; @@ -51,7 +50,6 @@ describe('<OrganizationAdd />', () => { const orgData = { name: 'new name', description: 'new description', - custom_virtualenv: 'Buzz', galaxy_credentials: [], }; OrganizationsAPI.create.mockResolvedValueOnce({ @@ -78,7 +76,6 @@ describe('<OrganizationAdd />', () => { const orgData = { name: 'new name', description: 'new description', - custom_virtualenv: 'Buzz', galaxy_credentials: [], }; OrganizationsAPI.create.mockResolvedValueOnce({ @@ -103,7 +100,6 @@ describe('<OrganizationAdd />', () => { const orgData = { name: 'new name', description: 'new description', - custom_virtualenv: 'Buzz', galaxy_credentials: [ { id: 9000, @@ -131,36 +127,6 @@ describe('<OrganizationAdd />', () => { ); }); - test('AnsibleSelect component renders if there are virtual environments', async () => { - const mockInstanceGroups = [ - { name: 'One', id: 1 }, - { name: 'Two', id: 2 }, - ]; - OrganizationsAPI.readInstanceGroups.mockReturnValue({ - data: { - results: mockInstanceGroups, - }, - }); - const config = { - custom_virtualenvs: ['foo', 'bar'], - }; - let wrapper; - await act(async () => { - wrapper = mountWithContexts(<OrganizationAdd />, { - context: { config }, - }); - }); - await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); - expect(wrapper.find('FormSelect')).toHaveLength(1); - expect(wrapper.find('FormSelectOption')).toHaveLength(3); - expect( - wrapper - .find('FormSelectOption') - .first() - .prop('value') - ).toEqual('/var/lib/awx/venv/ansible/'); - }); - test('AnsibleSelect component does not render if there are 0 virtual environments', async () => { const mockInstanceGroups = [ { name: 'One', id: 1 }, @@ -171,14 +137,9 @@ describe('<OrganizationAdd />', () => { results: mockInstanceGroups, }, }); - const config = { - custom_virtualenvs: [], - }; let wrapper; await act(async () => { - wrapper = mountWithContexts(<OrganizationAdd />, { - context: { config }, - }); + wrapper = mountWithContexts(<OrganizationAdd />, {}); }); await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); expect(wrapper.find('AnsibleSelect FormSelect')).toHaveLength(0); diff --git a/awx/ui_next/src/screens/Organization/OrganizationDetail/OrganizationDetail.jsx b/awx/ui_next/src/screens/Organization/OrganizationDetail/OrganizationDetail.jsx index e3b544c091c4..cefd8232023f 100644 --- a/awx/ui_next/src/screens/Organization/OrganizationDetail/OrganizationDetail.jsx +++ b/awx/ui_next/src/screens/Organization/OrganizationDetail/OrganizationDetail.jsx @@ -19,6 +19,7 @@ import DeleteButton from '../../../components/DeleteButton'; import ErrorDetail from '../../../components/ErrorDetail'; import useRequest, { useDismissableError } from '../../../util/useRequest'; import { useConfig } from '../../../contexts/Config'; +import ExecutionEnvironmentDetail from '../../../components/ExecutionEnvironmentDetail'; function OrganizationDetail({ i18n, organization }) { const { @@ -90,22 +91,11 @@ function OrganizationDetail({ i18n, organization }) { {license_info?.license_type !== 'open' && ( <Detail label={i18n._(t`Max Hosts`)} value={`${max_hosts}`} /> )} - <Detail - label={i18n._(t`Ansible Environment`)} - value={custom_virtualenv} + <ExecutionEnvironmentDetail + virtualEnvironment={custom_virtualenv} + executionEnvironment={summary_fields?.default_environment} + isDefaultEnvironment /> - {summary_fields?.default_environment?.name && ( - <Detail - label={i18n._(t`Default Execution Environment`)} - value={ - <Link - to={`/execution_environments/${summary_fields.default_environment.id}/details`} - > - {summary_fields.default_environment.name} - </Link> - } - /> - )} <UserDateDetail label={i18n._(t`Created`)} date={created} diff --git a/awx/ui_next/src/screens/Organization/OrganizationDetail/OrganizationDetail.test.jsx b/awx/ui_next/src/screens/Organization/OrganizationDetail/OrganizationDetail.test.jsx index 9ed9bbd16918..a33ddc1c6160 100644 --- a/awx/ui_next/src/screens/Organization/OrganizationDetail/OrganizationDetail.test.jsx +++ b/awx/ui_next/src/screens/Organization/OrganizationDetail/OrganizationDetail.test.jsx @@ -90,7 +90,6 @@ describe('<OrganizationDetail />', () => { const testParams = [ { label: 'Name', value: 'Foo' }, { label: 'Description', value: 'Bar' }, - { label: 'Ansible Environment', value: 'Fizz' }, { label: 'Created', value: '7/7/2015, 5:21:26 PM' }, { label: 'Last Modified', value: '8/11/2019, 7:47:37 PM' }, { label: 'Max Hosts', value: '0' }, diff --git a/awx/ui_next/src/screens/Organization/OrganizationEdit/OrganizationEdit.jsx b/awx/ui_next/src/screens/Organization/OrganizationEdit/OrganizationEdit.jsx index 3297d2fd6f31..76a548bcbae9 100644 --- a/awx/ui_next/src/screens/Organization/OrganizationEdit/OrganizationEdit.jsx +++ b/awx/ui_next/src/screens/Organization/OrganizationEdit/OrganizationEdit.jsx @@ -80,9 +80,5 @@ OrganizationEdit.propTypes = { organization: PropTypes.shape().isRequired, }; -OrganizationEdit.contextTypes = { - custom_virtualenvs: PropTypes.arrayOf(PropTypes.string), -}; - export { OrganizationEdit as _OrganizationEdit }; export default OrganizationEdit; diff --git a/awx/ui_next/src/screens/Organization/OrganizationEdit/OrganizationEdit.test.jsx b/awx/ui_next/src/screens/Organization/OrganizationEdit/OrganizationEdit.test.jsx index 5556ee05d558..a6324eb702bf 100644 --- a/awx/ui_next/src/screens/Organization/OrganizationEdit/OrganizationEdit.test.jsx +++ b/awx/ui_next/src/screens/Organization/OrganizationEdit/OrganizationEdit.test.jsx @@ -14,7 +14,6 @@ describe('<OrganizationEdit />', () => { const mockData = { name: 'Foo', description: 'Bar', - custom_virtualenv: 'Fizz', id: 1, related: { instance_groups: '/api/v2/organizations/1/instance_groups', @@ -24,6 +23,7 @@ describe('<OrganizationEdit />', () => { default_environment: { id: 1, name: 'Baz', + image: 'quay.io/ansible/awx-ee', }, }, }; @@ -37,7 +37,6 @@ describe('<OrganizationEdit />', () => { const updatedOrgData = { name: 'new name', description: 'new description', - custom_virtualenv: 'Buzz', default_environment: null, }; wrapper.find('OrganizationForm').prop('onSubmit')(updatedOrgData, [], []); @@ -54,7 +53,6 @@ describe('<OrganizationEdit />', () => { const updatedOrgData = { name: 'new name', description: 'new description', - custom_virtualenv: 'Buzz', }; await act(async () => { wrapper.find('OrganizationForm').invoke('onSubmit')( diff --git a/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationListItem.jsx b/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationListItem.jsx index dc727dba47cc..1eae64646a10 100644 --- a/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationListItem.jsx +++ b/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationListItem.jsx @@ -2,14 +2,22 @@ import React from 'react'; import { string, bool, func } from 'prop-types'; import { withI18n } from '@lingui/react'; import { t } from '@lingui/macro'; -import { Button } from '@patternfly/react-core'; +import { Button, Tooltip } from '@patternfly/react-core'; import { Tr, Td } from '@patternfly/react-table'; import { Link } from 'react-router-dom'; -import { PencilAltIcon } from '@patternfly/react-icons'; +import styled from 'styled-components'; +import { + ExclamationTriangleIcon as PFExclamationTriangleIcon, + PencilAltIcon, +} from '@patternfly/react-icons'; import { ActionsTd, ActionItem } from '../../../components/PaginatedTable'; import { Organization } from '../../../types'; +const ExclamationTriangleIcon = styled(PFExclamationTriangleIcon)` + color: var(--pf-global--warning-color--100); + margin-left: 18px; +`; function OrganizationListItem({ organization, isSelected, @@ -19,6 +27,10 @@ function OrganizationListItem({ i18n, }) { const labelId = `check-action-${organization.id}`; + + const missingExecutionEnvironment = + organization.custom_virtualenv && !organization.default_environment; + return ( <Tr id={`org-row-${organization.id}`}> <Td @@ -31,9 +43,23 @@ function OrganizationListItem({ dataLabel={i18n._(t`Selected`)} /> <Td id={labelId} dataLabel={i18n._(t`Name`)}> - <Link to={`${detailUrl}`}> - <b>{organization.name}</b> - </Link> + <span> + <Link to={`${detailUrl}`}> + <b>{organization.name}</b> + </Link> + </span> + {missingExecutionEnvironment && ( + <span> + <Tooltip + content={i18n._( + t`Organization is missing an execution environment.` + )} + position="right" + > + <ExclamationTriangleIcon /> + </Tooltip> + </span> + )} </Td> <Td dataLabel={i18n._(t`Members`)}> {organization.summary_fields.related_field_counts.users} diff --git a/awx/ui_next/src/screens/Organization/shared/OrganizationForm.jsx b/awx/ui_next/src/screens/Organization/shared/OrganizationForm.jsx index eb46f8c5cc5f..ee58fbd55f36 100644 --- a/awx/ui_next/src/screens/Organization/shared/OrganizationForm.jsx +++ b/awx/ui_next/src/screens/Organization/shared/OrganizationForm.jsx @@ -1,13 +1,12 @@ -import React, { useCallback, useContext, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import PropTypes 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 } from '@patternfly/react-core'; import { OrganizationsAPI } from '../../../api'; -import { ConfigContext, useConfig } from '../../../contexts/Config'; -import AnsibleSelect from '../../../components/AnsibleSelect'; +import { useConfig } from '../../../contexts/Config'; import ContentError from '../../../components/ContentError'; import ContentLoading from '../../../components/ContentLoading'; import FormField, { FormSubmitError } from '../../../components/FormField'; @@ -23,10 +22,8 @@ import CredentialLookup from '../../../components/Lookup/CredentialLookup'; function OrganizationFormFields({ i18n, instanceGroups, setInstanceGroups }) { const { license_info = {}, me = {} } = useConfig(); - const { custom_virtualenvs } = useContext(ConfigContext); const { setFieldValue } = useFormikContext(); - const [venvField] = useField('custom_virtualenv'); const [ galaxyCredentialsField, @@ -42,12 +39,6 @@ function OrganizationFormFields({ i18n, instanceGroups, setInstanceGroups }) { name: 'default_environment', }); - const defaultVenv = { - label: i18n._(t`Use Default Ansible Environment`), - value: '/var/lib/awx/venv/ansible/', - key: 'default', - }; - const handleCredentialUpdate = useCallback( value => { setFieldValue('galaxy_credentials', value); @@ -87,24 +78,6 @@ function OrganizationFormFields({ i18n, instanceGroups, setInstanceGroups }) { isDisabled={!me.is_superuser} /> )} - - {custom_virtualenvs && custom_virtualenvs.length > 1 && ( - <FormGroup - fieldId="org-custom-virtualenv" - label={i18n._(t`Ansible Environment`)} - > - <AnsibleSelect - id="org-custom-virtualenv" - data={[ - defaultVenv, - ...custom_virtualenvs - .filter(value => value !== defaultVenv.value) - .map(value => ({ value, label: value, key: value })), - ]} - {...venvField} - /> - </FormGroup> - )} <InstanceGroupsLookup value={instanceGroups} onChange={setInstanceGroups} @@ -208,11 +181,10 @@ function OrganizationForm({ initialValues={{ name: organization.name, description: organization.description, - custom_virtualenv: organization.custom_virtualenv || '', max_hosts: organization.max_hosts || '0', galaxy_credentials: organization.galaxy_credentials || [], default_environment: - organization.summary_fields?.default_environment || '', + organization.summary_fields?.default_environment || null, }} onSubmit={handleSubmit} > @@ -248,15 +220,10 @@ OrganizationForm.defaultProps = { name: '', description: '', max_hosts: '0', - custom_virtualenv: '', default_environment: '', }, submitError: null, }; -OrganizationForm.contextTypes = { - custom_virtualenvs: PropTypes.arrayOf(PropTypes.string), -}; - export { OrganizationForm as _OrganizationForm }; export default withI18n()(OrganizationForm); diff --git a/awx/ui_next/src/screens/Organization/shared/OrganizationForm.test.jsx b/awx/ui_next/src/screens/Organization/shared/OrganizationForm.test.jsx index 7dfbca620c8a..716bdd8096b2 100644 --- a/awx/ui_next/src/screens/Organization/shared/OrganizationForm.test.jsx +++ b/awx/ui_next/src/screens/Organization/shared/OrganizationForm.test.jsx @@ -22,7 +22,6 @@ describe('<OrganizationForm />', () => { name: 'Foo', description: 'Bar', max_hosts: 1, - custom_virtualenv: 'Fizz', related: { instance_groups: '/api/v2/organizations/1/instance_groups', }, @@ -32,7 +31,9 @@ describe('<OrganizationForm />', () => { { name: 'Two', id: 2 }, ]; - const mockExecutionEnvironment = [{ name: 'EE' }]; + const mockExecutionEnvironment = [ + { id: 1, name: 'EE', image: 'quay.io/ansible/awx-ee' }, + ]; afterEach(() => { jest.clearAllMocks(); @@ -176,46 +177,11 @@ describe('<OrganizationForm />', () => { name: 'new foo', description: 'new bar', galaxy_credentials: [], - custom_virtualenv: 'Fizz', max_hosts: 134, default_environment: { id: 1, name: 'Test EE' }, }); }); - test('AnsibleSelect component renders if there are virtual environments', async () => { - const config = { - custom_virtualenvs: ['foo', 'bar'], - }; - OrganizationsAPI.readInstanceGroups.mockReturnValue({ - data: { - results: mockInstanceGroups, - }, - }); - let wrapper; - await act(async () => { - wrapper = mountWithContexts( - <OrganizationForm - organization={mockData} - onSubmit={jest.fn()} - onCancel={jest.fn()} - me={meConfig.me} - />, - { - context: { config }, - } - ); - }); - await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); - expect(wrapper.find('FormSelect')).toHaveLength(1); - expect(wrapper.find('FormSelectOption')).toHaveLength(3); - expect( - wrapper - .find('FormSelectOption') - .first() - .prop('value') - ).toEqual('/var/lib/awx/venv/ansible/'); - }); - test('onSubmit associates and disassociates instance groups', async () => { OrganizationsAPI.readInstanceGroups.mockReturnValue({ data: { @@ -230,8 +196,7 @@ describe('<OrganizationForm />', () => { description: 'Bar', galaxy_credentials: [], max_hosts: 1, - custom_virtualenv: 'Fizz', - default_environment: '', + default_environment: null, }; const onSubmit = jest.fn(); OrganizationsAPI.update.mockResolvedValue(1, mockDataForm); @@ -336,8 +301,7 @@ describe('<OrganizationForm />', () => { description: 'Bar', galaxy_credentials: [], max_hosts: 0, - custom_virtualenv: 'Fizz', - default_environment: '', + default_environment: null, }, [], [] diff --git a/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.jsx b/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.jsx index 984aaab6fb1b..17a43f1fdeee 100644 --- a/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.jsx +++ b/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.jsx @@ -15,6 +15,7 @@ import { UserDateDetail, } from '../../../components/DetailList'; import ErrorDetail from '../../../components/ErrorDetail'; +import ExecutionEnvironmentDetail from '../../../components/ExecutionEnvironmentDetail'; import CredentialChip from '../../../components/CredentialChip'; import { ProjectsAPI } from '../../../api'; import { toTitleCase } from '../../../util/strings'; @@ -125,22 +126,11 @@ function ProjectDetail({ project, i18n }) { value={`${scm_update_cache_timeout} ${i18n._(t`Seconds`)}`} /> - <Detail - label={i18n._(t`Ansible Environment`)} - value={custom_virtualenv} + <ExecutionEnvironmentDetail + virtualEnvironment={custom_virtualenv} + executionEnvironment={summary_fields?.default_environment} + isDefaultEnvironment /> - {summary_fields?.default_environment?.name && ( - <Detail - label={i18n._(t`Execution Environment`)} - value={ - <Link - to={`/execution_environments/${summary_fields.default_environment.id}/details`} - > - {summary_fields.default_environment.name} - </Link> - } - /> - )} <Config> {({ project_base_dir }) => ( <Detail diff --git a/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.test.jsx b/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.test.jsx index 53eee50175ab..36caa48a12b2 100644 --- a/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.test.jsx +++ b/awx/ui_next/src/screens/Project/ProjectDetail/ProjectDetail.test.jsx @@ -100,11 +100,15 @@ describe('<ProjectDetail />', () => { 'Cache Timeout', `${mockProject.scm_update_cache_timeout} Seconds` ); - assertDetail('Ansible Environment', mockProject.custom_virtualenv); - assertDetail( - 'Execution Environment', + const executionEnvironment = wrapper.find('ExecutionEnvironmentDetail'); + expect(executionEnvironment).toHaveLength(1); + expect(executionEnvironment.find('dt').text()).toEqual( + 'Default Execution Environment' + ); + expect(executionEnvironment.find('dd').text()).toEqual( mockProject.summary_fields.default_environment.name ); + const dateDetails = wrapper.find('UserDateDetail'); expect(dateDetails).toHaveLength(2); expect(dateDetails.at(0).prop('label')).toEqual('Created'); diff --git a/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesListItem.jsx b/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesListItem.jsx index 2468ca83526d..2d4e96b43c8f 100644 --- a/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesListItem.jsx +++ b/awx/ui_next/src/screens/Project/ProjectJobTemplatesList/ProjectJobTemplatesListItem.jsx @@ -31,6 +31,11 @@ const DataListAction = styled(_DataListAction)` grid-template-columns: repeat(2, 40px); `; +const ExclamationTriangleIconWarning = styled(ExclamationTriangleIcon)` + color: var(--pf-global--warning-color--100); + margin-left: 18px; +`; + function ProjectJobTemplateListItem({ i18n, template, @@ -47,6 +52,11 @@ function ProjectJobTemplateListItem({ (!template.summary_fields.inventory && !template.ask_inventory_on_launch)); + const missingExecutionEnvironment = + template.type === 'job_template' && + template.custom_virtual_env && + !template.execution_environment; + return ( <DataListItem aria-labelledby={labelId} id={`${template.id}`}> <DataListItemRow> @@ -76,6 +86,18 @@ function ProjectJobTemplateListItem({ </Tooltip> </span> )} + {missingExecutionEnvironment && ( + <span> + <Tooltip + content={i18n._( + t`Job template is missing an execution environment.` + )} + position="right" + > + <ExclamationTriangleIconWarning /> + </Tooltip> + </span> + )} </DataListCell>, <DataListCell key="type"> {toTitleCase(template.type)} diff --git a/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.jsx b/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.jsx index 5b7498a9bc2f..97a7dedbc9e0 100644 --- a/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.jsx +++ b/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.jsx @@ -6,7 +6,10 @@ import { Button, Tooltip } from '@patternfly/react-core'; import { Tr, Td } from '@patternfly/react-table'; import { t } from '@lingui/macro'; import { Link } from 'react-router-dom'; -import { PencilAltIcon } from '@patternfly/react-icons'; +import { + PencilAltIcon, + ExclamationTriangleIcon as PFExclamationTriangleIcon, +} from '@patternfly/react-icons'; import styled from 'styled-components'; import { ActionsTd, ActionItem } from '../../../components/PaginatedTable'; import { formatDateString, timeOfDay } from '../../../util/dates'; @@ -22,6 +25,11 @@ const Label = styled.span` color: var(--pf-global--disabled-color--100); `; +const ExclamationTriangleIcon = styled(PFExclamationTriangleIcon)` + color: var(--pf-global--warning-color--100); + margin-left: 18px; +`; + function ProjectListItem({ project, isSelected, @@ -75,6 +83,9 @@ function ProjectListItem({ const labelId = `check-action-${project.id}`; + const missingExecutionEnvironment = + project.custom_virtualenv && !project.default_environment; + return ( <Tr id={`${project.id}`}> <Td @@ -86,9 +97,21 @@ function ProjectListItem({ dataLabel={i18n._(t`Selected`)} /> <Td id={labelId} dataLabel={i18n._(t`Name`)}> - <Link id={labelId} to={`${detailUrl}`}> - <b>{project.name}</b> - </Link> + <span> + <Link id={labelId} to={`${detailUrl}`}> + <b>{project.name}</b> + </Link> + </span> + {missingExecutionEnvironment && ( + <span> + <Tooltip + content={i18n._(t`Project is missing an execution environment.`)} + position="right" + > + <ExclamationTriangleIcon /> + </Tooltip> + </span> + )} </Td> <Td dataLabel={i18n._(t`Status`)}> {project.summary_fields.last_job && ( diff --git a/awx/ui_next/src/screens/Project/data.project.json b/awx/ui_next/src/screens/Project/data.project.json index c0e9ccaef99c..69ee22a34e09 100644 --- a/awx/ui_next/src/screens/Project/data.project.json +++ b/awx/ui_next/src/screens/Project/data.project.json @@ -30,6 +30,12 @@ "name": "Default", "description": "" }, + "execution_environment": { + "id": 1, + "name": "Default EE", + "description": "", + "image": "quay.io/ansible/awx-ee" + }, "last_job": { "id": 8, "name": "Mike's Project", @@ -111,5 +117,6 @@ "allow_override": false, "custom_virtualenv": null, "last_update_failed": false, - "last_updated": "2019-09-30T18:06:34.713654Z" + "last_updated": "2019-09-30T18:06:34.713654Z", + "execution_environment": 1 } \ No newline at end of file diff --git a/awx/ui_next/src/screens/Project/shared/ProjectForm.jsx b/awx/ui_next/src/screens/Project/shared/ProjectForm.jsx index b2b5b80486b4..7b110ba3cefc 100644 --- a/awx/ui_next/src/screens/Project/shared/ProjectForm.jsx +++ b/awx/ui_next/src/screens/Project/shared/ProjectForm.jsx @@ -19,7 +19,6 @@ import { FormColumnLayout, SubFormLayout, } from '../../../components/FormLayout'; -import Popover from '../../../components/Popover'; import { GitSubForm, SvnSubForm, @@ -96,7 +95,6 @@ function ProjectFormFields({ name: 'scm_type', validate: required(i18n._(t`Set a value for this field`), i18n), }); - const [venvField] = useField('custom_virtualenv'); const [organizationField, organizationMeta, organizationHelpers] = useField({ name: 'organization', validate: required(i18n._(t`Select a value for this field`), i18n), @@ -293,42 +291,6 @@ function ProjectFormFields({ </FormColumnLayout> </SubFormLayout> )} - <Config> - {({ custom_virtualenvs }) => - custom_virtualenvs && - custom_virtualenvs.length > 1 && ( - <FormGroup - fieldId="project-custom-virtualenv" - label={i18n._(t`Ansible Environment`)} - labelIcon={ - <Popover - content={i18n._(t`Select the playbook to be executed by - this job.`)} - /> - } - > - <AnsibleSelect - id="project-custom-virtualenv" - data={[ - { - label: i18n._(t`Use Default Ansible Environment`), - value: '/var/lib/awx/venv/ansible/', - key: 'default', - }, - ...custom_virtualenvs - .filter(datum => datum !== '/var/lib/awx/venv/ansible/') - .map(datum => ({ - label: datum, - value: datum, - key: datum, - })), - ]} - {...venvField} - /> - </FormGroup> - ) - } - </Config> </> ); } @@ -397,7 +359,6 @@ function ProjectForm({ i18n, project, submitError, ...props }) { allow_override: project.allow_override || false, base_dir: project_base_dir || '', credential: project.credential || '', - custom_virtualenv: project.custom_virtualenv || '', description: project.description || '', local_path: project.local_path || '', name: project.name || '', diff --git a/awx/ui_next/src/screens/Project/shared/ProjectForm.test.jsx b/awx/ui_next/src/screens/Project/shared/ProjectForm.test.jsx index 7e88bc7f10a9..33c528c45c15 100644 --- a/awx/ui_next/src/screens/Project/shared/ProjectForm.test.jsx +++ b/awx/ui_next/src/screens/Project/shared/ProjectForm.test.jsx @@ -107,15 +107,9 @@ describe('<ProjectForm />', () => { }); test('new form displays primary form fields', async () => { - const config = { - custom_virtualenvs: ['venv/foo', 'venv/bar'], - }; await act(async () => { wrapper = mountWithContexts( - <ProjectForm handleSubmit={jest.fn()} handleCancel={jest.fn()} />, - { - context: { config }, - } + <ProjectForm handleSubmit={jest.fn()} handleCancel={jest.fn()} /> ); }); await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); @@ -126,7 +120,7 @@ describe('<ProjectForm />', () => { wrapper.find('FormGroup[label="Source Control Credential Type"]').length ).toBe(1); expect(wrapper.find('FormGroup[label="Ansible Environment"]').length).toBe( - 1 + 0 ); expect(wrapper.find('FormGroup[label="Options"]').length).toBe(0); }); diff --git a/awx/ui_next/src/screens/Team/TeamEdit/TeamEdit.jsx b/awx/ui_next/src/screens/Team/TeamEdit/TeamEdit.jsx index 84d01c2341d3..55c518b2282a 100644 --- a/awx/ui_next/src/screens/Team/TeamEdit/TeamEdit.jsx +++ b/awx/ui_next/src/screens/Team/TeamEdit/TeamEdit.jsx @@ -46,8 +46,4 @@ TeamEdit.propTypes = { team: PropTypes.shape().isRequired, }; -TeamEdit.contextTypes = { - custom_virtualenvs: PropTypes.arrayOf(PropTypes.string), -}; - export default TeamEdit; diff --git a/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.test.jsx b/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.test.jsx index 0836d35bd53f..ae31308f4bbf 100644 --- a/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.test.jsx +++ b/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.test.jsx @@ -215,7 +215,10 @@ CredentialsAPI.read.mockResolvedValue({ CredentialTypesAPI.loadAllTypes.mockResolvedValue([]); ExecutionEnvironmentsAPI.read.mockResolvedValue({ - data: mockExecutionEnvironment, + data: { + results: mockExecutionEnvironment, + count: 1, + }, }); describe('<JobTemplateEdit />', () => { diff --git a/awx/ui_next/src/screens/Template/WorkflowJobTemplateEdit/WorkflowJobTemplateEdit.test.jsx b/awx/ui_next/src/screens/Template/WorkflowJobTemplateEdit/WorkflowJobTemplateEdit.test.jsx index 3e5d0fb0e3f7..1f760bb302fa 100644 --- a/awx/ui_next/src/screens/Template/WorkflowJobTemplateEdit/WorkflowJobTemplateEdit.test.jsx +++ b/awx/ui_next/src/screens/Template/WorkflowJobTemplateEdit/WorkflowJobTemplateEdit.test.jsx @@ -68,7 +68,10 @@ describe('<WorkflowJobTemplateEdit/>', () => { }); OrganizationsAPI.read.mockResolvedValue({ results: [{ id: 1 }] }); ExecutionEnvironmentsAPI.read.mockResolvedValue({ - data: mockExecutionEnvironment, + data: { + results: mockExecutionEnvironment, + count: 1, + }, }); await act(async () => { diff --git a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx index e28b12ef478a..7a45410d8e62 100644 --- a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx +++ b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx @@ -732,7 +732,7 @@ const FormikApp = withFormik({ i18n._(t`a new webhook key will be generated on save.`).toUpperCase(), webhook_credential: template?.summary_fields?.webhook_credential || null, execution_environment: - template.summary_fields?.execution_environment || '', + template.summary_fields?.execution_environment || null, }; }, handleSubmit: async (values, { props, setErrors }) => { diff --git a/awx/ui_next/src/screens/Template/shared/WorkflowJobTemplateForm.jsx b/awx/ui_next/src/screens/Template/shared/WorkflowJobTemplateForm.jsx index 6108b0c8f7da..01fd9378c0f9 100644 --- a/awx/ui_next/src/screens/Template/shared/WorkflowJobTemplateForm.jsx +++ b/awx/ui_next/src/screens/Template/shared/WorkflowJobTemplateForm.jsx @@ -322,7 +322,7 @@ const FormikApp = withFormik({ : '', webhook_key: template.webhook_key || '', execution_environment: - template.summary_fields?.execution_environment || '', + template.summary_fields?.execution_environment || null, }; }, handleSubmit: async (values, { props, setErrors }) => { diff --git a/awx/ui_next/src/types.js b/awx/ui_next/src/types.js index 744360eaea45..de27eadb9f2c 100644 --- a/awx/ui_next/src/types.js +++ b/awx/ui_next/src/types.js @@ -410,9 +410,10 @@ export const WorkflowApproval = shape({ export const ExecutionEnvironment = shape({ id: number.isRequired, + name: string, organization: number, credential: number, - image: string.isRequired, + image: string, url: string, summary_fields: shape({}), description: string,