|
-
- {organization.name}
-
+
+
+ {organization.name}
+
+
+ {missingExecutionEnvironment && (
+
+
+
+
+
+ )}
|
{organization.summary_fields.related_field_counts.users}
diff --git a/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationListItem.test.jsx b/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationListItem.test.jsx
index 1e3bce02e458..c51a6ae67933 100644
--- a/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationListItem.test.jsx
+++ b/awx/ui_next/src/screens/Organization/OrganizationList/OrganizationListItem.test.jsx
@@ -7,7 +7,7 @@ import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
import OrganizationListItem from './OrganizationListItem';
describe('', () => {
- test('initially renders succesfully', () => {
+ test('initially renders successfully', () => {
mountWithContexts(
@@ -101,4 +101,36 @@ describe('', () => {
);
expect(wrapper.find('PencilAltIcon').exists()).toBeFalsy();
});
+
+ test('should render warning about missing execution environment', () => {
+ const wrapper = mountWithContexts(
+
+ );
+ expect(
+ wrapper.find('.missing-execution-environment').prop('content')
+ ).toEqual('Organization is missing an execution environment.');
+ });
});
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 && (
-
- value !== defaultVenv.value)
- .map(value => ({ value, label: value, key: value })),
- ]}
- {...venvField}
- />
-
- )}
@@ -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('', () => {
name: 'Foo',
description: 'Bar',
max_hosts: 1,
- custom_virtualenv: 'Fizz',
related: {
instance_groups: '/api/v2/organizations/1/instance_groups',
},
@@ -32,7 +31,9 @@ describe('', () => {
{ 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('', () => {
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(
- ,
- {
- 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('', () => {
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('', () => {
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`)}`}
/>
-
- {summary_fields?.default_environment?.name && (
-
- {summary_fields.default_environment.name}
-
- }
- />
- )}
{({ project_base_dir }) => (
', () => {
'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 (
@@ -76,6 +86,18 @@ function ProjectJobTemplateListItem({
)}
+ {missingExecutionEnvironment && (
+
+
+
+
+
+ )}
,
{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..8d1710e1eddf 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 (
|
-
- {project.name}
-
+
+
+ {project.name}
+
+
+ {missingExecutionEnvironment && (
+
+
+
+
+
+ )}
|
{project.summary_fields.last_job && (
diff --git a/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.test.jsx b/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.test.jsx
index 21f96efc4de1..c95e8c39bf38 100644
--- a/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.test.jsx
+++ b/awx/ui_next/src/screens/Project/ProjectList/ProjectListItem.test.jsx
@@ -40,6 +40,43 @@ describe('', () => {
expect(wrapper.find('ProjectSyncButton').exists()).toBeTruthy();
});
+ test('should render warning about missing execution environment', () => {
+ const wrapper = mountWithContexts(
+
+
+ {}}
+ project={{
+ id: 1,
+ name: 'Project 1',
+ url: '/api/v2/projects/1',
+ type: 'project',
+ scm_type: 'git',
+ scm_revision: '7788f7erga0jijodfgsjisiodf98sdga9hg9a98gaf',
+ summary_fields: {
+ last_job: {
+ id: 9000,
+ status: 'successful',
+ },
+ user_capabilities: {
+ start: true,
+ },
+ },
+ custom_virtualenv: '/var/lib/awx/env',
+ default_environment: null,
+ }}
+ />
+
+
+ );
+
+ expect(
+ wrapper.find('.missing-execution-environment').prop('content')
+ ).toEqual('Project is missing an execution environment.');
+ });
+
test('launch button hidden from users without start capabilities', () => {
const wrapper = mountWithContexts(
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({
)}
-
- {({ custom_virtualenvs }) =>
- custom_virtualenvs &&
- custom_virtualenvs.length > 1 && (
-
- }
- >
- datum !== '/var/lib/awx/venv/ansible/')
- .map(datum => ({
- label: datum,
- value: datum,
- key: datum,
- })),
- ]}
- {...venvField}
- />
-
- )
- }
-
>
);
}
@@ -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('', () => {
});
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);
@@ -126,7 +120,7 @@ describe('', () => {
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('', () => {
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('', () => {
});
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,
| |