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 />', () => { describe('<AppContainer />', () => {
const ansible_version = '111'; const ansible_version = '111';
const custom_virtualenvs = [];
const version = '222'; const version = '222';
beforeEach(() => { beforeEach(() => {
ConfigAPI.read.mockResolvedValue({ ConfigAPI.read.mockResolvedValue({
data: { data: {
ansible_version, ansible_version,
custom_virtualenvs,
version, 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 { VariablesDetail } from '../CodeEditor';
import CredentialChip from '../CredentialChip'; import CredentialChip from '../CredentialChip';
import ChipGroup from '../ChipGroup'; import ChipGroup from '../ChipGroup';
import ExecutionEnvironmentDetail from '../ExecutionEnvironmentDetail';
function PromptInventorySourceDetail({ i18n, resource }) { function PromptInventorySourceDetail({ i18n, resource }) {
const { const {
@@ -83,10 +84,6 @@ function PromptInventorySourceDetail({ i18n, resource }) {
/> />
)} )}
<Detail label={i18n._(t`Source`)} value={source} /> <Detail label={i18n._(t`Source`)} value={source} />
<Detail
label={i18n._(t`Ansible Environment`)}
value={custom_virtualenv}
/>
{summary_fields?.source_project && ( {summary_fields?.source_project && (
<Detail <Detail
label={i18n._(t`Project`)} 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`Inventory File`)} value={source_path} />
<Detail label={i18n._(t`Verbosity`)} value={VERBOSITY[verbosity]} /> <Detail label={i18n._(t`Verbosity`)} value={VERBOSITY[verbosity]} />
<Detail <Detail

View File

@@ -9,6 +9,7 @@ import ChipGroup from '../ChipGroup';
import Sparkline from '../Sparkline'; import Sparkline from '../Sparkline';
import { Detail, DeletedDetail } from '../DetailList'; import { Detail, DeletedDetail } from '../DetailList';
import { VariablesDetail } from '../CodeEditor'; import { VariablesDetail } from '../CodeEditor';
import ExecutionEnvironmentDetail from '../ExecutionEnvironmentDetail';
import { toTitleCase } from '../../util/strings'; import { toTitleCase } from '../../util/strings';
function PromptJobTemplateDetail({ i18n, resource }) { function PromptJobTemplateDetail({ i18n, resource }) {
@@ -34,6 +35,7 @@ function PromptJobTemplateDetail({ i18n, resource }) {
verbosity, verbosity,
webhook_key, webhook_key,
webhook_service, webhook_service,
custom_virtualenv,
} = resource; } = resource;
const VERBOSITY = { const VERBOSITY = {
@@ -128,6 +130,10 @@ function PromptJobTemplateDetail({ i18n, resource }) {
) : ( ) : (
<DeletedDetail label={i18n._(t`Project`)} /> <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`Source Control Branch`)} value={scm_branch} />
<Detail label={i18n._(t`Playbook`)} value={playbook} /> <Detail label={i18n._(t`Playbook`)} value={playbook} />
<Detail label={i18n._(t`Forks`)} value={forks || '0'} /> <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 { Detail, DeletedDetail } from '../DetailList';
import CredentialChip from '../CredentialChip'; import CredentialChip from '../CredentialChip';
import { toTitleCase } from '../../util/strings'; import { toTitleCase } from '../../util/strings';
import ExecutionEnvironmentDetail from '../ExecutionEnvironmentDetail';
function PromptProjectDetail({ i18n, resource }) { function PromptProjectDetail({ i18n, resource }) {
const { const {
@@ -64,6 +65,11 @@ function PromptProjectDetail({ i18n, resource }) {
) : ( ) : (
<DeletedDetail label={i18n._(t`Organization`)} /> <DeletedDetail label={i18n._(t`Organization`)} />
)} )}
<ExecutionEnvironmentDetail
virtualEnvironment={custom_virtualenv}
executionEnvironment={summary_fields?.default_environment}
isDefaultEnvironment
/>
<Detail <Detail
label={i18n._(t`Source Control Type`)} label={i18n._(t`Source Control Type`)}
value={scm_type === '' ? i18n._(t`Manual`) : toTitleCase(scm_type)} value={scm_type === '' ? i18n._(t`Manual`) : toTitleCase(scm_type)}
@@ -88,10 +94,6 @@ function PromptProjectDetail({ i18n, resource }) {
label={i18n._(t`Cache Timeout`)} label={i18n._(t`Cache Timeout`)}
value={`${scm_update_cache_timeout} ${i18n._(t`Seconds`)}`} value={`${scm_update_cache_timeout} ${i18n._(t`Seconds`)}`}
/> />
<Detail
label={i18n._(t`Ansible Environment`)}
value={custom_virtualenv}
/>
<Config> <Config>
{({ project_base_dir }) => ( {({ project_base_dir }) => (
<Detail <Detail

View File

@@ -41,10 +41,17 @@ describe('PromptProjectDetail', () => {
assertDetail(wrapper, 'Source Control Branch', 'foo'); assertDetail(wrapper, 'Source Control Branch', 'foo');
assertDetail(wrapper, 'Source Control Refspec', 'refs/'); assertDetail(wrapper, 'Source Control Refspec', 'refs/');
assertDetail(wrapper, 'Cache Timeout', '3 Seconds'); assertDetail(wrapper, 'Cache Timeout', '3 Seconds');
assertDetail(wrapper, 'Ansible Environment', 'mock virtual env');
assertDetail(wrapper, 'Project Base Path', 'dir/foo/bar'); assertDetail(wrapper, 'Project Base Path', 'dir/foo/bar');
assertDetail(wrapper, 'Playbook Directory', '_6__demo_project'); assertDetail(wrapper, 'Playbook Directory', '_6__demo_project');
assertDetail(wrapper, 'Source Control Credential', 'Scm: mock scm'); 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( expect(
wrapper wrapper
.find('Detail[label="Options"]') .find('Detail[label="Options"]')

View File

@@ -45,6 +45,12 @@
"organization_id": 1, "organization_id": 1,
"kind": "" "kind": ""
}, },
"execution_environment": {
"id": 1,
"name": "Default EE",
"description": "",
"image": "quay.io/ansible/awx-ee"
},
"project": { "project": {
"id": 6, "id": 6,
"name": "Mock Project", "name": "Mock Project",
@@ -192,5 +198,6 @@
"custom_virtualenv": null, "custom_virtualenv": null,
"job_slice_count": 1, "job_slice_count": 1,
"webhook_service": "github", "webhook_service": "github",
"webhook_credential": 8 "webhook_credential": 8,
"execution_environment": 1
} }

View File

@@ -28,6 +28,12 @@
"name":"Default", "name":"Default",
"description":"" "description":""
}, },
"default_environment": {
"id": 1,
"name": "Default EE",
"description": "",
"image": "quay.io/ansible/awx-ee"
},
"credential": { "credential": {
"id": 9, "id": 9,
"name": "mock scm", "name": "mock scm",
@@ -103,5 +109,6 @@
"allow_override":true, "allow_override":true,
"custom_virtualenv": "mock virtual env", "custom_virtualenv": "mock virtual env",
"last_update_failed":false, "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, ProjectDiagramIcon,
RocketIcon, RocketIcon,
} from '@patternfly/react-icons'; } from '@patternfly/react-icons';
import styled from 'styled-components';
import { ActionsTd, ActionItem } from '../PaginatedTable'; import { ActionsTd, ActionItem } from '../PaginatedTable';
import { DetailList, Detail, DeletedDetail } from '../DetailList'; import { DetailList, Detail, DeletedDetail } from '../DetailList';
import ChipGroup from '../ChipGroup'; import ChipGroup from '../ChipGroup';
@@ -23,6 +25,11 @@ import Sparkline from '../Sparkline';
import { toTitleCase } from '../../util/strings'; import { toTitleCase } from '../../util/strings';
import CopyButton from '../CopyButton'; import CopyButton from '../CopyButton';
const ExclamationTriangleIconWarning = styled(ExclamationTriangleIcon)`
color: var(--pf-global--warning-color--100);
margin-left: 18px;
`;
function TemplateListItem({ function TemplateListItem({
i18n, i18n,
template, template,
@@ -67,6 +74,11 @@ function TemplateListItem({
(!summaryFields.project || (!summaryFields.project ||
(!summaryFields.inventory && !askInventoryOnLaunch)); (!summaryFields.inventory && !askInventoryOnLaunch));
const missingExecutionEnvironment =
template.type === 'job_template' &&
template.custom_virtualenv &&
!template.execution_environment;
const inventoryValue = (kind, id) => { const inventoryValue = (kind, id) => {
const inventorykind = kind === 'smart' ? 'smart_inventory' : 'inventory'; const inventorykind = kind === 'smart' ? 'smart_inventory' : 'inventory';
@@ -125,6 +137,19 @@ function TemplateListItem({
</Tooltip> </Tooltip>
</span> </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>
<Td dataLabel={i18n._(t`Type`)}>{toTitleCase(template.type)}</Td> <Td dataLabel={i18n._(t`Type`)}>{toTitleCase(template.type)}</Td>
<Td dataLabel={i18n._(t`Last Ran`)}>{lastRun}</Td> <Td dataLabel={i18n._(t`Last Ran`)}>{lastRun}</Td>

View File

@@ -320,4 +320,61 @@ describe('<TemplateListItem />', () => {
); );
expect(wrapper.find('ProjectDiagramIcon').length).toBe(0); 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 () => { test('new form displays primary form fields', async () => {
const config = {
custom_virtualenvs: ['venv/foo', 'venv/bar'],
};
await act(async () => { await act(async () => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
<InventorySourceAdd inventory={mockInventory} />, <InventorySourceAdd inventory={mockInventory} />
{
context: { config },
}
); );
}); });
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); 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="Description"]')).toHaveLength(1);
expect(wrapper.find('FormGroup[label="Source"]')).toHaveLength(1); expect(wrapper.find('FormGroup[label="Source"]')).toHaveLength(1);
expect(wrapper.find('FormGroup[label="Ansible Environment"]')).toHaveLength( 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 ContentLoading from '../../../components/ContentLoading';
import CredentialChip from '../../../components/CredentialChip'; import CredentialChip from '../../../components/CredentialChip';
import DeleteButton from '../../../components/DeleteButton'; import DeleteButton from '../../../components/DeleteButton';
import ExecutionEnvironmentDetail from '../../../components/ExecutionEnvironmentDetail';
import InventorySourceSyncButton from '../shared/InventorySourceSyncButton'; import InventorySourceSyncButton from '../shared/InventorySourceSyncButton';
import { import {
DetailList, DetailList,
@@ -201,9 +202,9 @@ function InventorySourceDetail({ inventorySource, i18n }) {
} }
/> />
)} )}
<Detail <ExecutionEnvironmentDetail
label={i18n._(t`Ansible environment`)} virtualEnvironment={custom_virtualenv}
value={custom_virtualenv} executionEnvironment={execution_environment}
/> />
{source_project && ( {source_project && (
<Detail <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' ? ( {source === 'scm' ? (
<Detail <Detail
label={i18n._(t`Inventory file`)} label={i18n._(t`Inventory file`)}

View File

@@ -58,11 +58,19 @@ describe('InventorySourceDetail', () => {
assertDetail(wrapper, 'Description', 'mock description'); assertDetail(wrapper, 'Description', 'mock description');
assertDetail(wrapper, 'Source', 'Sourced from a Project'); assertDetail(wrapper, 'Source', 'Sourced from a Project');
assertDetail(wrapper, 'Organization', 'Mock Org'); assertDetail(wrapper, 'Organization', 'Mock Org');
assertDetail(wrapper, 'Ansible environment', '/var/lib/awx/venv/custom');
assertDetail(wrapper, 'Project', 'Mock Project'); assertDetail(wrapper, 'Project', 'Mock Project');
assertDetail(wrapper, 'Inventory file', 'foo'); assertDetail(wrapper, 'Inventory file', 'foo');
assertDetail(wrapper, 'Verbosity', '2 (Debug)'); assertDetail(wrapper, 'Verbosity', '2 (Debug)');
assertDetail(wrapper, 'Cache timeout', '2 seconds'); 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('CredentialChip').text()).toBe('Cloud: mock cred');
expect(wrapper.find('VariablesDetail').prop('value')).toEqual( expect(wrapper.find('VariablesDetail').prop('value')).toEqual(
'---\nfoo: bar' '---\nfoo: bar'

View File

@@ -12,10 +12,20 @@ import {
DataListAction, DataListAction,
Tooltip, Tooltip,
} from '@patternfly/react-core'; } 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 StatusIcon from '../../../components/StatusIcon';
import InventorySourceSyncButton from '../shared/InventorySourceSyncButton'; import InventorySourceSyncButton from '../shared/InventorySourceSyncButton';
const ExclamationTriangleIcon = styled(PFExclamationTriangleIcon)`
color: var(--pf-global--warning-color--100);
margin-left: 18px;
`;
function InventorySourceListItem({ function InventorySourceListItem({
source, source,
isSelected, isSelected,
@@ -42,6 +52,10 @@ function InventorySourceListItem({
</> </>
); );
}; };
const missingExecutionEnvironment =
source.custom_virtualenv && !source.execution_environment;
return ( return (
<> <>
<DataListItem aria-labelledby={`check-action-${source.id}`}> <DataListItem aria-labelledby={`check-action-${source.id}`}>
@@ -79,6 +93,19 @@ function InventorySourceListItem({
<b>{source.name}</b> <b>{source.name}</b>
</Link> </Link>
</span> </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>,
<DataListCell aria-label={i18n._(t`type`)} key="type"> <DataListCell aria-label={i18n._(t`type`)} key="type">
{label} {label}

View File

@@ -139,4 +139,25 @@ describe('<InventorySourceListItem />', () => {
expect(wrapper.find('Button[aria-label="Edit Source"]').length).toBe(0); expect(wrapper.find('Button[aria-label="Edit Source"]').length).toBe(0);
expect(wrapper.find('InventorySourceSyncButton').length).toBe(1); 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 { Formik, useField, useFormikContext } from 'formik';
import { func, shape } from 'prop-types'; import { func, shape } from 'prop-types';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Form, FormGroup, Title } from '@patternfly/react-core'; import { Form, FormGroup, Title } from '@patternfly/react-core';
import { InventorySourcesAPI } from '../../../api'; import { InventorySourcesAPI } from '../../../api';
import { ConfigContext } from '../../../contexts/Config';
import useRequest from '../../../util/useRequest'; import useRequest from '../../../util/useRequest';
import { required } from '../../../util/validators'; import { required } from '../../../util/validators';
@@ -18,7 +17,6 @@ import {
FormColumnLayout, FormColumnLayout,
SubFormLayout, SubFormLayout,
} from '../../../components/FormLayout'; } from '../../../components/FormLayout';
import Popover from '../../../components/Popover';
import { import {
AzureSubForm, AzureSubForm,
@@ -64,13 +62,6 @@ const InventorySourceFormFields = ({
] = useField({ ] = useField({
name: 'execution_environment', 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 => { const resetSubFormFields = sourceType => {
if (sourceType === initialValues.source) { if (sourceType === initialValues.source) {
@@ -79,7 +70,6 @@ const InventorySourceFormFields = ({
...initialValues, ...initialValues,
name: values.name, name: values.name,
description: values.description, description: values.description,
custom_virtualenv: values.custom_virtualenv,
source: sourceType, source: sourceType,
}, },
}); });
@@ -161,30 +151,6 @@ const InventorySourceFormFields = ({
}} }}
/> />
</FormGroup> </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) && ( {!['', 'custom'].includes(sourceField.value) && (
<SubFormLayout> <SubFormLayout>
<Title size="md" headingLevel="h4"> <Title size="md" headingLevel="h4">
@@ -272,7 +238,6 @@ const InventorySourceForm = ({
}) => { }) => {
const initialValues = { const initialValues = {
credential: source?.summary_fields?.credential || null, credential: source?.summary_fields?.credential || null,
custom_virtualenv: source?.custom_virtualenv || '',
description: source?.description || '', description: source?.description || '',
name: source?.name || '', name: source?.name || '',
overwrite: source?.overwrite || false, overwrite: source?.overwrite || false,

View File

@@ -46,15 +46,9 @@ describe('<InventorySourceForm />', () => {
const onSubmit = jest.fn(); const onSubmit = jest.fn();
beforeAll(async () => { beforeAll(async () => {
const config = {
custom_virtualenvs: ['venv/foo', 'venv/bar'],
};
await act(async () => { await act(async () => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
<InventorySourceForm onCancel={() => {}} onSubmit={onSubmit} />, <InventorySourceForm onCancel={() => {}} onSubmit={onSubmit} />
{
context: { config },
}
); );
}); });
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); 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="Source"]')).toHaveLength(1);
expect( expect(
wrapper.find('FormGroup[label="Ansible Environment"]') wrapper.find('FormGroup[label="Ansible Environment"]')
).toHaveLength(1); ).toHaveLength(0);
expect(wrapper.find('ExecutionEnvironmentLookup')).toHaveLength(1); expect(wrapper.find('ExecutionEnvironmentLookup')).toHaveLength(1);
}); });

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,7 +10,6 @@ jest.mock('../../../../api/models/Credentials');
const initialValues = { const initialValues = {
credential: null, credential: null,
custom_virtualenv: '',
overwrite: false, overwrite: false,
overwrite_vars: false, overwrite_vars: false,
source_path: '', source_path: '',
@@ -115,7 +114,6 @@ describe('<SCMSubForm />', () => {
test('should be able to create custom source path', async () => { test('should be able to create custom source path', async () => {
const customInitialValues = { const customInitialValues = {
credential: { id: 1, name: 'Credential' }, credential: { id: 1, name: 'Credential' },
custom_virtualenv: '',
overwrite: false, overwrite: false,
overwrite_vars: false, overwrite_vars: false,
source_path: '/path', source_path: '/path',

View File

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

View File

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

View File

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

View File

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

View File

@@ -39,6 +39,12 @@
"organization_id":1, "organization_id":1,
"kind":"" "kind":""
}, },
"execution_environment": {
"id": 1,
"name": "Default EE",
"description": "",
"image": "quay.io/ansible/awx-ee"
},
"source_project":{ "source_project":{
"id":8, "id":8,
"name":"Mock Project", "name":"Mock Project",
@@ -111,5 +117,6 @@
"source_project":8, "source_project":8,
"update_on_project_update":true, "update_on_project_update":true,
"last_update_failed": true, "last_update_failed": true,
"last_updated":null "last_updated":null,
"execution_environment": 1
} }

View File

@@ -24,6 +24,7 @@ import {
ReLaunchDropDown, ReLaunchDropDown,
} from '../../../components/LaunchButton'; } from '../../../components/LaunchButton';
import StatusIcon from '../../../components/StatusIcon'; import StatusIcon from '../../../components/StatusIcon';
import ExecutionEnvironmentDetail from '../../../components/ExecutionEnvironmentDetail';
import { toTitleCase } from '../../../util/strings'; import { toTitleCase } from '../../../util/strings';
import { formatDateString } from '../../../util/dates'; import { formatDateString } from '../../../util/dates';
import { Job } from '../../../types'; import { Job } from '../../../types';
@@ -71,6 +72,7 @@ function JobDetail({ job, i18n }) {
labels, labels,
project, project,
source_workflow_job, source_workflow_job,
execution_environment: executionEnvironment,
} = job.summary_fields; } = job.summary_fields;
const [errorMsg, setErrorMsg] = useState(); const [errorMsg, setErrorMsg] = useState();
const history = useHistory(); const history = useHistory();
@@ -250,7 +252,10 @@ function JobDetail({ job, i18n }) {
<Detail label={i18n._(t`Playbook`)} value={job.playbook} /> <Detail label={i18n._(t`Playbook`)} value={job.playbook} />
<Detail label={i18n._(t`Limit`)} value={job.limit} /> <Detail label={i18n._(t`Limit`)} value={job.limit} />
<Detail label={i18n._(t`Verbosity`)} value={VERBOSITY[job.verbosity]} /> <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} /> <Detail label={i18n._(t`Execution Node`)} value={job.execution_node} />
{instanceGroup && !instanceGroup?.is_container_group && ( {instanceGroup && !instanceGroup?.is_container_group && (
<Detail <Detail

View File

@@ -60,7 +60,6 @@ describe('<JobDetail />', () => {
assertDetail('Revision', mockJobData.scm_revision); assertDetail('Revision', mockJobData.scm_revision);
assertDetail('Playbook', mockJobData.playbook); assertDetail('Playbook', mockJobData.playbook);
assertDetail('Verbosity', '0 (Normal)'); assertDetail('Verbosity', '0 (Normal)');
assertDetail('Environment', mockJobData.custom_virtualenv);
assertDetail('Execution Node', mockJobData.execution_node); assertDetail('Execution Node', mockJobData.execution_node);
assertDetail( assertDetail(
'Instance Group', 'Instance Group',
@@ -70,6 +69,15 @@ describe('<JobDetail />', () => {
assertDetail('Credentials', 'SSH: Demo Credential'); assertDetail('Credentials', 'SSH: Demo Credential');
assertDetail('Machine Credential', 'SSH: Machine cred'); 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( const credentialChip = wrapper.find(
`Detail[label="Credentials"] CredentialChip` `Detail[label="Credentials"] CredentialChip`
); );

View File

@@ -36,6 +36,12 @@
"organization_id": 1, "organization_id": 1,
"kind": "" "kind": ""
}, },
"execution_environment": {
"id": 1,
"name": "Default EE",
"description": "",
"image": "quay.io/ansible/awx-ee"
},
"project": { "project": {
"id": 6, "id": 6,
"name": "Demo Project", "name": "Demo Project",
@@ -184,5 +190,6 @@
"play_count": 1, "play_count": 1,
"task_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 React, { useState, useEffect, useCallback } from 'react';
import PropTypes from 'prop-types';
import { useHistory, Link } from 'react-router-dom'; import { useHistory, Link } from 'react-router-dom';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { withI18n } from '@lingui/react'; 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 { NotificationTemplateAdd as _NotificationTemplateAdd };
export default withI18n()(NotificationTemplateAdd); export default withI18n()(NotificationTemplateAdd);

View File

@@ -40,9 +40,5 @@ NotificationTemplateEdit.propTypes = {
template: PropTypes.shape().isRequired, template: PropTypes.shape().isRequired,
}; };
NotificationTemplateEdit.contextTypes = {
custom_virtualenvs: PropTypes.arrayOf(PropTypes.string),
};
export { NotificationTemplateEdit as _NotificationTemplateEdit }; export { NotificationTemplateEdit as _NotificationTemplateEdit };
export default NotificationTemplateEdit; export default NotificationTemplateEdit;

View File

@@ -1,5 +1,4 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { PageSection, Card } from '@patternfly/react-core'; 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 { OrganizationAdd as _OrganizationAdd };
export default OrganizationAdd; export default OrganizationAdd;

View File

@@ -15,7 +15,6 @@ describe('<OrganizationAdd />', () => {
const updatedOrgData = { const updatedOrgData = {
name: 'new name', name: 'new name',
description: 'new description', description: 'new description',
custom_virtualenv: 'Buzz',
galaxy_credentials: [], galaxy_credentials: [],
default_environment: { id: 1, name: 'Foo' }, default_environment: { id: 1, name: 'Foo' },
}; };
@@ -51,7 +50,6 @@ describe('<OrganizationAdd />', () => {
const orgData = { const orgData = {
name: 'new name', name: 'new name',
description: 'new description', description: 'new description',
custom_virtualenv: 'Buzz',
galaxy_credentials: [], galaxy_credentials: [],
}; };
OrganizationsAPI.create.mockResolvedValueOnce({ OrganizationsAPI.create.mockResolvedValueOnce({
@@ -78,7 +76,6 @@ describe('<OrganizationAdd />', () => {
const orgData = { const orgData = {
name: 'new name', name: 'new name',
description: 'new description', description: 'new description',
custom_virtualenv: 'Buzz',
galaxy_credentials: [], galaxy_credentials: [],
}; };
OrganizationsAPI.create.mockResolvedValueOnce({ OrganizationsAPI.create.mockResolvedValueOnce({
@@ -103,7 +100,6 @@ describe('<OrganizationAdd />', () => {
const orgData = { const orgData = {
name: 'new name', name: 'new name',
description: 'new description', description: 'new description',
custom_virtualenv: 'Buzz',
galaxy_credentials: [ galaxy_credentials: [
{ {
id: 9000, 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 () => { test('AnsibleSelect component does not render if there are 0 virtual environments', async () => {
const mockInstanceGroups = [ const mockInstanceGroups = [
{ name: 'One', id: 1 }, { name: 'One', id: 1 },
@@ -171,14 +137,9 @@ describe('<OrganizationAdd />', () => {
results: mockInstanceGroups, results: mockInstanceGroups,
}, },
}); });
const config = {
custom_virtualenvs: [],
};
let wrapper; let wrapper;
await act(async () => { await act(async () => {
wrapper = mountWithContexts(<OrganizationAdd />, { wrapper = mountWithContexts(<OrganizationAdd />, {});
context: { config },
});
}); });
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
expect(wrapper.find('AnsibleSelect FormSelect')).toHaveLength(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 ErrorDetail from '../../../components/ErrorDetail';
import useRequest, { useDismissableError } from '../../../util/useRequest'; import useRequest, { useDismissableError } from '../../../util/useRequest';
import { useConfig } from '../../../contexts/Config'; import { useConfig } from '../../../contexts/Config';
import ExecutionEnvironmentDetail from '../../../components/ExecutionEnvironmentDetail';
function OrganizationDetail({ i18n, organization }) { function OrganizationDetail({ i18n, organization }) {
const { const {
@@ -90,22 +91,11 @@ function OrganizationDetail({ i18n, organization }) {
{license_info?.license_type !== 'open' && ( {license_info?.license_type !== 'open' && (
<Detail label={i18n._(t`Max Hosts`)} value={`${max_hosts}`} /> <Detail label={i18n._(t`Max Hosts`)} value={`${max_hosts}`} />
)} )}
<Detail <ExecutionEnvironmentDetail
label={i18n._(t`Ansible Environment`)} virtualEnvironment={custom_virtualenv}
value={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 <UserDateDetail
label={i18n._(t`Created`)} label={i18n._(t`Created`)}
date={created} date={created}

View File

@@ -90,7 +90,6 @@ describe('<OrganizationDetail />', () => {
const testParams = [ const testParams = [
{ label: 'Name', value: 'Foo' }, { label: 'Name', value: 'Foo' },
{ label: 'Description', value: 'Bar' }, { label: 'Description', value: 'Bar' },
{ label: 'Ansible Environment', value: 'Fizz' },
{ label: 'Created', value: '7/7/2015, 5:21:26 PM' }, { label: 'Created', value: '7/7/2015, 5:21:26 PM' },
{ label: 'Last Modified', value: '8/11/2019, 7:47:37 PM' }, { label: 'Last Modified', value: '8/11/2019, 7:47:37 PM' },
{ label: 'Max Hosts', value: '0' }, { label: 'Max Hosts', value: '0' },

View File

@@ -80,9 +80,5 @@ OrganizationEdit.propTypes = {
organization: PropTypes.shape().isRequired, organization: PropTypes.shape().isRequired,
}; };
OrganizationEdit.contextTypes = {
custom_virtualenvs: PropTypes.arrayOf(PropTypes.string),
};
export { OrganizationEdit as _OrganizationEdit }; export { OrganizationEdit as _OrganizationEdit };
export default OrganizationEdit; export default OrganizationEdit;

View File

@@ -14,7 +14,6 @@ describe('<OrganizationEdit />', () => {
const mockData = { const mockData = {
name: 'Foo', name: 'Foo',
description: 'Bar', description: 'Bar',
custom_virtualenv: 'Fizz',
id: 1, id: 1,
related: { related: {
instance_groups: '/api/v2/organizations/1/instance_groups', instance_groups: '/api/v2/organizations/1/instance_groups',
@@ -24,6 +23,7 @@ describe('<OrganizationEdit />', () => {
default_environment: { default_environment: {
id: 1, id: 1,
name: 'Baz', name: 'Baz',
image: 'quay.io/ansible/awx-ee',
}, },
}, },
}; };
@@ -37,7 +37,6 @@ describe('<OrganizationEdit />', () => {
const updatedOrgData = { const updatedOrgData = {
name: 'new name', name: 'new name',
description: 'new description', description: 'new description',
custom_virtualenv: 'Buzz',
default_environment: null, default_environment: null,
}; };
wrapper.find('OrganizationForm').prop('onSubmit')(updatedOrgData, [], []); wrapper.find('OrganizationForm').prop('onSubmit')(updatedOrgData, [], []);
@@ -54,7 +53,6 @@ describe('<OrganizationEdit />', () => {
const updatedOrgData = { const updatedOrgData = {
name: 'new name', name: 'new name',
description: 'new description', description: 'new description',
custom_virtualenv: 'Buzz',
}; };
await act(async () => { await act(async () => {
wrapper.find('OrganizationForm').invoke('onSubmit')( wrapper.find('OrganizationForm').invoke('onSubmit')(

View File

@@ -2,14 +2,22 @@ import React from 'react';
import { string, bool, func } from 'prop-types'; import { string, bool, func } from 'prop-types';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; 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 { Tr, Td } from '@patternfly/react-table';
import { Link } from 'react-router-dom'; 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 { ActionsTd, ActionItem } from '../../../components/PaginatedTable';
import { Organization } from '../../../types'; import { Organization } from '../../../types';
const ExclamationTriangleIcon = styled(PFExclamationTriangleIcon)`
color: var(--pf-global--warning-color--100);
margin-left: 18px;
`;
function OrganizationListItem({ function OrganizationListItem({
organization, organization,
isSelected, isSelected,
@@ -19,6 +27,10 @@ function OrganizationListItem({
i18n, i18n,
}) { }) {
const labelId = `check-action-${organization.id}`; const labelId = `check-action-${organization.id}`;
const missingExecutionEnvironment =
organization.custom_virtualenv && !organization.default_environment;
return ( return (
<Tr id={`org-row-${organization.id}`}> <Tr id={`org-row-${organization.id}`}>
<Td <Td
@@ -31,9 +43,24 @@ function OrganizationListItem({
dataLabel={i18n._(t`Selected`)} dataLabel={i18n._(t`Selected`)}
/> />
<Td id={labelId} dataLabel={i18n._(t`Name`)}> <Td id={labelId} dataLabel={i18n._(t`Name`)}>
<Link to={`${detailUrl}`}> <span>
<b>{organization.name}</b> <Link to={`${detailUrl}`}>
</Link> <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>
<Td dataLabel={i18n._(t`Members`)}> <Td dataLabel={i18n._(t`Members`)}>
{organization.summary_fields.related_field_counts.users} {organization.summary_fields.related_field_counts.users}

View File

@@ -7,7 +7,7 @@ import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
import OrganizationListItem from './OrganizationListItem'; import OrganizationListItem from './OrganizationListItem';
describe('<OrganizationListItem />', () => { describe('<OrganizationListItem />', () => {
test('initially renders succesfully', () => { test('initially renders successfully', () => {
mountWithContexts( mountWithContexts(
<I18nProvider> <I18nProvider>
<MemoryRouter initialEntries={['/organizations']} initialIndex={0}> <MemoryRouter initialEntries={['/organizations']} initialIndex={0}>
@@ -101,4 +101,38 @@ describe('<OrganizationListItem />', () => {
); );
expect(wrapper.find('PencilAltIcon').exists()).toBeFalsy(); 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 PropTypes from 'prop-types';
import { Formik, useField, useFormikContext } from 'formik'; import { Formik, useField, useFormikContext } from 'formik';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Form, FormGroup } from '@patternfly/react-core'; import { Form } from '@patternfly/react-core';
import { OrganizationsAPI } from '../../../api'; import { OrganizationsAPI } from '../../../api';
import { ConfigContext, useConfig } from '../../../contexts/Config'; import { useConfig } from '../../../contexts/Config';
import AnsibleSelect from '../../../components/AnsibleSelect';
import ContentError from '../../../components/ContentError'; import ContentError from '../../../components/ContentError';
import ContentLoading from '../../../components/ContentLoading'; import ContentLoading from '../../../components/ContentLoading';
import FormField, { FormSubmitError } from '../../../components/FormField'; import FormField, { FormSubmitError } from '../../../components/FormField';
@@ -23,10 +22,8 @@ import CredentialLookup from '../../../components/Lookup/CredentialLookup';
function OrganizationFormFields({ i18n, instanceGroups, setInstanceGroups }) { function OrganizationFormFields({ i18n, instanceGroups, setInstanceGroups }) {
const { license_info = {}, me = {} } = useConfig(); const { license_info = {}, me = {} } = useConfig();
const { custom_virtualenvs } = useContext(ConfigContext);
const { setFieldValue } = useFormikContext(); const { setFieldValue } = useFormikContext();
const [venvField] = useField('custom_virtualenv');
const [ const [
galaxyCredentialsField, galaxyCredentialsField,
@@ -42,12 +39,6 @@ function OrganizationFormFields({ i18n, instanceGroups, setInstanceGroups }) {
name: 'default_environment', name: 'default_environment',
}); });
const defaultVenv = {
label: i18n._(t`Use Default Ansible Environment`),
value: '/var/lib/awx/venv/ansible/',
key: 'default',
};
const handleCredentialUpdate = useCallback( const handleCredentialUpdate = useCallback(
value => { value => {
setFieldValue('galaxy_credentials', value); setFieldValue('galaxy_credentials', value);
@@ -87,24 +78,6 @@ function OrganizationFormFields({ i18n, instanceGroups, setInstanceGroups }) {
isDisabled={!me.is_superuser} 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 <InstanceGroupsLookup
value={instanceGroups} value={instanceGroups}
onChange={setInstanceGroups} onChange={setInstanceGroups}
@@ -208,11 +181,10 @@ function OrganizationForm({
initialValues={{ initialValues={{
name: organization.name, name: organization.name,
description: organization.description, description: organization.description,
custom_virtualenv: organization.custom_virtualenv || '',
max_hosts: organization.max_hosts || '0', max_hosts: organization.max_hosts || '0',
galaxy_credentials: organization.galaxy_credentials || [], galaxy_credentials: organization.galaxy_credentials || [],
default_environment: default_environment:
organization.summary_fields?.default_environment || '', organization.summary_fields?.default_environment || null,
}} }}
onSubmit={handleSubmit} onSubmit={handleSubmit}
> >
@@ -248,15 +220,10 @@ OrganizationForm.defaultProps = {
name: '', name: '',
description: '', description: '',
max_hosts: '0', max_hosts: '0',
custom_virtualenv: '',
default_environment: '', default_environment: '',
}, },
submitError: null, submitError: null,
}; };
OrganizationForm.contextTypes = {
custom_virtualenvs: PropTypes.arrayOf(PropTypes.string),
};
export { OrganizationForm as _OrganizationForm }; export { OrganizationForm as _OrganizationForm };
export default withI18n()(OrganizationForm); export default withI18n()(OrganizationForm);

View File

@@ -22,7 +22,6 @@ describe('<OrganizationForm />', () => {
name: 'Foo', name: 'Foo',
description: 'Bar', description: 'Bar',
max_hosts: 1, max_hosts: 1,
custom_virtualenv: 'Fizz',
related: { related: {
instance_groups: '/api/v2/organizations/1/instance_groups', instance_groups: '/api/v2/organizations/1/instance_groups',
}, },
@@ -32,7 +31,9 @@ describe('<OrganizationForm />', () => {
{ name: 'Two', id: 2 }, { name: 'Two', id: 2 },
]; ];
const mockExecutionEnvironment = [{ name: 'EE' }]; const mockExecutionEnvironment = [
{ id: 1, name: 'EE', image: 'quay.io/ansible/awx-ee' },
];
afterEach(() => { afterEach(() => {
jest.clearAllMocks(); jest.clearAllMocks();
@@ -176,46 +177,11 @@ describe('<OrganizationForm />', () => {
name: 'new foo', name: 'new foo',
description: 'new bar', description: 'new bar',
galaxy_credentials: [], galaxy_credentials: [],
custom_virtualenv: 'Fizz',
max_hosts: 134, max_hosts: 134,
default_environment: { id: 1, name: 'Test EE' }, 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 () => { test('onSubmit associates and disassociates instance groups', async () => {
OrganizationsAPI.readInstanceGroups.mockReturnValue({ OrganizationsAPI.readInstanceGroups.mockReturnValue({
data: { data: {
@@ -230,8 +196,7 @@ describe('<OrganizationForm />', () => {
description: 'Bar', description: 'Bar',
galaxy_credentials: [], galaxy_credentials: [],
max_hosts: 1, max_hosts: 1,
custom_virtualenv: 'Fizz', default_environment: null,
default_environment: '',
}; };
const onSubmit = jest.fn(); const onSubmit = jest.fn();
OrganizationsAPI.update.mockResolvedValue(1, mockDataForm); OrganizationsAPI.update.mockResolvedValue(1, mockDataForm);
@@ -336,8 +301,7 @@ describe('<OrganizationForm />', () => {
description: 'Bar', description: 'Bar',
galaxy_credentials: [], galaxy_credentials: [],
max_hosts: 0, max_hosts: 0,
custom_virtualenv: 'Fizz', default_environment: null,
default_environment: '',
}, },
[], [],
[] []

View File

@@ -15,6 +15,7 @@ import {
UserDateDetail, UserDateDetail,
} from '../../../components/DetailList'; } from '../../../components/DetailList';
import ErrorDetail from '../../../components/ErrorDetail'; import ErrorDetail from '../../../components/ErrorDetail';
import ExecutionEnvironmentDetail from '../../../components/ExecutionEnvironmentDetail';
import CredentialChip from '../../../components/CredentialChip'; import CredentialChip from '../../../components/CredentialChip';
import { ProjectsAPI } from '../../../api'; import { ProjectsAPI } from '../../../api';
import { toTitleCase } from '../../../util/strings'; import { toTitleCase } from '../../../util/strings';
@@ -124,23 +125,11 @@ function ProjectDetail({ project, i18n }) {
label={i18n._(t`Cache Timeout`)} label={i18n._(t`Cache Timeout`)}
value={`${scm_update_cache_timeout} ${i18n._(t`Seconds`)}`} value={`${scm_update_cache_timeout} ${i18n._(t`Seconds`)}`}
/> />
<ExecutionEnvironmentDetail
<Detail virtualEnvironment={custom_virtualenv}
label={i18n._(t`Ansible Environment`)} executionEnvironment={summary_fields?.default_environment}
value={custom_virtualenv} 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> <Config>
{({ project_base_dir }) => ( {({ project_base_dir }) => (
<Detail <Detail

View File

@@ -100,11 +100,15 @@ describe('<ProjectDetail />', () => {
'Cache Timeout', 'Cache Timeout',
`${mockProject.scm_update_cache_timeout} Seconds` `${mockProject.scm_update_cache_timeout} Seconds`
); );
assertDetail('Ansible Environment', mockProject.custom_virtualenv); const executionEnvironment = wrapper.find('ExecutionEnvironmentDetail');
assertDetail( expect(executionEnvironment).toHaveLength(1);
'Execution Environment', expect(executionEnvironment.find('dt').text()).toEqual(
'Default Execution Environment'
);
expect(executionEnvironment.find('dd').text()).toEqual(
mockProject.summary_fields.default_environment.name mockProject.summary_fields.default_environment.name
); );
const dateDetails = wrapper.find('UserDateDetail'); const dateDetails = wrapper.find('UserDateDetail');
expect(dateDetails).toHaveLength(2); expect(dateDetails).toHaveLength(2);
expect(dateDetails.at(0).prop('label')).toEqual('Created'); expect(dateDetails.at(0).prop('label')).toEqual('Created');

View File

@@ -31,6 +31,11 @@ const DataListAction = styled(_DataListAction)`
grid-template-columns: repeat(2, 40px); grid-template-columns: repeat(2, 40px);
`; `;
const ExclamationTriangleIconWarning = styled(ExclamationTriangleIcon)`
color: var(--pf-global--warning-color--100);
margin-left: 18px;
`;
function ProjectJobTemplateListItem({ function ProjectJobTemplateListItem({
i18n, i18n,
template, template,
@@ -47,6 +52,11 @@ function ProjectJobTemplateListItem({
(!template.summary_fields.inventory && (!template.summary_fields.inventory &&
!template.ask_inventory_on_launch)); !template.ask_inventory_on_launch));
const missingExecutionEnvironment =
template.type === 'job_template' &&
template.custom_virtualenv &&
!template.execution_environment;
return ( return (
<DataListItem aria-labelledby={labelId} id={`${template.id}`}> <DataListItem aria-labelledby={labelId} id={`${template.id}`}>
<DataListItemRow> <DataListItemRow>
@@ -76,6 +86,19 @@ function ProjectJobTemplateListItem({
</Tooltip> </Tooltip>
</span> </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>,
<DataListCell key="type"> <DataListCell key="type">
{toTitleCase(template.type)} {toTitleCase(template.type)}

View File

@@ -186,4 +186,31 @@ describe('<ProjectJobTemplatesListItem />', () => {
'/templates/job_template/2/details' '/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 { Tr, Td } from '@patternfly/react-table';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { Link } from 'react-router-dom'; 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 styled from 'styled-components';
import { ActionsTd, ActionItem } from '../../../components/PaginatedTable'; import { ActionsTd, ActionItem } from '../../../components/PaginatedTable';
import { formatDateString, timeOfDay } from '../../../util/dates'; import { formatDateString, timeOfDay } from '../../../util/dates';
@@ -22,6 +25,11 @@ const Label = styled.span`
color: var(--pf-global--disabled-color--100); color: var(--pf-global--disabled-color--100);
`; `;
const ExclamationTriangleIcon = styled(PFExclamationTriangleIcon)`
color: var(--pf-global--warning-color--100);
margin-left: 18px;
`;
function ProjectListItem({ function ProjectListItem({
project, project,
isSelected, isSelected,
@@ -75,6 +83,9 @@ function ProjectListItem({
const labelId = `check-action-${project.id}`; const labelId = `check-action-${project.id}`;
const missingExecutionEnvironment =
project.custom_virtualenv && !project.default_environment;
return ( return (
<Tr id={`${project.id}`}> <Tr id={`${project.id}`}>
<Td <Td
@@ -86,9 +97,24 @@ function ProjectListItem({
dataLabel={i18n._(t`Selected`)} dataLabel={i18n._(t`Selected`)}
/> />
<Td id={labelId} dataLabel={i18n._(t`Name`)}> <Td id={labelId} dataLabel={i18n._(t`Name`)}>
<Link id={labelId} to={`${detailUrl}`}> <span>
<b>{project.name}</b> <Link id={labelId} to={`${detailUrl}`}>
</Link> <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>
<Td dataLabel={i18n._(t`Status`)}> <Td dataLabel={i18n._(t`Status`)}>
{project.summary_fields.last_job && ( {project.summary_fields.last_job && (

View File

@@ -40,6 +40,45 @@ describe('<ProjectsListItem />', () => {
expect(wrapper.find('ProjectSyncButton').exists()).toBeTruthy(); 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', () => { test('launch button hidden from users without start capabilities', () => {
const wrapper = mountWithContexts( const wrapper = mountWithContexts(
<table> <table>

View File

@@ -30,6 +30,12 @@
"name": "Default", "name": "Default",
"description": "" "description": ""
}, },
"execution_environment": {
"id": 1,
"name": "Default EE",
"description": "",
"image": "quay.io/ansible/awx-ee"
},
"last_job": { "last_job": {
"id": 8, "id": 8,
"name": "Mike's Project", "name": "Mike's Project",
@@ -111,5 +117,6 @@
"allow_override": false, "allow_override": false,
"custom_virtualenv": null, "custom_virtualenv": null,
"last_update_failed": false, "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, FormColumnLayout,
SubFormLayout, SubFormLayout,
} from '../../../components/FormLayout'; } from '../../../components/FormLayout';
import Popover from '../../../components/Popover';
import { import {
GitSubForm, GitSubForm,
SvnSubForm, SvnSubForm,
@@ -96,7 +95,6 @@ function ProjectFormFields({
name: 'scm_type', name: 'scm_type',
validate: required(i18n._(t`Set a value for this field`), i18n), validate: required(i18n._(t`Set a value for this field`), i18n),
}); });
const [venvField] = useField('custom_virtualenv');
const [organizationField, organizationMeta, organizationHelpers] = useField({ const [organizationField, organizationMeta, organizationHelpers] = useField({
name: 'organization', name: 'organization',
validate: required(i18n._(t`Select a value for this field`), i18n), validate: required(i18n._(t`Select a value for this field`), i18n),
@@ -293,42 +291,6 @@ function ProjectFormFields({
</FormColumnLayout> </FormColumnLayout>
</SubFormLayout> </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, allow_override: project.allow_override || false,
base_dir: project_base_dir || '', base_dir: project_base_dir || '',
credential: project.credential || '', credential: project.credential || '',
custom_virtualenv: project.custom_virtualenv || '',
description: project.description || '', description: project.description || '',
local_path: project.local_path || '', local_path: project.local_path || '',
name: project.name || '', name: project.name || '',

View File

@@ -107,15 +107,9 @@ describe('<ProjectForm />', () => {
}); });
test('new form displays primary form fields', async () => { test('new form displays primary form fields', async () => {
const config = {
custom_virtualenvs: ['venv/foo', 'venv/bar'],
};
await act(async () => { await act(async () => {
wrapper = mountWithContexts( wrapper = mountWithContexts(
<ProjectForm handleSubmit={jest.fn()} handleCancel={jest.fn()} />, <ProjectForm handleSubmit={jest.fn()} handleCancel={jest.fn()} />
{
context: { config },
}
); );
}); });
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
@@ -126,7 +120,7 @@ describe('<ProjectForm />', () => {
wrapper.find('FormGroup[label="Source Control Credential Type"]').length wrapper.find('FormGroup[label="Source Control Credential Type"]').length
).toBe(1); ).toBe(1);
expect(wrapper.find('FormGroup[label="Ansible Environment"]').length).toBe( expect(wrapper.find('FormGroup[label="Ansible Environment"]').length).toBe(
1 0
); );
expect(wrapper.find('FormGroup[label="Options"]').length).toBe(0); expect(wrapper.find('FormGroup[label="Options"]').length).toBe(0);
}); });

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -68,7 +68,10 @@ describe('<WorkflowJobTemplateEdit/>', () => {
}); });
OrganizationsAPI.read.mockResolvedValue({ results: [{ id: 1 }] }); OrganizationsAPI.read.mockResolvedValue({ results: [{ id: 1 }] });
ExecutionEnvironmentsAPI.read.mockResolvedValue({ ExecutionEnvironmentsAPI.read.mockResolvedValue({
data: mockExecutionEnvironment, data: {
results: mockExecutionEnvironment,
count: 1,
},
}); });
await act(async () => { await act(async () => {

View File

@@ -732,7 +732,7 @@ const FormikApp = withFormik({
i18n._(t`a new webhook key will be generated on save.`).toUpperCase(), i18n._(t`a new webhook key will be generated on save.`).toUpperCase(),
webhook_credential: template?.summary_fields?.webhook_credential || null, webhook_credential: template?.summary_fields?.webhook_credential || null,
execution_environment: execution_environment:
template.summary_fields?.execution_environment || '', template.summary_fields?.execution_environment || null,
}; };
}, },
handleSubmit: async (values, { props, setErrors }) => { handleSubmit: async (values, { props, setErrors }) => {

View File

@@ -322,7 +322,7 @@ const FormikApp = withFormik({
: '', : '',
webhook_key: template.webhook_key || '', webhook_key: template.webhook_key || '',
execution_environment: execution_environment:
template.summary_fields?.execution_environment || '', template.summary_fields?.execution_environment || null,
}; };
}, },
handleSubmit: async (values, { props, setErrors }) => { handleSubmit: async (values, { props, setErrors }) => {

View File

@@ -410,9 +410,10 @@ export const WorkflowApproval = shape({
export const ExecutionEnvironment = shape({ export const ExecutionEnvironment = shape({
id: number.isRequired, id: number.isRequired,
name: string,
organization: number, organization: number,
credential: number, credential: number,
image: string.isRequired, image: string,
url: string, url: string,
summary_fields: shape({}), summary_fields: shape({}),
description: string, description: string,