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
This commit is contained in:
nixocio 2021-03-02 16:32:31 -05:00
parent de52adedef
commit babea5d599
63 changed files with 610 additions and 356 deletions

View File

@ -11,14 +11,12 @@ jest.mock('../../api');
describe('<AppContainer />', () => {
const ansible_version = '111';
const custom_virtualenvs = [];
const version = '222';
beforeEach(() => {
ConfigAPI.read.mockResolvedValue({
data: {
ansible_version,
custom_virtualenvs,
version,
},
});

View File

@ -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 (
<Detail
label={label}
value={
<Link
to={`/execution_environments/${executionEnvironment.id}/details`}
>
{executionEnvironment.name}
</Link>
}
dataCy="execution-environment-detail"
/>
);
}
if (virtualEnvironment && !executionEnvironment) {
return (
<Detail
label={label}
value={
<>
{i18n._(t`Missing resource`)}
<span>
<Tooltip
content={i18n._(
t`Custom virtual environment ${virtualEnvironment} must be replaced by an execution environment.`
)}
position="right"
>
<ExclamationTriangleIcon />
</Tooltip>
</span>
</>
}
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);

View File

@ -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('<ExecutionEnvironmentDetail/>', () => {
test('should display execution environment detail', async () => {
const wrapper = mountWithContexts(
<ExecutionEnvironmentDetail
executionEnvironment={mockExecutionEnvironment}
/>
);
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(
<ExecutionEnvironmentDetail
executionEnvironment={mockExecutionEnvironment}
virtualEnvironment={virtualEnvironment}
/>
);
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(
<ExecutionEnvironmentDetail virtualEnvironment={virtualEnvironment} />
);
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.`
);
});
});

View File

@ -0,0 +1 @@
export { default } from './ExecutionEnvironmentDetail';

View File

@ -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 }) {
/>
)}
<Detail label={i18n._(t`Source`)} value={source} />
<Detail
label={i18n._(t`Ansible Environment`)}
value={custom_virtualenv}
/>
{summary_fields?.source_project && (
<Detail
label={i18n._(t`Project`)}
@ -97,6 +94,10 @@ function PromptInventorySourceDetail({ i18n, resource }) {
}
/>
)}
<ExecutionEnvironmentDetail
virtualEnvironment={custom_virtualenv}
executionEnvironment={summary_fields?.execution_environment}
/>
<Detail label={i18n._(t`Inventory File`)} value={source_path} />
<Detail label={i18n._(t`Verbosity`)} value={VERBOSITY[verbosity]} />
<Detail

View File

@ -9,6 +9,7 @@ import ChipGroup from '../ChipGroup';
import Sparkline from '../Sparkline';
import { Detail, DeletedDetail } from '../DetailList';
import { VariablesDetail } from '../CodeEditor';
import ExecutionEnvironmentDetail from '../ExecutionEnvironmentDetail';
import { toTitleCase } from '../../util/strings';
function PromptJobTemplateDetail({ i18n, resource }) {
@ -34,6 +35,7 @@ function PromptJobTemplateDetail({ i18n, resource }) {
verbosity,
webhook_key,
webhook_service,
custom_virtualenv,
} = resource;
const VERBOSITY = {
@ -128,6 +130,10 @@ function PromptJobTemplateDetail({ i18n, resource }) {
) : (
<DeletedDetail label={i18n._(t`Project`)} />
)}
<ExecutionEnvironmentDetail
virtualEnvironment={custom_virtualenv}
executionEnvironment={summary_fields?.execution_environment}
/>
<Detail label={i18n._(t`Source Control Branch`)} value={scm_branch} />
<Detail label={i18n._(t`Playbook`)} value={playbook} />
<Detail label={i18n._(t`Forks`)} value={forks || '0'} />

View File

@ -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 }) {
) : (
<DeletedDetail label={i18n._(t`Organization`)} />
)}
<ExecutionEnvironmentDetail
virtualEnvironment={custom_virtualenv}
executionEnvironment={summary_fields?.default_environment}
isDefaultEnvironment
/>
<Detail
label={i18n._(t`Source Control Type`)}
value={scm_type === '' ? i18n._(t`Manual`) : toTitleCase(scm_type)}
@ -88,10 +94,6 @@ function PromptProjectDetail({ i18n, resource }) {
label={i18n._(t`Cache Timeout`)}
value={`${scm_update_cache_timeout} ${i18n._(t`Seconds`)}`}
/>
<Detail
label={i18n._(t`Ansible Environment`)}
value={custom_virtualenv}
/>
<Config>
{({ project_base_dir }) => (
<Detail

View File

@ -41,10 +41,17 @@ describe('PromptProjectDetail', () => {
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"]')

View File

@ -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
}

View File

@ -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
}

View File

@ -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,19 @@ function TemplateListItem({
</Tooltip>
</span>
)}
{missingExecutionEnvironment && (
<span>
<Tooltip
className="missing-execution-environment"
content={i18n._(
t`Custom virtual environment ${template.custom_virtualenv} must be replaced by an execution environment.`
)}
position="right"
>
<ExclamationTriangleIconWarning />
</Tooltip>
</span>
)}
</Td>
<Td dataLabel={i18n._(t`Type`)}>{toTitleCase(template.type)}</Td>
<Td dataLabel={i18n._(t`Last Ran`)}>{lastRun}</Td>

View File

@ -320,4 +320,61 @@ describe('<TemplateListItem />', () => {
);
expect(wrapper.find('ProjectDiagramIcon').length).toBe(0);
});
test('should render warning about missing execution environment', () => {
const wrapper = mountWithContexts(
<table>
<tbody>
<TemplateListItem
isSelected={false}
template={{
id: 1,
name: 'Template 1',
url: '/templates/job_template/1',
type: 'job_template',
summary_fields: {
inventory: {
id: 1,
name: 'Demo Inventory',
description: '',
has_active_failures: false,
total_hosts: 0,
hosts_with_active_failures: 0,
total_groups: 0,
has_inventory_sources: false,
total_inventory_sources: 0,
inventory_sources_with_failures: 0,
organization_id: 1,
kind: '',
},
project: {
id: 6,
name: 'Demo Project',
description: '',
status: 'never updated',
scm_type: 'git',
},
user_capabilities: {
edit: true,
delete: true,
start: true,
schedule: true,
copy: true,
},
},
custom_virtualenv: '/var/lib/awx/env',
execution_environment: null,
project: 6,
inventory: 1,
}}
/>
</tbody>
</table>
);
expect(
wrapper.find('.missing-execution-environment').prop('content')
).toEqual(
'Custom virtual environment /var/lib/awx/env must be replaced by an execution environment.'
);
});
});

View File

@ -74,15 +74,9 @@ describe('<InventorySourceAdd />', () => {
});
test('new form displays primary form fields', async () => {
const config = {
custom_virtualenvs: ['venv/foo', 'venv/bar'],
};
await act(async () => {
wrapper = mountWithContexts(
<InventorySourceAdd inventory={mockInventory} />,
{
context: { config },
}
<InventorySourceAdd inventory={mockInventory} />
);
});
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
@ -90,7 +84,7 @@ describe('<InventorySourceAdd />', () => {
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
);
});

View File

@ -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 }) {
}
/>
)}
<Detail
label={i18n._(t`Ansible environment`)}
value={custom_virtualenv}
<ExecutionEnvironmentDetail
virtualEnvironment={custom_virtualenv}
executionEnvironment={execution_environment}
/>
{source_project && (
<Detail
@ -215,18 +216,6 @@ function InventorySourceDetail({ inventorySource, i18n }) {
}
/>
)}
{execution_environment?.name && (
<Detail
label={i18n._(t`Execution Environment`)}
value={
<Link
to={`/execution_environments/${execution_environment.id}/details`}
>
{execution_environment.name}
</Link>
}
/>
)}
{source === 'scm' ? (
<Detail
label={i18n._(t`Inventory file`)}

View File

@ -58,11 +58,19 @@ describe('InventorySourceDetail', () => {
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'

View File

@ -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 (
<>
<DataListItem aria-labelledby={`check-action-${source.id}`}>
@ -79,6 +93,19 @@ function InventorySourceListItem({
<b>{source.name}</b>
</Link>
</span>
{missingExecutionEnvironment && (
<span>
<Tooltip
className="missing-execution-environment"
content={i18n._(
t`Custom virtual environment ${source.custom_virtualenv} must be replaced by an execution environment.`
)}
position="right"
>
<ExclamationTriangleIcon />
</Tooltip>
</span>
)}
</DataListCell>,
<DataListCell aria-label={i18n._(t`type`)} key="type">
{label}

View File

@ -139,4 +139,25 @@ describe('<InventorySourceListItem />', () => {
expect(wrapper.find('Button[aria-label="Edit Source"]').length).toBe(0);
expect(wrapper.find('InventorySourceSyncButton').length).toBe(1);
});
test('should render warning about missing execution environment', () => {
const onSelect = jest.fn();
wrapper = mountWithContexts(
<InventorySourceListItem
source={{
...source,
custom_virtualenv: '/var/lib/awx/env',
execution_environment: null,
}}
isSelected={false}
onSelect={onSelect}
label="Source Bar"
/>
);
expect(
wrapper.find('.missing-execution-environment').prop('content')
).toEqual(
'Custom virtual environment /var/lib/awx/env must be replaced by an execution environment.'
);
});
});

View File

@ -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 = ({
}}
/>
</FormGroup>
{custom_virtualenvs && custom_virtualenvs.length > 1 && (
<FormGroup
fieldId="custom-virtualenv"
label={i18n._(t`Ansible Environment`)}
labelIcon={
<Popover
content={i18n._(t`Select the custom
Python virtual environment for this
inventory source sync to run on.`)}
/>
}
>
<AnsibleSelect
id="custom-virtualenv"
data={[
defaultVenv,
...custom_virtualenvs
.filter(value => value !== defaultVenv.value)
.map(value => ({ value, label: value, key: value })),
]}
{...venvField}
/>
</FormGroup>
)}
{!['', 'custom'].includes(sourceField.value) && (
<SubFormLayout>
<Title size="md" headingLevel="h4">
@ -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,

View File

@ -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);
});

View File

@ -9,7 +9,6 @@ jest.mock('../../../../api/models/Credentials');
const initialValues = {
credential: null,
custom_virtualenv: '',
overwrite: false,
overwrite_vars: false,
source_path: '',

View File

@ -9,7 +9,6 @@ jest.mock('../../../../api/models/Credentials');
const initialValues = {
credential: null,
custom_virtualenv: '',
overwrite: false,
overwrite_vars: false,
source_path: '',

View File

@ -9,7 +9,6 @@ jest.mock('../../../../api/models/Credentials');
const initialValues = {
credential: null,
custom_virtualenv: '',
overwrite: false,
overwrite_vars: false,
source_path: '',

View File

@ -9,7 +9,6 @@ jest.mock('../../../../api/models/Credentials');
const initialValues = {
credential: null,
custom_virtualenv: '',
overwrite: false,
overwrite_vars: false,
source_path: '',

View File

@ -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',

View File

@ -9,7 +9,6 @@ jest.mock('../../../../api/models/Credentials');
const initialValues = {
credential: null,
custom_virtualenv: '',
overwrite: false,
overwrite_vars: false,
source_path: '',

View File

@ -9,7 +9,6 @@ jest.mock('../../../../api/models/Credentials');
const initialValues = {
credential: null,
custom_virtualenv: '',
overwrite: false,
overwrite_vars: false,
source_path: '',

View File

@ -9,7 +9,6 @@ jest.mock('../../../../api/models/Credentials');
const initialValues = {
credential: null,
custom_virtualenv: '',
overwrite: false,
overwrite_vars: false,
source_path: '',

View File

@ -9,7 +9,6 @@ jest.mock('../../../../api/models/Credentials');
const initialValues = {
credential: null,
custom_virtualenv: '',
overwrite: false,
overwrite_vars: false,
source_path: '',

View File

@ -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
}

View File

@ -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

View File

@ -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`
);

View File

@ -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
}

View File

@ -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);

View File

@ -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;

View File

@ -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;

View File

@ -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);

View File

@ -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}

View File

@ -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' },

View File

@ -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;

View File

@ -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')(

View File

@ -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,24 @@ 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
className="missing-execution-environment"
content={i18n._(
t`Custom virtual environment ${organization.custom_virtualenv} must be replaced by an execution environment.`
)}
position="right"
>
<ExclamationTriangleIcon />
</Tooltip>
</span>
)}
</Td>
<Td dataLabel={i18n._(t`Members`)}>
{organization.summary_fields.related_field_counts.users}

View File

@ -7,7 +7,7 @@ import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
import OrganizationListItem from './OrganizationListItem';
describe('<OrganizationListItem />', () => {
test('initially renders succesfully', () => {
test('initially renders successfully', () => {
mountWithContexts(
<I18nProvider>
<MemoryRouter initialEntries={['/organizations']} initialIndex={0}>
@ -101,4 +101,38 @@ describe('<OrganizationListItem />', () => {
);
expect(wrapper.find('PencilAltIcon').exists()).toBeFalsy();
});
test('should render warning about missing execution environment', () => {
const wrapper = mountWithContexts(
<table>
<tbody>
<OrganizationListItem
organization={{
id: 1,
name: 'Org',
summary_fields: {
related_field_counts: {
users: 1,
teams: 1,
},
user_capabilities: {
edit: true,
},
},
custom_virtualenv: '/var/lib/awx/env',
default_environment: null,
}}
detailUrl="/organization/1"
isSelected
onSelect={() => {}}
/>
</tbody>
</table>
);
expect(
wrapper.find('.missing-execution-environment').prop('content')
).toEqual(
'Custom virtual environment /var/lib/awx/env must be replaced by an execution environment.'
);
});
});

View File

@ -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);

View File

@ -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,
},
[],
[]

View File

@ -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';
@ -124,23 +125,11 @@ function ProjectDetail({ project, i18n }) {
label={i18n._(t`Cache Timeout`)}
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

View File

@ -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');

View File

@ -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_virtualenv &&
!template.execution_environment;
return (
<DataListItem aria-labelledby={labelId} id={`${template.id}`}>
<DataListItemRow>
@ -76,6 +86,19 @@ function ProjectJobTemplateListItem({
</Tooltip>
</span>
)}
{missingExecutionEnvironment && (
<span>
<Tooltip
content={i18n._(
t`Custom virtual environment ${template.custom_virtualenv} must be replaced by an execution environment.`
)}
position="right"
className="missing-execution-environment"
>
<ExclamationTriangleIconWarning />
</Tooltip>
</span>
)}
</DataListCell>,
<DataListCell key="type">
{toTitleCase(template.type)}

View File

@ -186,4 +186,31 @@ describe('<ProjectJobTemplatesListItem />', () => {
'/templates/job_template/2/details'
);
});
test('should render warning about missing execution environment', () => {
const wrapper = mountWithContexts(
<ProjectJobTemplatesListItem
isSelected={false}
template={{
id: 1,
name: 'Template 1',
url: '/templates/job_template/1',
type: 'job_template',
summary_fields: {
user_capabilities: {
edit: true,
},
},
custom_virtualenv: '/var/lib/awx/env',
execution_environment: null,
}}
/>
);
expect(
wrapper.find('.missing-execution-environment').prop('content')
).toEqual(
'Custom virtual environment /var/lib/awx/env must be replaced by an execution environment.'
);
});
});

View File

@ -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,24 @@ 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`Custom virtual environment ${project.custom_virtualenv} must be replaced by an execution environment.`
)}
position="right"
className="missing-execution-environment"
>
<ExclamationTriangleIcon />
</Tooltip>
</span>
)}
</Td>
<Td dataLabel={i18n._(t`Status`)}>
{project.summary_fields.last_job && (

View File

@ -40,6 +40,45 @@ describe('<ProjectsListItem />', () => {
expect(wrapper.find('ProjectSyncButton').exists()).toBeTruthy();
});
test('should render warning about missing execution environment', () => {
const wrapper = mountWithContexts(
<table>
<tbody>
<ProjectsListItem
isSelected={false}
detailUrl="/project/1"
onSelect={() => {}}
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,
}}
/>
</tbody>
</table>
);
expect(
wrapper.find('.missing-execution-environment').prop('content')
).toEqual(
'Custom virtual environment /var/lib/awx/env must be replaced by an execution environment.'
);
});
test('launch button hidden from users without start capabilities', () => {
const wrapper = mountWithContexts(
<table>

View File

@ -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
}

View File

@ -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 || '',

View File

@ -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);
});

