mirror of
https://github.com/ansible/awx.git
synced 2026-05-07 17:37:37 -02:30
Add Execution Environments into a few screens
Add EE to the following screens: * Job Template * Organization * Project * Workflow Job Template Also, add a new lookup component - ExecutionEnvironmentLoookup. See: https://github.com/ansible/awx/issues/9189
This commit is contained in:
167
awx/ui_next/src/components/Lookup/ExecutionEnvironmentLookup.jsx
Normal file
167
awx/ui_next/src/components/Lookup/ExecutionEnvironmentLookup.jsx
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
import React, { useCallback, useEffect } from 'react';
|
||||||
|
import { string, func, bool } from 'prop-types';
|
||||||
|
import { withRouter, useLocation } from 'react-router-dom';
|
||||||
|
import { withI18n } from '@lingui/react';
|
||||||
|
import { t } from '@lingui/macro';
|
||||||
|
import { FormGroup, Tooltip } from '@patternfly/react-core';
|
||||||
|
|
||||||
|
import { ExecutionEnvironmentsAPI } from '../../api';
|
||||||
|
import { ExecutionEnvironment } from '../../types';
|
||||||
|
import { getQSConfig, parseQueryString, mergeParams } from '../../util/qs';
|
||||||
|
import Popover from '../Popover';
|
||||||
|
import OptionsList from '../OptionsList';
|
||||||
|
import useRequest from '../../util/useRequest';
|
||||||
|
|
||||||
|
import Lookup from './Lookup';
|
||||||
|
import LookupErrorMessage from './shared/LookupErrorMessage';
|
||||||
|
|
||||||
|
const QS_CONFIG = getQSConfig('execution_environments', {
|
||||||
|
page: 1,
|
||||||
|
page_size: 5,
|
||||||
|
order_by: 'name',
|
||||||
|
});
|
||||||
|
|
||||||
|
function ExecutionEnvironmentLookup({
|
||||||
|
globallyAvailable,
|
||||||
|
i18n,
|
||||||
|
isDefaultEnvironment,
|
||||||
|
isDisabled,
|
||||||
|
onChange,
|
||||||
|
organizationId,
|
||||||
|
popoverContent,
|
||||||
|
tooltip,
|
||||||
|
value,
|
||||||
|
onBlur,
|
||||||
|
}) {
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const {
|
||||||
|
result: {
|
||||||
|
executionEnvironments,
|
||||||
|
count,
|
||||||
|
relatedSearchableKeys,
|
||||||
|
searchableKeys,
|
||||||
|
},
|
||||||
|
request: fetchExecutionEnvironments,
|
||||||
|
error,
|
||||||
|
isLoading,
|
||||||
|
} = useRequest(
|
||||||
|
useCallback(async () => {
|
||||||
|
const params = parseQueryString(QS_CONFIG, location.search);
|
||||||
|
const globallyAvailableParams = globallyAvailable
|
||||||
|
? { or__organization__isnull: 'True' }
|
||||||
|
: {};
|
||||||
|
const organizationIdParams = organizationId
|
||||||
|
? { or__organization__id: organizationId }
|
||||||
|
: {};
|
||||||
|
const [{ data }, actionsResponse] = await Promise.all([
|
||||||
|
ExecutionEnvironmentsAPI.read(
|
||||||
|
mergeParams(params, {
|
||||||
|
...globallyAvailableParams,
|
||||||
|
...organizationIdParams,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
ExecutionEnvironmentsAPI.readOptions(),
|
||||||
|
]);
|
||||||
|
return {
|
||||||
|
executionEnvironments: data.results,
|
||||||
|
count: data.count,
|
||||||
|
relatedSearchableKeys: (
|
||||||
|
actionsResponse?.data?.related_search_fields || []
|
||||||
|
).map(val => val.slice(0, -8)),
|
||||||
|
searchableKeys: Object.keys(
|
||||||
|
actionsResponse.data.actions?.GET || {}
|
||||||
|
).filter(key => actionsResponse.data.actions?.GET[key].filterable),
|
||||||
|
};
|
||||||
|
}, [location, globallyAvailable, organizationId]),
|
||||||
|
{
|
||||||
|
executionEnvironments: [],
|
||||||
|
count: 0,
|
||||||
|
relatedSearchableKeys: [],
|
||||||
|
searchableKeys: [],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchExecutionEnvironments();
|
||||||
|
}, [fetchExecutionEnvironments]);
|
||||||
|
|
||||||
|
const renderLookup = () => (
|
||||||
|
<>
|
||||||
|
<Lookup
|
||||||
|
id="execution-environments"
|
||||||
|
header={i18n._(t`Execution Environments`)}
|
||||||
|
value={value}
|
||||||
|
onBlur={onBlur}
|
||||||
|
onChange={onChange}
|
||||||
|
qsConfig={QS_CONFIG}
|
||||||
|
isLoading={isLoading}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
renderOptionsList={({ state, dispatch, canDelete }) => (
|
||||||
|
<OptionsList
|
||||||
|
value={state.selectedItems}
|
||||||
|
options={executionEnvironments}
|
||||||
|
optionCount={count}
|
||||||
|
searchColumns={[
|
||||||
|
{
|
||||||
|
name: i18n._(t`Name`),
|
||||||
|
key: 'name__icontains',
|
||||||
|
isDefault: true,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
sortColumns={[
|
||||||
|
{
|
||||||
|
name: i18n._(t`Name`),
|
||||||
|
key: 'name',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
searchableKeys={searchableKeys}
|
||||||
|
relatedSearchableKeys={relatedSearchableKeys}
|
||||||
|
multiple={state.multiple}
|
||||||
|
header={i18n._(t`Execution Environment`)}
|
||||||
|
name="executionEnvironments"
|
||||||
|
qsConfig={QS_CONFIG}
|
||||||
|
readOnly={!canDelete}
|
||||||
|
selectItem={item => dispatch({ type: 'SELECT_ITEM', item })}
|
||||||
|
deselectItem={item => dispatch({ type: 'DESELECT_ITEM', item })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormGroup
|
||||||
|
fieldId="execution-environment-lookup"
|
||||||
|
label={
|
||||||
|
isDefaultEnvironment
|
||||||
|
? i18n._(t`Default Execution Environment`)
|
||||||
|
: i18n._(t`Execution Environment`)
|
||||||
|
}
|
||||||
|
labelIcon={popoverContent && <Popover content={popoverContent} />}
|
||||||
|
>
|
||||||
|
{isDisabled ? (
|
||||||
|
<Tooltip content={tooltip}>{renderLookup()}</Tooltip>
|
||||||
|
) : (
|
||||||
|
renderLookup()
|
||||||
|
)}
|
||||||
|
|
||||||
|
<LookupErrorMessage error={error} />
|
||||||
|
</FormGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ExecutionEnvironmentLookup.propTypes = {
|
||||||
|
value: ExecutionEnvironment,
|
||||||
|
popoverContent: string,
|
||||||
|
onChange: func.isRequired,
|
||||||
|
isDefaultEnvironment: bool,
|
||||||
|
};
|
||||||
|
|
||||||
|
ExecutionEnvironmentLookup.defaultProps = {
|
||||||
|
popoverContent: '',
|
||||||
|
isDefaultEnvironment: false,
|
||||||
|
value: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withI18n()(withRouter(ExecutionEnvironmentLookup));
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
|
||||||
|
import ExecutionEnvironmentLookup from './ExecutionEnvironmentLookup';
|
||||||
|
import { ExecutionEnvironmentsAPI } from '../../api';
|
||||||
|
|
||||||
|
jest.mock('../../api');
|
||||||
|
|
||||||
|
const mockedExecutionEnvironments = {
|
||||||
|
count: 1,
|
||||||
|
results: [
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'Foo',
|
||||||
|
image: 'quay.io/ansible/awx-ee',
|
||||||
|
pull: 'missing',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const executionEnvironment = {
|
||||||
|
id: 42,
|
||||||
|
name: 'Bar',
|
||||||
|
image: 'quay.io/ansible/bar',
|
||||||
|
pull: 'missing',
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('ExecutionEnvironmentLookup', () => {
|
||||||
|
let wrapper;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
ExecutionEnvironmentsAPI.read.mockResolvedValue(
|
||||||
|
mockedExecutionEnvironments
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should render successfully', async () => {
|
||||||
|
ExecutionEnvironmentsAPI.readOptions.mockReturnValue({
|
||||||
|
data: {
|
||||||
|
actions: {
|
||||||
|
GET: {},
|
||||||
|
POST: {},
|
||||||
|
},
|
||||||
|
related_search_fields: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<ExecutionEnvironmentLookup
|
||||||
|
value={executionEnvironment}
|
||||||
|
onChange={() => {}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(ExecutionEnvironmentsAPI.read).toHaveBeenCalledTimes(1);
|
||||||
|
expect(wrapper.find('ExecutionEnvironmentLookup')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should fetch execution environments', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(
|
||||||
|
<ExecutionEnvironmentLookup
|
||||||
|
value={executionEnvironment}
|
||||||
|
onChange={() => {}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
expect(ExecutionEnvironmentsAPI.read).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -7,3 +7,4 @@ export { default as CredentialLookup } from './CredentialLookup';
|
|||||||
export { default as ApplicationLookup } from './ApplicationLookup';
|
export { default as ApplicationLookup } from './ApplicationLookup';
|
||||||
export { default as HostFilterLookup } from './HostFilterLookup';
|
export { default as HostFilterLookup } from './HostFilterLookup';
|
||||||
export { default as OrganizationLookup } from './OrganizationLookup';
|
export { default as OrganizationLookup } from './OrganizationLookup';
|
||||||
|
export { default as ExecutionEnvironmentLookup } from './ExecutionEnvironmentLookup';
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ function OrganizationAdd() {
|
|||||||
|
|
||||||
const handleSubmit = async (values, groupsToAssociate) => {
|
const handleSubmit = async (values, groupsToAssociate) => {
|
||||||
try {
|
try {
|
||||||
const { data: response } = await OrganizationsAPI.create(values);
|
const { data: response } = await OrganizationsAPI.create({
|
||||||
|
...values,
|
||||||
|
default_environment: values.default_environment?.id,
|
||||||
|
});
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
groupsToAssociate
|
groupsToAssociate
|
||||||
.map(id => OrganizationsAPI.associateInstanceGroup(response.id, id))
|
.map(id => OrganizationsAPI.associateInstanceGroup(response.id, id))
|
||||||
|
|||||||
@@ -17,13 +17,18 @@ describe('<OrganizationAdd />', () => {
|
|||||||
description: 'new description',
|
description: 'new description',
|
||||||
custom_virtualenv: 'Buzz',
|
custom_virtualenv: 'Buzz',
|
||||||
galaxy_credentials: [],
|
galaxy_credentials: [],
|
||||||
|
default_environment: { id: 1, name: 'Foo' },
|
||||||
};
|
};
|
||||||
OrganizationsAPI.create.mockResolvedValueOnce({ data: {} });
|
OrganizationsAPI.create.mockResolvedValueOnce({ data: {} });
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
const wrapper = mountWithContexts(<OrganizationAdd />);
|
const wrapper = mountWithContexts(<OrganizationAdd />);
|
||||||
wrapper.find('OrganizationForm').prop('onSubmit')(updatedOrgData, []);
|
wrapper.find('OrganizationForm').prop('onSubmit')(updatedOrgData, []);
|
||||||
});
|
});
|
||||||
expect(OrganizationsAPI.create).toHaveBeenCalledWith(updatedOrgData);
|
expect(OrganizationsAPI.create).toHaveBeenCalledWith({
|
||||||
|
...updatedOrgData,
|
||||||
|
default_environment: 1,
|
||||||
|
});
|
||||||
|
expect(OrganizationsAPI.create).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should navigate to organizations list when cancel is clicked', async () => {
|
test('should navigate to organizations list when cancel is clicked', async () => {
|
||||||
|
|||||||
@@ -94,6 +94,12 @@ function OrganizationDetail({ i18n, organization }) {
|
|||||||
label={i18n._(t`Ansible Environment`)}
|
label={i18n._(t`Ansible Environment`)}
|
||||||
value={custom_virtualenv}
|
value={custom_virtualenv}
|
||||||
/>
|
/>
|
||||||
|
{summary_fields?.default_environment?.name && (
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Default Execution Environment`)}
|
||||||
|
value={summary_fields.default_environment.name}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<UserDateDetail
|
<UserDateDetail
|
||||||
label={i18n._(t`Created`)}
|
label={i18n._(t`Created`)}
|
||||||
date={created}
|
date={created}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ jest.mock('../../../api');
|
|||||||
|
|
||||||
describe('<OrganizationDetail />', () => {
|
describe('<OrganizationDetail />', () => {
|
||||||
const mockOrganization = {
|
const mockOrganization = {
|
||||||
|
id: 12,
|
||||||
name: 'Foo',
|
name: 'Foo',
|
||||||
description: 'Bar',
|
description: 'Bar',
|
||||||
custom_virtualenv: 'Fizz',
|
custom_virtualenv: 'Fizz',
|
||||||
@@ -24,7 +25,14 @@ describe('<OrganizationDetail />', () => {
|
|||||||
edit: true,
|
edit: true,
|
||||||
delete: true,
|
delete: true,
|
||||||
},
|
},
|
||||||
|
default_environment: {
|
||||||
|
id: 1,
|
||||||
|
name: 'Default EE',
|
||||||
|
description: '',
|
||||||
|
image: 'quay.io/ansible/awx-ee',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
default_environment: 1,
|
||||||
};
|
};
|
||||||
const mockInstanceGroups = {
|
const mockInstanceGroups = {
|
||||||
data: {
|
data: {
|
||||||
@@ -43,7 +51,7 @@ describe('<OrganizationDetail />', () => {
|
|||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('initially renders succesfully', async () => {
|
test('initially renders successfully', async () => {
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
mountWithContexts(<OrganizationDetail organization={mockOrganization} />);
|
mountWithContexts(<OrganizationDetail organization={mockOrganization} />);
|
||||||
});
|
});
|
||||||
@@ -86,6 +94,7 @@ describe('<OrganizationDetail />', () => {
|
|||||||
{ 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' },
|
||||||
|
{ label: 'Default Execution Environment', value: 'Default EE' },
|
||||||
];
|
];
|
||||||
for (let i = 0; i < testParams.length; i++) {
|
for (let i = 0; i < testParams.length; i++) {
|
||||||
const { label, value } = testParams[i];
|
const { label, value } = testParams[i];
|
||||||
|
|||||||
@@ -28,7 +28,10 @@ function OrganizationEdit({ organization }) {
|
|||||||
const addedCredentialIds = addedCredentials.map(({ id }) => id);
|
const addedCredentialIds = addedCredentials.map(({ id }) => id);
|
||||||
const removedCredentialIds = removedCredentials.map(({ id }) => id);
|
const removedCredentialIds = removedCredentials.map(({ id }) => id);
|
||||||
|
|
||||||
await OrganizationsAPI.update(organization.id, values);
|
await OrganizationsAPI.update(organization.id, {
|
||||||
|
...values,
|
||||||
|
default_environment: values.default_environment?.id || null,
|
||||||
|
});
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
groupsToAssociate
|
groupsToAssociate
|
||||||
.map(id =>
|
.map(id =>
|
||||||
|
|||||||
@@ -19,6 +19,13 @@ describe('<OrganizationEdit />', () => {
|
|||||||
related: {
|
related: {
|
||||||
instance_groups: '/api/v2/organizations/1/instance_groups',
|
instance_groups: '/api/v2/organizations/1/instance_groups',
|
||||||
},
|
},
|
||||||
|
default_environment: 1,
|
||||||
|
summary_fields: {
|
||||||
|
default_environment: {
|
||||||
|
id: 1,
|
||||||
|
name: 'Baz',
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
test('onSubmit should call api update', async () => {
|
test('onSubmit should call api update', async () => {
|
||||||
@@ -31,6 +38,7 @@ describe('<OrganizationEdit />', () => {
|
|||||||
name: 'new name',
|
name: 'new name',
|
||||||
description: 'new description',
|
description: 'new description',
|
||||||
custom_virtualenv: 'Buzz',
|
custom_virtualenv: 'Buzz',
|
||||||
|
default_environment: null,
|
||||||
};
|
};
|
||||||
wrapper.find('OrganizationForm').prop('onSubmit')(updatedOrgData, [], []);
|
wrapper.find('OrganizationForm').prop('onSubmit')(updatedOrgData, [], []);
|
||||||
|
|
||||||
|
|||||||
@@ -12,16 +12,21 @@ 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';
|
||||||
import FormActionGroup from '../../../components/FormActionGroup/FormActionGroup';
|
import FormActionGroup from '../../../components/FormActionGroup/FormActionGroup';
|
||||||
import { InstanceGroupsLookup } from '../../../components/Lookup';
|
import {
|
||||||
|
InstanceGroupsLookup,
|
||||||
|
ExecutionEnvironmentLookup,
|
||||||
|
} from '../../../components/Lookup';
|
||||||
import { getAddedAndRemoved } from '../../../util/lists';
|
import { getAddedAndRemoved } from '../../../util/lists';
|
||||||
import { required, minMaxValue } from '../../../util/validators';
|
import { required, minMaxValue } from '../../../util/validators';
|
||||||
import { FormColumnLayout } from '../../../components/FormLayout';
|
import { FormColumnLayout } from '../../../components/FormLayout';
|
||||||
import CredentialLookup from '../../../components/Lookup/CredentialLookup';
|
import CredentialLookup from '../../../components/Lookup/CredentialLookup';
|
||||||
|
|
||||||
function OrganizationFormFields({ i18n, instanceGroups, setInstanceGroups }) {
|
function OrganizationFormFields({ i18n, instanceGroups, setInstanceGroups }) {
|
||||||
|
const { license_info = {}, me = {} } = useConfig();
|
||||||
|
const { custom_virtualenvs } = useContext(ConfigContext);
|
||||||
|
|
||||||
const { setFieldValue } = useFormikContext();
|
const { setFieldValue } = useFormikContext();
|
||||||
const [venvField] = useField('custom_virtualenv');
|
const [venvField] = useField('custom_virtualenv');
|
||||||
const { license_info = {}, me = {} } = useConfig();
|
|
||||||
|
|
||||||
const [
|
const [
|
||||||
galaxyCredentialsField,
|
galaxyCredentialsField,
|
||||||
@@ -29,12 +34,19 @@ function OrganizationFormFields({ i18n, instanceGroups, setInstanceGroups }) {
|
|||||||
galaxyCredentialsHelpers,
|
galaxyCredentialsHelpers,
|
||||||
] = useField('galaxy_credentials');
|
] = useField('galaxy_credentials');
|
||||||
|
|
||||||
|
const [
|
||||||
|
executionEnvironmentField,
|
||||||
|
executionEnvironmentMeta,
|
||||||
|
executionEnvironmentHelpers,
|
||||||
|
] = useField({
|
||||||
|
name: 'default_environment',
|
||||||
|
});
|
||||||
|
|
||||||
const defaultVenv = {
|
const defaultVenv = {
|
||||||
label: i18n._(t`Use Default Ansible Environment`),
|
label: i18n._(t`Use Default Ansible Environment`),
|
||||||
value: '/var/lib/awx/venv/ansible/',
|
value: '/var/lib/awx/venv/ansible/',
|
||||||
key: 'default',
|
key: 'default',
|
||||||
};
|
};
|
||||||
const { custom_virtualenvs } = useContext(ConfigContext);
|
|
||||||
|
|
||||||
const handleCredentialUpdate = useCallback(
|
const handleCredentialUpdate = useCallback(
|
||||||
value => {
|
value => {
|
||||||
@@ -100,6 +112,20 @@ function OrganizationFormFields({ i18n, instanceGroups, setInstanceGroups }) {
|
|||||||
t`Select the Instance Groups for this Organization to run on.`
|
t`Select the Instance Groups for this Organization to run on.`
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<ExecutionEnvironmentLookup
|
||||||
|
helperTextInvalid={executionEnvironmentMeta.error}
|
||||||
|
isValid={
|
||||||
|
!executionEnvironmentMeta.touched || !executionEnvironmentMeta.error
|
||||||
|
}
|
||||||
|
onBlur={() => executionEnvironmentHelpers.setTouched()}
|
||||||
|
value={executionEnvironmentField.value}
|
||||||
|
onChange={value => executionEnvironmentHelpers.setValue(value)}
|
||||||
|
popoverContent={i18n._(
|
||||||
|
t`Select the default execution environment for this organization.`
|
||||||
|
)}
|
||||||
|
globallyAvailable
|
||||||
|
isDefaultEnvironment
|
||||||
|
/>
|
||||||
<CredentialLookup
|
<CredentialLookup
|
||||||
credentialTypeNamespace="galaxy_api_token"
|
credentialTypeNamespace="galaxy_api_token"
|
||||||
label={i18n._(t`Galaxy Credentials`)}
|
label={i18n._(t`Galaxy Credentials`)}
|
||||||
@@ -185,6 +211,8 @@ function OrganizationForm({
|
|||||||
custom_virtualenv: organization.custom_virtualenv || '',
|
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:
|
||||||
|
organization.summary_fields?.default_environment || '',
|
||||||
}}
|
}}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
>
|
>
|
||||||
@@ -221,6 +249,7 @@ OrganizationForm.defaultProps = {
|
|||||||
description: '',
|
description: '',
|
||||||
max_hosts: '0',
|
max_hosts: '0',
|
||||||
custom_virtualenv: '',
|
custom_virtualenv: '',
|
||||||
|
default_environment: '',
|
||||||
},
|
},
|
||||||
submitError: null,
|
submitError: null,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
mountWithContexts,
|
mountWithContexts,
|
||||||
waitForElement,
|
waitForElement,
|
||||||
} from '../../../../testUtils/enzymeHelpers';
|
} from '../../../../testUtils/enzymeHelpers';
|
||||||
import { OrganizationsAPI } from '../../../api';
|
import { OrganizationsAPI, ExecutionEnvironmentsAPI } from '../../../api';
|
||||||
|
|
||||||
import OrganizationForm from './OrganizationForm';
|
import OrganizationForm from './OrganizationForm';
|
||||||
|
|
||||||
@@ -32,6 +32,8 @@ describe('<OrganizationForm />', () => {
|
|||||||
{ name: 'Two', id: 2 },
|
{ name: 'Two', id: 2 },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const mockExecutionEnvironment = [{ name: 'EE' }];
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
@@ -132,6 +134,11 @@ describe('<OrganizationForm />', () => {
|
|||||||
results: mockInstanceGroups,
|
results: mockInstanceGroups,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
ExecutionEnvironmentsAPI.read.mockReturnValue({
|
||||||
|
data: {
|
||||||
|
results: mockExecutionEnvironment,
|
||||||
|
},
|
||||||
|
});
|
||||||
let wrapper;
|
let wrapper;
|
||||||
const onSubmit = jest.fn();
|
const onSubmit = jest.fn();
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
@@ -155,10 +162,15 @@ describe('<OrganizationForm />', () => {
|
|||||||
wrapper.find('input#org-max_hosts').simulate('change', {
|
wrapper.find('input#org-max_hosts').simulate('change', {
|
||||||
target: { value: 134, name: 'max_hosts' },
|
target: { value: 134, name: 'max_hosts' },
|
||||||
});
|
});
|
||||||
|
wrapper.find('ExecutionEnvironmentLookup').invoke('onChange')({
|
||||||
|
id: 1,
|
||||||
|
name: 'Test EE',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper.find('button[aria-label="Save"]').simulate('click');
|
wrapper.find('button[aria-label="Save"]').simulate('click');
|
||||||
});
|
});
|
||||||
|
wrapper.update();
|
||||||
expect(onSubmit).toHaveBeenCalledTimes(1);
|
expect(onSubmit).toHaveBeenCalledTimes(1);
|
||||||
expect(onSubmit.mock.calls[0][0]).toEqual({
|
expect(onSubmit.mock.calls[0][0]).toEqual({
|
||||||
name: 'new foo',
|
name: 'new foo',
|
||||||
@@ -166,6 +178,7 @@ describe('<OrganizationForm />', () => {
|
|||||||
galaxy_credentials: [],
|
galaxy_credentials: [],
|
||||||
custom_virtualenv: 'Fizz',
|
custom_virtualenv: 'Fizz',
|
||||||
max_hosts: 134,
|
max_hosts: 134,
|
||||||
|
default_environment: { id: 1, name: 'Test EE' },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -209,12 +222,16 @@ describe('<OrganizationForm />', () => {
|
|||||||
results: mockInstanceGroups,
|
results: mockInstanceGroups,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
ExecutionEnvironmentsAPI.read.mockReturnValue({
|
||||||
|
data: { results: mockExecutionEnvironment },
|
||||||
|
});
|
||||||
const mockDataForm = {
|
const mockDataForm = {
|
||||||
name: 'Foo',
|
name: 'Foo',
|
||||||
description: 'Bar',
|
description: 'Bar',
|
||||||
galaxy_credentials: [],
|
galaxy_credentials: [],
|
||||||
max_hosts: 1,
|
max_hosts: 1,
|
||||||
custom_virtualenv: 'Fizz',
|
custom_virtualenv: 'Fizz',
|
||||||
|
default_environment: '',
|
||||||
};
|
};
|
||||||
const onSubmit = jest.fn();
|
const onSubmit = jest.fn();
|
||||||
OrganizationsAPI.update.mockResolvedValue(1, mockDataForm);
|
OrganizationsAPI.update.mockResolvedValue(1, mockDataForm);
|
||||||
@@ -320,6 +337,7 @@ describe('<OrganizationForm />', () => {
|
|||||||
galaxy_credentials: [],
|
galaxy_credentials: [],
|
||||||
max_hosts: 0,
|
max_hosts: 0,
|
||||||
custom_virtualenv: 'Fizz',
|
custom_virtualenv: 'Fizz',
|
||||||
|
default_environment: '',
|
||||||
},
|
},
|
||||||
[],
|
[],
|
||||||
[]
|
[]
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ function ProjectAdd() {
|
|||||||
} = await ProjectsAPI.create({
|
} = await ProjectsAPI.create({
|
||||||
...values,
|
...values,
|
||||||
organization: values.organization.id,
|
organization: values.organization.id,
|
||||||
|
default_environment: values.default_environment?.id,
|
||||||
});
|
});
|
||||||
history.push(`/projects/${id}/details`);
|
history.push(`/projects/${id}/details`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -20,11 +20,12 @@ describe('<ProjectAdd />', () => {
|
|||||||
scm_clean: true,
|
scm_clean: true,
|
||||||
credential: 100,
|
credential: 100,
|
||||||
local_path: '',
|
local_path: '',
|
||||||
organization: 2,
|
organization: { id: 2, name: 'Bar' },
|
||||||
scm_update_on_launch: true,
|
scm_update_on_launch: true,
|
||||||
scm_update_cache_timeout: 3,
|
scm_update_cache_timeout: 3,
|
||||||
allow_override: false,
|
allow_override: false,
|
||||||
custom_virtualenv: '/var/lib/awx/venv/custom-env',
|
custom_virtualenv: '/var/lib/awx/venv/custom-env',
|
||||||
|
default_environment: { id: 1, name: 'Foo' },
|
||||||
};
|
};
|
||||||
|
|
||||||
const projectOptionsResolve = {
|
const projectOptionsResolve = {
|
||||||
@@ -102,6 +103,11 @@ describe('<ProjectAdd />', () => {
|
|||||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
wrapper.find('ProjectForm').invoke('handleSubmit')(projectData);
|
wrapper.find('ProjectForm').invoke('handleSubmit')(projectData);
|
||||||
expect(ProjectsAPI.create).toHaveBeenCalledTimes(1);
|
expect(ProjectsAPI.create).toHaveBeenCalledTimes(1);
|
||||||
|
expect(ProjectsAPI.create).toHaveBeenCalledWith({
|
||||||
|
...projectData,
|
||||||
|
organization: 2,
|
||||||
|
default_environment: 1,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('handleSubmit should throw an error', async () => {
|
test('handleSubmit should throw an error', async () => {
|
||||||
|
|||||||
@@ -124,10 +124,18 @@ 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`)}`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Detail
|
<Detail
|
||||||
label={i18n._(t`Ansible Environment`)}
|
label={i18n._(t`Ansible Environment`)}
|
||||||
value={custom_virtualenv}
|
value={custom_virtualenv}
|
||||||
/>
|
/>
|
||||||
|
{summary_fields?.default_environment?.name && (
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Execution Environment`)}
|
||||||
|
value={summary_fields.default_environment.name}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<Config>
|
<Config>
|
||||||
{({ project_base_dir }) => (
|
{({ project_base_dir }) => (
|
||||||
<Detail
|
<Detail
|
||||||
@@ -137,6 +145,7 @@ function ProjectDetail({ project, i18n }) {
|
|||||||
)}
|
)}
|
||||||
</Config>
|
</Config>
|
||||||
<Detail label={i18n._(t`Playbook Directory`)} value={local_path} />
|
<Detail label={i18n._(t`Playbook Directory`)} value={local_path} />
|
||||||
|
|
||||||
<UserDateDetail
|
<UserDateDetail
|
||||||
label={i18n._(t`Created`)}
|
label={i18n._(t`Created`)}
|
||||||
date={created}
|
date={created}
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ describe('<ProjectDetail />', () => {
|
|||||||
id: 10,
|
id: 10,
|
||||||
name: 'Foo',
|
name: 'Foo',
|
||||||
},
|
},
|
||||||
|
default_environment: {
|
||||||
|
id: 12,
|
||||||
|
name: 'Bar',
|
||||||
|
image: 'quay.io/ansible/awx-ee',
|
||||||
|
},
|
||||||
credential: {
|
credential: {
|
||||||
id: 1000,
|
id: 1000,
|
||||||
name: 'qux',
|
name: 'qux',
|
||||||
@@ -67,9 +72,10 @@ describe('<ProjectDetail />', () => {
|
|||||||
scm_update_cache_timeout: 5,
|
scm_update_cache_timeout: 5,
|
||||||
allow_override: true,
|
allow_override: true,
|
||||||
custom_virtualenv: '/custom-venv',
|
custom_virtualenv: '/custom-venv',
|
||||||
|
default_environment: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
test('initially renders succesfully', () => {
|
test('initially renders successfully', () => {
|
||||||
mountWithContexts(<ProjectDetail project={mockProject} />);
|
mountWithContexts(<ProjectDetail project={mockProject} />);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -95,6 +101,10 @@ describe('<ProjectDetail />', () => {
|
|||||||
`${mockProject.scm_update_cache_timeout} Seconds`
|
`${mockProject.scm_update_cache_timeout} Seconds`
|
||||||
);
|
);
|
||||||
assertDetail('Ansible Environment', mockProject.custom_virtualenv);
|
assertDetail('Ansible Environment', mockProject.custom_virtualenv);
|
||||||
|
assertDetail(
|
||||||
|
'Execution Environment',
|
||||||
|
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');
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ function ProjectEdit({ project }) {
|
|||||||
} = await ProjectsAPI.update(project.id, {
|
} = await ProjectsAPI.update(project.id, {
|
||||||
...values,
|
...values,
|
||||||
organization: values.organization.id,
|
organization: values.organization.id,
|
||||||
|
default_environment: values.default_environment?.id || null,
|
||||||
});
|
});
|
||||||
history.push(`/projects/${id}/details`);
|
history.push(`/projects/${id}/details`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import ContentLoading from '../../../components/ContentLoading';
|
|||||||
import FormActionGroup from '../../../components/FormActionGroup/FormActionGroup';
|
import FormActionGroup from '../../../components/FormActionGroup/FormActionGroup';
|
||||||
import FormField, { FormSubmitError } from '../../../components/FormField';
|
import FormField, { FormSubmitError } from '../../../components/FormField';
|
||||||
import OrganizationLookup from '../../../components/Lookup/OrganizationLookup';
|
import OrganizationLookup from '../../../components/Lookup/OrganizationLookup';
|
||||||
|
import ExecutionEnvironmentLookup from '../../../components/Lookup/ExecutionEnvironmentLookup';
|
||||||
import { CredentialTypesAPI, ProjectsAPI } from '../../../api';
|
import { CredentialTypesAPI, ProjectsAPI } from '../../../api';
|
||||||
import { required } from '../../../util/validators';
|
import { required } from '../../../util/validators';
|
||||||
import {
|
import {
|
||||||
@@ -101,6 +102,14 @@ function ProjectFormFields({
|
|||||||
validate: required(i18n._(t`Select a value for this field`), i18n),
|
validate: required(i18n._(t`Select a value for this field`), i18n),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [
|
||||||
|
executionEnvironmentField,
|
||||||
|
executionEnvironmentMeta,
|
||||||
|
executionEnvironmentHelpers,
|
||||||
|
] = useField({
|
||||||
|
name: 'default_environment',
|
||||||
|
});
|
||||||
|
|
||||||
/* Save current scm subform field values to state */
|
/* Save current scm subform field values to state */
|
||||||
const saveSubFormState = form => {
|
const saveSubFormState = form => {
|
||||||
const currentScmFormFields = { ...scmFormFields };
|
const currentScmFormFields = { ...scmFormFields };
|
||||||
@@ -178,6 +187,25 @@ function ProjectFormFields({
|
|||||||
required
|
required
|
||||||
autoPopulate={!project?.id}
|
autoPopulate={!project?.id}
|
||||||
/>
|
/>
|
||||||
|
<ExecutionEnvironmentLookup
|
||||||
|
helperTextInvalid={executionEnvironmentMeta.error}
|
||||||
|
isValid={
|
||||||
|
!executionEnvironmentMeta.touched || !executionEnvironmentMeta.error
|
||||||
|
}
|
||||||
|
onBlur={() => executionEnvironmentHelpers.setTouched()}
|
||||||
|
value={executionEnvironmentField.value}
|
||||||
|
onChange={value => executionEnvironmentHelpers.setValue(value)}
|
||||||
|
popoverContent={i18n._(
|
||||||
|
t`Select the default execution environment for this project.`
|
||||||
|
)}
|
||||||
|
tooltip={i18n._(
|
||||||
|
t`Select an organization before editing the default execution environment.`
|
||||||
|
)}
|
||||||
|
globallyAvailable
|
||||||
|
isDisabled={!organizationField.value}
|
||||||
|
organizationId={organizationField.value?.id}
|
||||||
|
isDefaultEnvironment
|
||||||
|
/>
|
||||||
<FormGroup
|
<FormGroup
|
||||||
fieldId="project-scm-type"
|
fieldId="project-scm-type"
|
||||||
helperTextInvalid={scmTypeMeta.error}
|
helperTextInvalid={scmTypeMeta.error}
|
||||||
@@ -387,6 +415,8 @@ function ProjectForm({ i18n, project, submitError, ...props }) {
|
|||||||
scm_update_cache_timeout: project.scm_update_cache_timeout || 0,
|
scm_update_cache_timeout: project.scm_update_cache_timeout || 0,
|
||||||
scm_update_on_launch: project.scm_update_on_launch || false,
|
scm_update_on_launch: project.scm_update_on_launch || false,
|
||||||
scm_url: project.scm_url || '',
|
scm_url: project.scm_url || '',
|
||||||
|
default_environment:
|
||||||
|
project.summary_fields?.default_environment || null,
|
||||||
}}
|
}}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -27,7 +27,10 @@ function JobTemplateAdd() {
|
|||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
data: { id, type },
|
data: { id, type },
|
||||||
} = await JobTemplatesAPI.create(remainingValues);
|
} = await JobTemplatesAPI.create({
|
||||||
|
...remainingValues,
|
||||||
|
execution_environment: values.execution_environment?.id,
|
||||||
|
});
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
submitLabels(id, labels, values.project.summary_fields.organization.id),
|
submitLabels(id, labels, values.project.summary_fields.organization.id),
|
||||||
submitInstanceGroups(id, instanceGroups),
|
submitInstanceGroups(id, instanceGroups),
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ const jobTemplateData = {
|
|||||||
timeout: 0,
|
timeout: 0,
|
||||||
use_fact_cache: false,
|
use_fact_cache: false,
|
||||||
verbosity: '0',
|
verbosity: '0',
|
||||||
|
execution_environment: { id: 1, name: 'Foo' },
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('<JobTemplateAdd />', () => {
|
describe('<JobTemplateAdd />', () => {
|
||||||
@@ -77,6 +78,12 @@ describe('<JobTemplateAdd />', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
LabelsAPI.read.mockResolvedValue({ data: { results: [] } });
|
LabelsAPI.read.mockResolvedValue({ data: { results: [] } });
|
||||||
|
ProjectsAPI.readDetail.mockReturnValue({
|
||||||
|
name: 'foo',
|
||||||
|
id: 1,
|
||||||
|
allow_override: true,
|
||||||
|
organization: 1,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -126,12 +133,13 @@ describe('<JobTemplateAdd />', () => {
|
|||||||
...jobTemplateData,
|
...jobTemplateData,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
let wrapper;
|
let wrapper;
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(<JobTemplateAdd />);
|
wrapper = mountWithContexts(<JobTemplateAdd />);
|
||||||
});
|
});
|
||||||
await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0);
|
await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0);
|
||||||
act(() => {
|
await act(() => {
|
||||||
wrapper.find('input#template-name').simulate('change', {
|
wrapper.find('input#template-name').simulate('change', {
|
||||||
target: { value: 'Bar', name: 'name' },
|
target: { value: 'Bar', name: 'name' },
|
||||||
});
|
});
|
||||||
@@ -144,6 +152,10 @@ describe('<JobTemplateAdd />', () => {
|
|||||||
name: 'project',
|
name: 'project',
|
||||||
summary_fields: { organization: { id: 1, name: 'Org Foo' } },
|
summary_fields: { organization: { id: 1, name: 'Org Foo' } },
|
||||||
});
|
});
|
||||||
|
wrapper.find('ExecutionEnvironmentLookup').invoke('onChange')({
|
||||||
|
id: 1,
|
||||||
|
name: 'Foo',
|
||||||
|
});
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
wrapper.find('Select#template-playbook').prop('onToggle')();
|
wrapper.find('Select#template-playbook').prop('onToggle')();
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
@@ -170,6 +182,7 @@ describe('<JobTemplateAdd />', () => {
|
|||||||
inventory: 2,
|
inventory: 2,
|
||||||
webhook_credential: undefined,
|
webhook_credential: undefined,
|
||||||
webhook_service: '',
|
webhook_service: '',
|
||||||
|
execution_environment: 1,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -190,7 +203,7 @@ describe('<JobTemplateAdd />', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0);
|
await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0);
|
||||||
act(() => {
|
await act(async () => {
|
||||||
wrapper.find('input#template-name').simulate('change', {
|
wrapper.find('input#template-name').simulate('change', {
|
||||||
target: { value: 'Foo', name: 'name' },
|
target: { value: 'Foo', name: 'name' },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -206,6 +206,12 @@ function JobTemplateDetail({ i18n, template }) {
|
|||||||
) : (
|
) : (
|
||||||
<DeletedDetail label={i18n._(t`Project`)} />
|
<DeletedDetail label={i18n._(t`Project`)} />
|
||||||
)}
|
)}
|
||||||
|
{summary_fields?.execution_environment && (
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Execution Environment`)}
|
||||||
|
value={summary_fields.execution_environment.name}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Detail
|
<Detail
|
||||||
label={i18n._(t`Source Control Branch`)}
|
label={i18n._(t`Source Control Branch`)}
|
||||||
value={template.scm_branch}
|
value={template.scm_branch}
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ describe('<JobTemplateDetail />', () => {
|
|||||||
el => el.length === 0
|
el => el.length === 0
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('webhook fields should render properly', () => {
|
test('webhook fields should render properly', () => {
|
||||||
expect(wrapper.find('Detail[label="Webhook Service"]').length).toBe(1);
|
expect(wrapper.find('Detail[label="Webhook Service"]').length).toBe(1);
|
||||||
expect(wrapper.find('Detail[label="Webhook Service"]').prop('value')).toBe(
|
expect(wrapper.find('Detail[label="Webhook Service"]').prop('value')).toBe(
|
||||||
@@ -154,4 +155,13 @@ describe('<JobTemplateDetail />', () => {
|
|||||||
expect(wrapper.find('Detail[label="Webhook Key"]').length).toBe(1);
|
expect(wrapper.find('Detail[label="Webhook Key"]').length).toBe(1);
|
||||||
expect(wrapper.find('Detail[label="Webhook Credential"]').length).toBe(1);
|
expect(wrapper.find('Detail[label="Webhook Credential"]').length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('execution environment field should render properly', () => {
|
||||||
|
expect(wrapper.find('Detail[label="Execution Environment"]').length).toBe(
|
||||||
|
1
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
wrapper.find('Detail[label="Execution Environment"]').prop('value')
|
||||||
|
).toBe('Default EE');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -57,7 +57,10 @@ function JobTemplateEdit({ template }) {
|
|||||||
remainingValues.project = values.project.id;
|
remainingValues.project = values.project.id;
|
||||||
remainingValues.webhook_credential = webhook_credential?.id || null;
|
remainingValues.webhook_credential = webhook_credential?.id || null;
|
||||||
try {
|
try {
|
||||||
await JobTemplatesAPI.update(template.id, remainingValues);
|
await JobTemplatesAPI.update(template.id, {
|
||||||
|
...remainingValues,
|
||||||
|
execution_environment: values.execution_environment?.id,
|
||||||
|
});
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
submitLabels(labels, template?.organization),
|
submitLabels(labels, template?.organization),
|
||||||
submitInstanceGroups(instanceGroups, initialInstanceGroups),
|
submitInstanceGroups(instanceGroups, initialInstanceGroups),
|
||||||
|
|||||||
@@ -28,7 +28,10 @@ function WorkflowJobTemplateAdd() {
|
|||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
data: { id },
|
data: { id },
|
||||||
} = await WorkflowJobTemplatesAPI.create(templatePayload);
|
} = await WorkflowJobTemplatesAPI.create({
|
||||||
|
...templatePayload,
|
||||||
|
execution_environment: values.execution_environment?.id,
|
||||||
|
});
|
||||||
await Promise.all(await submitLabels(id, labels, organizationId));
|
await Promise.all(await submitLabels(id, labels, organizationId));
|
||||||
history.push(`/templates/workflow_job_template/${id}/visualizer`);
|
history.push(`/templates/workflow_job_template/${id}/visualizer`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -125,6 +125,12 @@ function WorkflowJobTemplateDetail({ template, i18n }) {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{summary_fields?.execution_environment && (
|
||||||
|
<Detail
|
||||||
|
label={i18n._(t`Execution Environment`)}
|
||||||
|
value={summary_fields.execution_environment.name}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Detail label={i18n._(t`Job Type`)} value={toTitleCase(type)} />
|
<Detail label={i18n._(t`Job Type`)} value={toTitleCase(type)} />
|
||||||
{summary_fields.inventory && (
|
{summary_fields.inventory && (
|
||||||
<Detail
|
<Detail
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ describe('<WorkflowJobTemplateDetail/>', () => {
|
|||||||
created_by: { id: 1, username: 'Athena' },
|
created_by: { id: 1, username: 'Athena' },
|
||||||
modified_by: { id: 1, username: 'Apollo' },
|
modified_by: { id: 1, username: 'Apollo' },
|
||||||
organization: { id: 1, name: 'Org' },
|
organization: { id: 1, name: 'Org' },
|
||||||
|
execution_environment: {
|
||||||
|
id: 4,
|
||||||
|
name: 'Demo EE',
|
||||||
|
description: '',
|
||||||
|
image: 'quay.io/ansible/awx-ee',
|
||||||
|
},
|
||||||
inventory: { kind: 'Foo', id: 1, name: 'Bar' },
|
inventory: { kind: 'Foo', id: 1, name: 'Bar' },
|
||||||
labels: {
|
labels: {
|
||||||
results: [
|
results: [
|
||||||
@@ -40,6 +46,7 @@ describe('<WorkflowJobTemplateDetail/>', () => {
|
|||||||
},
|
},
|
||||||
webhook_service: 'Github',
|
webhook_service: 'Github',
|
||||||
webhook_key: 'Foo webhook key',
|
webhook_key: 'Foo webhook key',
|
||||||
|
execution_environment: 4,
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@@ -127,6 +134,11 @@ describe('<WorkflowJobTemplateDetail/>', () => {
|
|||||||
prop: 'value',
|
prop: 'value',
|
||||||
value: 'Workflow Job Template',
|
value: 'Workflow Job Template',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
element: 'Detail[label="Execution Environment"]',
|
||||||
|
prop: 'value',
|
||||||
|
value: 'Demo EE',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const organization = wrapper
|
const organization = wrapper
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ function WorkflowJobTemplateEdit({ template }) {
|
|||||||
...templatePayload
|
...templatePayload
|
||||||
} = values;
|
} = values;
|
||||||
templatePayload.inventory = inventory?.id || null;
|
templatePayload.inventory = inventory?.id || null;
|
||||||
templatePayload.organization = organization?.id;
|
templatePayload.organization = organization?.id || null;
|
||||||
templatePayload.webhook_credential = webhook_credential?.id || null;
|
templatePayload.webhook_credential = webhook_credential?.id || null;
|
||||||
|
|
||||||
const formOrgId =
|
const formOrgId =
|
||||||
@@ -29,7 +29,10 @@ function WorkflowJobTemplateEdit({ template }) {
|
|||||||
await Promise.all(
|
await Promise.all(
|
||||||
await submitLabels(labels, formOrgId, template.organization)
|
await submitLabels(labels, formOrgId, template.organization)
|
||||||
);
|
);
|
||||||
await WorkflowJobTemplatesAPI.update(template.id, templatePayload);
|
await WorkflowJobTemplatesAPI.update(template.id, {
|
||||||
|
...templatePayload,
|
||||||
|
execution_environment: values.execution_environment?.id,
|
||||||
|
});
|
||||||
history.push(`/templates/workflow_job_template/${template.id}/details`);
|
history.push(`/templates/workflow_job_template/${template.id}/details`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setFormSubmitError(err);
|
setFormSubmitError(err);
|
||||||
|
|||||||
@@ -37,9 +37,10 @@ import {
|
|||||||
InstanceGroupsLookup,
|
InstanceGroupsLookup,
|
||||||
ProjectLookup,
|
ProjectLookup,
|
||||||
MultiCredentialsLookup,
|
MultiCredentialsLookup,
|
||||||
|
ExecutionEnvironmentLookup,
|
||||||
} from '../../../components/Lookup';
|
} from '../../../components/Lookup';
|
||||||
import Popover from '../../../components/Popover';
|
import Popover from '../../../components/Popover';
|
||||||
import { JobTemplatesAPI } from '../../../api';
|
import { JobTemplatesAPI, ProjectsAPI } from '../../../api';
|
||||||
import LabelSelect from './LabelSelect';
|
import LabelSelect from './LabelSelect';
|
||||||
import PlaybookSelect from './PlaybookSelect';
|
import PlaybookSelect from './PlaybookSelect';
|
||||||
import WebhookSubForm from './WebhookSubForm';
|
import WebhookSubForm from './WebhookSubForm';
|
||||||
@@ -101,10 +102,40 @@ function JobTemplateForm({
|
|||||||
'webhook_credential'
|
'webhook_credential'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [
|
||||||
|
executionEnvironmentField,
|
||||||
|
executionEnvironmentMeta,
|
||||||
|
executionEnvironmentHelpers,
|
||||||
|
] = useField({ name: 'execution_environment' });
|
||||||
|
|
||||||
|
const projectId = projectField.value?.id;
|
||||||
|
|
||||||
|
const {
|
||||||
|
request: fetchProject,
|
||||||
|
error: fetchProjectError,
|
||||||
|
isLoading: fetchProjectLoading,
|
||||||
|
result: projectData,
|
||||||
|
} = useRequest(
|
||||||
|
useCallback(async () => {
|
||||||
|
if (!projectId) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const { data } = await ProjectsAPI.readDetail(projectId);
|
||||||
|
return data;
|
||||||
|
}, [projectId]),
|
||||||
|
{
|
||||||
|
projectData: null,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchProject();
|
||||||
|
}, [fetchProject]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
request: loadRelatedInstanceGroups,
|
request: loadRelatedInstanceGroups,
|
||||||
error: instanceGroupError,
|
error: instanceGroupError,
|
||||||
contentLoading: instanceGroupLoading,
|
isLoading: instanceGroupLoading,
|
||||||
} = useRequest(
|
} = useRequest(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
if (!template?.id) {
|
if (!template?.id) {
|
||||||
@@ -182,12 +213,16 @@ function JobTemplateForm({
|
|||||||
callbackUrl = `${origin}${path}`;
|
callbackUrl = `${origin}${path}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (instanceGroupLoading) {
|
if (instanceGroupLoading || fetchProjectLoading) {
|
||||||
return <ContentLoading />;
|
return <ContentLoading />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contentError || instanceGroupError) {
|
if (contentError || instanceGroupError || fetchProjectError) {
|
||||||
return <ContentError error={contentError || instanceGroupError} />;
|
return (
|
||||||
|
<ContentError
|
||||||
|
error={contentError || instanceGroupError || fetchProjectError}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -258,6 +293,7 @@ function JobTemplateForm({
|
|||||||
isOverrideDisabled={isOverrideDisabledLookup}
|
isOverrideDisabled={isOverrideDisabledLookup}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<ProjectLookup
|
<ProjectLookup
|
||||||
value={projectField.value}
|
value={projectField.value}
|
||||||
onBlur={() => projectHelpers.setTouched()}
|
onBlur={() => projectHelpers.setTouched()}
|
||||||
@@ -270,6 +306,26 @@ function JobTemplateForm({
|
|||||||
autoPopulate={!template?.id}
|
autoPopulate={!template?.id}
|
||||||
isOverrideDisabled={isOverrideDisabledLookup}
|
isOverrideDisabled={isOverrideDisabledLookup}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ExecutionEnvironmentLookup
|
||||||
|
helperTextInvalid={executionEnvironmentMeta.error}
|
||||||
|
isValid={
|
||||||
|
!executionEnvironmentMeta.touched || !executionEnvironmentMeta.error
|
||||||
|
}
|
||||||
|
onBlur={() => executionEnvironmentHelpers.setTouched()}
|
||||||
|
value={executionEnvironmentField.value}
|
||||||
|
onChange={value => executionEnvironmentHelpers.setValue(value)}
|
||||||
|
popoverContent={i18n._(
|
||||||
|
t`Select the execution environment for this job template.`
|
||||||
|
)}
|
||||||
|
tooltip={i18n._(
|
||||||
|
t`Select a project before editing the execution environment.`
|
||||||
|
)}
|
||||||
|
globallyAvailable
|
||||||
|
isDisabled={!projectField.value}
|
||||||
|
organizationId={projectData?.organization}
|
||||||
|
/>
|
||||||
|
|
||||||
{projectField.value?.allow_override && (
|
{projectField.value?.allow_override && (
|
||||||
<FieldWithPrompt
|
<FieldWithPrompt
|
||||||
fieldId="template-scm-branch"
|
fieldId="template-scm-branch"
|
||||||
@@ -703,6 +759,8 @@ const FormikApp = withFormik({
|
|||||||
template.webhook_key ||
|
template.webhook_key ||
|
||||||
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:
|
||||||
|
template.summary_fields?.execution_environment || '',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
handleSubmit: async (values, { props, setErrors }) => {
|
handleSubmit: async (values, { props, setErrors }) => {
|
||||||
|
|||||||
@@ -22,7 +22,10 @@ import {
|
|||||||
SubFormLayout,
|
SubFormLayout,
|
||||||
} from '../../../components/FormLayout';
|
} from '../../../components/FormLayout';
|
||||||
import OrganizationLookup from '../../../components/Lookup/OrganizationLookup';
|
import OrganizationLookup from '../../../components/Lookup/OrganizationLookup';
|
||||||
import { InventoryLookup } from '../../../components/Lookup';
|
import {
|
||||||
|
InventoryLookup,
|
||||||
|
ExecutionEnvironmentLookup,
|
||||||
|
} from '../../../components/Lookup';
|
||||||
import { VariablesField } from '../../../components/CodeMirrorInput';
|
import { VariablesField } from '../../../components/CodeMirrorInput';
|
||||||
import FormActionGroup from '../../../components/FormActionGroup';
|
import FormActionGroup from '../../../components/FormActionGroup';
|
||||||
import ContentError from '../../../components/ContentError';
|
import ContentError from '../../../components/ContentError';
|
||||||
@@ -63,6 +66,14 @@ function WorkflowJobTemplateForm({
|
|||||||
'webhook_credential'
|
'webhook_credential'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [
|
||||||
|
executionEnvironmentField,
|
||||||
|
executionEnvironmentMeta,
|
||||||
|
executionEnvironmentHelpers,
|
||||||
|
] = useField({
|
||||||
|
name: 'execution_environment',
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (enableWebhooks) {
|
if (enableWebhooks) {
|
||||||
webhookServiceHelpers.setValue(webhookServiceMeta.initialValue);
|
webhookServiceHelpers.setValue(webhookServiceMeta.initialValue);
|
||||||
@@ -178,6 +189,20 @@ function WorkflowJobTemplateForm({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FieldWithPrompt>
|
</FieldWithPrompt>
|
||||||
|
<ExecutionEnvironmentLookup
|
||||||
|
helperTextInvalid={executionEnvironmentMeta.error}
|
||||||
|
isValid={
|
||||||
|
!executionEnvironmentMeta.touched || !executionEnvironmentMeta.error
|
||||||
|
}
|
||||||
|
onBlur={() => executionEnvironmentHelpers.setTouched()}
|
||||||
|
value={executionEnvironmentField.value}
|
||||||
|
onChange={value => executionEnvironmentHelpers.setValue(value)}
|
||||||
|
tooltip={i18n._(
|
||||||
|
t`Select the default execution environment for this organization to run on.`
|
||||||
|
)}
|
||||||
|
globallyAvailable
|
||||||
|
organizationId={organizationField.value?.id}
|
||||||
|
/>
|
||||||
</FormColumnLayout>
|
</FormColumnLayout>
|
||||||
<FormFullWidthLayout>
|
<FormFullWidthLayout>
|
||||||
<FormGroup
|
<FormGroup
|
||||||
@@ -296,6 +321,8 @@ const FormikApp = withFormik({
|
|||||||
? `${urlOrigin}${template.related.webhook_receiver}`
|
? `${urlOrigin}${template.related.webhook_receiver}`
|
||||||
: '',
|
: '',
|
||||||
webhook_key: template.webhook_key || '',
|
webhook_key: template.webhook_key || '',
|
||||||
|
execution_environment:
|
||||||
|
template.summary_fields?.execution_environment || '',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
handleSubmit: async (values, { props, setErrors }) => {
|
handleSubmit: async (values, { props, setErrors }) => {
|
||||||
|
|||||||
@@ -133,6 +133,12 @@
|
|||||||
"id": "1",
|
"id": "1",
|
||||||
"name": "Webhook Credential"
|
"name": "Webhook Credential"
|
||||||
|
|
||||||
|
},
|
||||||
|
"execution_environment": {
|
||||||
|
"id": 1,
|
||||||
|
"name": "Default EE",
|
||||||
|
"description": "",
|
||||||
|
"image": "quay.io/ansible/awx-ee"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"created": "2019-09-30T16:18:34.564820Z",
|
"created": "2019-09-30T16:18:34.564820Z",
|
||||||
@@ -177,5 +183,6 @@
|
|||||||
"job_slice_count": 1,
|
"job_slice_count": 1,
|
||||||
"webhook_credential": 1,
|
"webhook_credential": 1,
|
||||||
"webhook_key": "asertdyuhjkhgfd234567kjgfds",
|
"webhook_key": "asertdyuhjkhgfd234567kjgfds",
|
||||||
"webhook_service": "github"
|
"webhook_service": "github",
|
||||||
|
"execution_environment": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -416,4 +416,5 @@ export const ExecutionEnvironment = shape({
|
|||||||
url: string,
|
url: string,
|
||||||
summary_fields: shape({}),
|
summary_fields: shape({}),
|
||||||
description: string,
|
description: string,
|
||||||
|
pull: string,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user