View File

@ -37,7 +37,6 @@ function MiscSystemDetail({ i18n }) {
'AUTH_BASIC_ENABLED',
'AUTOMATION_ANALYTICS_GATHER_INTERVAL',
'AUTOMATION_ANALYTICS_URL',
'CUSTOM_VENV_PATHS',
'INSIGHTS_TRACKING_STATE',
'LOGIN_REDIRECT_OVERRIDE',
'MANAGE_ORGANIZATION_AUTH',

View File

@ -110,7 +110,6 @@ describe('<MiscSystemDetail />', () => {
assertDetail(wrapper, 'Red Hat customer username', 'mock name');
assertDetail(wrapper, 'Refresh Token Expiration', '3 seconds');
assertVariableDetail(wrapper, 'Remote Host Headers', '[]');
assertVariableDetail(wrapper, 'Custom virtual environment paths', '[]');
});
test('should hide edit button from non-superusers', async () => {

View File

@ -130,7 +130,6 @@ function MiscSystemEdit({ i18n }) {
} = form;
await submitForm({
...formData,
CUSTOM_VENV_PATHS: formatJson(formData.CUSTOM_VENV_PATHS),
REMOTE_HOST_HEADERS: formatJson(formData.REMOTE_HOST_HEADERS),
OAUTH2_PROVIDER: {
ACCESS_TOKEN_EXPIRE_SECONDS,
@ -271,10 +270,6 @@ function MiscSystemEdit({ i18n }) {
config={system.REMOTE_HOST_HEADERS}
isRequired
/>
<ObjectField
name="CUSTOM_VENV_PATHS"
config={system.CUSTOM_VENV_PATHS}
/>
{submitError && <FormSubmitError error={submitError} />}
</FormColumnLayout>
<RevertFormActionGroup

View File

@ -46,8 +46,4 @@ TeamEdit.propTypes = {
team: PropTypes.shape().isRequired,
};
TeamEdit.contextTypes = {
custom_virtualenvs: PropTypes.arrayOf(PropTypes.string),
};
export default TeamEdit;

View File

@ -30,6 +30,7 @@ import { LaunchButton } from '../../../components/LaunchButton';
import { VariablesDetail } from '../../../components/CodeEditor';
import { JobTemplatesAPI } from '../../../api';
import useRequest, { useDismissableError } from '../../../util/useRequest';
import ExecutionEnvironmentDetail from '../../../components/ExecutionEnvironmentDetail';
function JobTemplateDetail({ i18n, template }) {
const {
@ -58,6 +59,7 @@ function JobTemplateDetail({ i18n, template }) {
webhook_service,
related: { webhook_receiver },
webhook_key,
custom_virtualenv,
} = template;
const { id: templateId } = useParams();
const history = useHistory();
@ -206,18 +208,10 @@ function JobTemplateDetail({ i18n, template }) {
) : (
<DeletedDetail label={i18n._(t`Project`)} />
)}
{summary_fields?.execution_environment && (
<Detail
label={i18n._(t`Execution Environment`)}
value={
<Link
to={`/execution_environments/${summary_fields.execution_environment.id}/details`}
>
{summary_fields.execution_environment.name}
</Link>
}
/>
)}
<ExecutionEnvironmentDetail
virtualEnvironment={custom_virtualenv}
executionEnvironment={summary_fields?.execution_environment}
/>
<Detail
label={i18n._(t`Source Control Branch`)}
value={template.scm_branch}

View File

@ -215,7 +215,10 @@ CredentialsAPI.read.mockResolvedValue({
CredentialTypesAPI.loadAllTypes.mockResolvedValue([]);
ExecutionEnvironmentsAPI.read.mockResolvedValue({
data: mockExecutionEnvironment,
data: {
results: mockExecutionEnvironment,
count: 1,
},
});
describe('<JobTemplateEdit />', () => {

View File

@ -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 () => {

View File

@ -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 }) => {

View File

@ -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 }) => {

View File

@ -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,