mirror of
https://github.com/ansible/awx.git
synced 2026-01-11 01:57:35 -03: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:
parent
adf708366a
commit
6e67ae68fd
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 HostFilterLookup } from './HostFilterLookup';
|
||||
export { default as OrganizationLookup } from './OrganizationLookup';
|
||||
export { default as ExecutionEnvironmentLookup } from './ExecutionEnvironmentLookup';
|
||||
|
||||
@ -13,7 +13,10 @@ function OrganizationAdd() {
|
||||
|
||||
const handleSubmit = async (values, groupsToAssociate) => {
|
||||
try {
|
||||
const { data: response } = await OrganizationsAPI.create(values);
|
||||
const { data: response } = await OrganizationsAPI.create({
|
||||
...values,
|
||||
default_environment: values.default_environment?.id,
|
||||
});
|
||||
await Promise.all(
|
||||
groupsToAssociate
|
||||
.map(id => OrganizationsAPI.associateInstanceGroup(response.id, id))
|
||||
|
||||
@ -17,13 +17,18 @@ describe('<OrganizationAdd />', () => {
|
||||
description: 'new description',
|
||||
custom_virtualenv: 'Buzz',
|
||||
galaxy_credentials: [],
|
||||
default_environment: { id: 1, name: 'Foo' },
|
||||
};
|
||||
OrganizationsAPI.create.mockResolvedValueOnce({ data: {} });
|
||||
await act(async () => {
|
||||
const wrapper = mountWithContexts(<OrganizationAdd />);
|
||||
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 () => {
|
||||
|
||||
@ -94,6 +94,12 @@ function OrganizationDetail({ i18n, organization }) {
|
||||
label={i18n._(t`Ansible Environment`)}
|
||||
value={custom_virtualenv}
|
||||
/>
|
||||
{summary_fields?.default_environment?.name && (
|
||||
<Detail
|
||||
label={i18n._(t`Default Execution Environment`)}
|
||||
value={summary_fields.default_environment.name}
|
||||
/>
|
||||
)}
|
||||
<UserDateDetail
|
||||
label={i18n._(t`Created`)}
|
||||
date={created}
|
||||
|
||||
@ -13,6 +13,7 @@ jest.mock('../../../api');
|
||||
|
||||
describe('<OrganizationDetail />', () => {
|
||||
const mockOrganization = {
|
||||
id: 12,
|
||||
name: 'Foo',
|
||||
description: 'Bar',
|
||||
custom_virtualenv: 'Fizz',
|
||||
@ -24,7 +25,14 @@ describe('<OrganizationDetail />', () => {
|
||||
edit: true,
|
||||
delete: true,
|
||||
},
|
||||
default_environment: {
|
||||
id: 1,
|
||||
name: 'Default EE',
|
||||
description: '',
|
||||
image: 'quay.io/ansible/awx-ee',
|
||||
},
|
||||
},
|
||||
default_environment: 1,
|
||||
};
|
||||
const mockInstanceGroups = {
|
||||
data: {
|
||||
@ -43,7 +51,7 @@ describe('<OrganizationDetail />', () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('initially renders succesfully', async () => {
|
||||
test('initially renders successfully', async () => {
|
||||
await act(async () => {
|
||||
mountWithContexts(<OrganizationDetail organization={mockOrganization} />);
|
||||
});
|
||||
@ -86,6 +94,7 @@ describe('<OrganizationDetail />', () => {
|
||||
{ label: 'Created', value: '7/7/2015, 5:21:26 PM' },
|
||||
{ label: 'Last Modified', value: '8/11/2019, 7:47:37 PM' },
|
||||
{ label: 'Max Hosts', value: '0' },
|
||||
{ label: 'Default Execution Environment', value: 'Default EE' },
|
||||
];
|
||||
for (let i = 0; i < testParams.length; i++) {
|
||||
const { label, value } = testParams[i];
|
||||
|
||||
@ -28,7 +28,10 @@ function OrganizationEdit({ organization }) {
|
||||
const addedCredentialIds = addedCredentials.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(
|
||||
groupsToAssociate
|
||||
.map(id =>
|
||||
|
||||
@ -19,6 +19,13 @@ describe('<OrganizationEdit />', () => {
|
||||
related: {
|
||||
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 () => {
|
||||
@ -31,6 +38,7 @@ describe('<OrganizationEdit />', () => {
|
||||
name: 'new name',
|
||||
description: 'new description',
|
||||
custom_virtualenv: 'Buzz',
|
||||
default_environment: null,
|
||||
};
|
||||
wrapper.find('OrganizationForm').prop('onSubmit')(updatedOrgData, [], []);
|
||||
|
||||
|
||||
@ -12,16 +12,21 @@ import ContentError from '../../../components/ContentError';
|
||||
import ContentLoading from '../../../components/ContentLoading';
|
||||
import FormField, { FormSubmitError } from '../../../components/FormField';
|
||||
import FormActionGroup from '../../../components/FormActionGroup/FormActionGroup';
|
||||
import { InstanceGroupsLookup } from '../../../components/Lookup';
|
||||
import {
|
||||
InstanceGroupsLookup,
|
||||
ExecutionEnvironmentLookup,
|
||||
} from '../../../components/Lookup';
|
||||
import { getAddedAndRemoved } from '../../../util/lists';
|
||||
import { required, minMaxValue } from '../../../util/validators';
|
||||
import { FormColumnLayout } from '../../../components/FormLayout';
|
||||
import CredentialLookup from '../../../components/Lookup/CredentialLookup';
|
||||
|
||||
function OrganizationFormFields({ i18n, instanceGroups, setInstanceGroups }) {
|
||||
const { license_info = {}, me = {} } = useConfig();
|
||||
const { custom_virtualenvs } = useContext(ConfigContext);
|
||||
|
||||
const { setFieldValue } = useFormikContext();
|
||||
const [venvField] = useField('custom_virtualenv');
|
||||
const { license_info = {}, me = {} } = useConfig();
|
||||
|
||||
const [
|
||||
galaxyCredentialsField,
|
||||
@ -29,12 +34,19 @@ function OrganizationFormFields({ i18n, instanceGroups, setInstanceGroups }) {
|
||||
galaxyCredentialsHelpers,
|
||||
] = useField('galaxy_credentials');
|
||||
|
||||
const [
|
||||
executionEnvironmentField,
|
||||
executionEnvironmentMeta,
|
||||
executionEnvironmentHelpers,
|
||||
] = useField({
|
||||
name: 'default_environment',
|
||||
});
|
||||
|
||||
const defaultVenv = {
|
||||
label: i18n._(t`Use Default Ansible Environment`),
|
||||
value: '/var/lib/awx/venv/ansible/',
|
||||
key: 'default',
|
||||
};
|
||||
const { custom_virtualenvs } = useContext(ConfigContext);
|
||||
|
||||
const handleCredentialUpdate = useCallback(
|
||||
value => {
|
||||
@ -100,6 +112,20 @@ function OrganizationFormFields({ i18n, instanceGroups, setInstanceGroups }) {
|
||||
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
|
||||
credentialTypeNamespace="galaxy_api_token"
|
||||
label={i18n._(t`Galaxy Credentials`)}
|
||||
@ -185,6 +211,8 @@ function OrganizationForm({
|
||||
custom_virtualenv: organization.custom_virtualenv || '',
|
||||
max_hosts: organization.max_hosts || '0',
|
||||
galaxy_credentials: organization.galaxy_credentials || [],
|
||||
default_environment:
|
||||
organization.summary_fields?.default_environment || '',
|
||||
}}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
@ -221,6 +249,7 @@ OrganizationForm.defaultProps = {
|
||||
description: '',
|
||||
max_hosts: '0',
|
||||
custom_virtualenv: '',
|
||||
default_environment: '',
|
||||
},
|
||||
submitError: null,
|
||||
};
|
||||
|
||||
@ -4,7 +4,7 @@ import {
|
||||
mountWithContexts,
|
||||
waitForElement,
|
||||
} from '../../../../testUtils/enzymeHelpers';
|
||||
import { OrganizationsAPI } from '../../../api';
|
||||
import { OrganizationsAPI, ExecutionEnvironmentsAPI } from '../../../api';
|
||||
|
||||
import OrganizationForm from './OrganizationForm';
|
||||
|
||||
@ -32,6 +32,8 @@ describe('<OrganizationForm />', () => {
|
||||
{ name: 'Two', id: 2 },
|
||||
];
|
||||
|
||||
const mockExecutionEnvironment = [{ name: 'EE' }];
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
@ -132,6 +134,11 @@ describe('<OrganizationForm />', () => {
|
||||
results: mockInstanceGroups,
|
||||
},
|
||||
});
|
||||
ExecutionEnvironmentsAPI.read.mockReturnValue({
|
||||
data: {
|
||||
results: mockExecutionEnvironment,
|
||||
},
|
||||
});
|
||||
let wrapper;
|
||||
const onSubmit = jest.fn();
|
||||
await act(async () => {
|
||||
@ -155,10 +162,15 @@ describe('<OrganizationForm />', () => {
|
||||
wrapper.find('input#org-max_hosts').simulate('change', {
|
||||
target: { value: 134, name: 'max_hosts' },
|
||||
});
|
||||
wrapper.find('ExecutionEnvironmentLookup').invoke('onChange')({
|
||||
id: 1,
|
||||
name: 'Test EE',
|
||||
});
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper.find('button[aria-label="Save"]').simulate('click');
|
||||
});
|
||||
wrapper.update();
|
||||
expect(onSubmit).toHaveBeenCalledTimes(1);
|
||||
expect(onSubmit.mock.calls[0][0]).toEqual({
|
||||
name: 'new foo',
|
||||
@ -166,6 +178,7 @@ describe('<OrganizationForm />', () => {
|
||||
galaxy_credentials: [],
|
||||
custom_virtualenv: 'Fizz',
|
||||
max_hosts: 134,
|
||||
default_environment: { id: 1, name: 'Test EE' },
|
||||
});
|
||||
});
|
||||
|
||||
@ -209,12 +222,16 @@ describe('<OrganizationForm />', () => {
|
||||
results: mockInstanceGroups,
|
||||
},
|
||||
});
|
||||
ExecutionEnvironmentsAPI.read.mockReturnValue({
|
||||
data: { results: mockExecutionEnvironment },
|
||||
});
|
||||
const mockDataForm = {
|
||||
name: 'Foo',
|
||||
description: 'Bar',
|
||||
galaxy_credentials: [],
|
||||
max_hosts: 1,
|
||||
custom_virtualenv: 'Fizz',
|
||||
default_environment: '',
|
||||
};
|
||||
const onSubmit = jest.fn();
|
||||
OrganizationsAPI.update.mockResolvedValue(1, mockDataForm);
|
||||
@ -320,6 +337,7 @@ describe('<OrganizationForm />', () => {
|
||||
galaxy_credentials: [],
|
||||
max_hosts: 0,
|
||||
custom_virtualenv: 'Fizz',
|
||||
default_environment: '',
|
||||
},
|
||||
[],
|
||||
[]
|
||||
|
||||
@ -27,6 +27,7 @@ function ProjectAdd() {
|
||||
} = await ProjectsAPI.create({
|
||||
...values,
|
||||
organization: values.organization.id,
|
||||
default_environment: values.default_environment?.id,
|
||||
});
|
||||
history.push(`/projects/${id}/details`);
|
||||
} catch (error) {
|
||||
|
||||
@ -20,11 +20,12 @@ describe('<ProjectAdd />', () => {
|
||||
scm_clean: true,
|
||||
credential: 100,
|
||||
local_path: '',
|
||||
organization: 2,
|
||||
organization: { id: 2, name: 'Bar' },
|
||||
scm_update_on_launch: true,
|
||||
scm_update_cache_timeout: 3,
|
||||
allow_override: false,
|
||||
custom_virtualenv: '/var/lib/awx/venv/custom-env',
|
||||
default_environment: { id: 1, name: 'Foo' },
|
||||
};
|
||||
|
||||
const projectOptionsResolve = {
|
||||
@ -102,6 +103,11 @@ describe('<ProjectAdd />', () => {
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
wrapper.find('ProjectForm').invoke('handleSubmit')(projectData);
|
||||
expect(ProjectsAPI.create).toHaveBeenCalledTimes(1);
|
||||
expect(ProjectsAPI.create).toHaveBeenCalledWith({
|
||||
...projectData,
|
||||
organization: 2,
|
||||
default_environment: 1,
|
||||
});
|
||||
});
|
||||
|
||||
test('handleSubmit should throw an error', async () => {
|
||||
|
||||
@ -124,10 +124,18 @@ function ProjectDetail({ project, i18n }) {
|
||||
label={i18n._(t`Cache Timeout`)}
|
||||
value={`${scm_update_cache_timeout} ${i18n._(t`Seconds`)}`}
|
||||
/>
|
||||
|
||||
<Detail
|
||||
label={i18n._(t`Ansible Environment`)}
|
||||
value={custom_virtualenv}
|
||||
/>
|
||||
{summary_fields?.default_environment?.name && (
|
||||
<Detail
|
||||
label={i18n._(t`Execution Environment`)}
|
||||
value={summary_fields.default_environment.name}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Config>
|
||||
{({ project_base_dir }) => (
|
||||
<Detail
|
||||
@ -137,6 +145,7 @@ function ProjectDetail({ project, i18n }) {
|
||||
)}
|
||||
</Config>
|
||||
<Detail label={i18n._(t`Playbook Directory`)} value={local_path} />
|
||||
|
||||
<UserDateDetail
|
||||
label={i18n._(t`Created`)}
|
||||
date={created}
|
||||
|
||||
@ -25,6 +25,11 @@ describe('<ProjectDetail />', () => {
|
||||
id: 10,
|
||||
name: 'Foo',
|
||||
},
|
||||
default_environment: {
|
||||
id: 12,
|
||||
name: 'Bar',
|
||||
image: 'quay.io/ansible/awx-ee',
|
||||
},
|
||||
credential: {
|
||||
id: 1000,
|
||||
name: 'qux',
|
||||
@ -67,9 +72,10 @@ describe('<ProjectDetail />', () => {
|
||||
scm_update_cache_timeout: 5,
|
||||
allow_override: true,
|
||||
custom_virtualenv: '/custom-venv',
|
||||
default_environment: 1,
|
||||
};
|
||||
|
||||
test('initially renders succesfully', () => {
|
||||
test('initially renders successfully', () => {
|
||||
mountWithContexts(<ProjectDetail project={mockProject} />);
|
||||
});
|
||||
|
||||
@ -95,6 +101,10 @@ describe('<ProjectDetail />', () => {
|
||||
`${mockProject.scm_update_cache_timeout} Seconds`
|
||||
);
|
||||
assertDetail('Ansible Environment', mockProject.custom_virtualenv);
|
||||
assertDetail(
|
||||
'Execution Environment',
|
||||
mockProject.summary_fields.default_environment.name
|
||||
);
|
||||
const dateDetails = wrapper.find('UserDateDetail');
|
||||
expect(dateDetails).toHaveLength(2);
|
||||
expect(dateDetails.at(0).prop('label')).toEqual('Created');
|
||||
|
||||
@ -26,6 +26,7 @@ function ProjectEdit({ project }) {
|
||||
} = await ProjectsAPI.update(project.id, {
|
||||
...values,
|
||||
organization: values.organization.id,
|
||||
default_environment: values.default_environment?.id || null,
|
||||
});
|
||||
history.push(`/projects/${id}/details`);
|
||||
} catch (error) {
|
||||
|
||||
@ -12,6 +12,7 @@ import ContentLoading from '../../../components/ContentLoading';
|
||||
import FormActionGroup from '../../../components/FormActionGroup/FormActionGroup';
|
||||
import FormField, { FormSubmitError } from '../../../components/FormField';
|
||||
import OrganizationLookup from '../../../components/Lookup/OrganizationLookup';
|
||||
import ExecutionEnvironmentLookup from '../../../components/Lookup/ExecutionEnvironmentLookup';
|
||||
import { CredentialTypesAPI, ProjectsAPI } from '../../../api';
|
||||
import { required } from '../../../util/validators';
|
||||
import {
|
||||
@ -101,6 +102,14 @@ function ProjectFormFields({
|
||||
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 */
|
||||
const saveSubFormState = form => {
|
||||
const currentScmFormFields = { ...scmFormFields };
|
||||
@ -178,6 +187,25 @@ function ProjectFormFields({
|
||||
required
|
||||
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
|
||||
fieldId="project-scm-type"
|
||||
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_on_launch: project.scm_update_on_launch || false,
|
||||
scm_url: project.scm_url || '',
|
||||
default_environment:
|
||||
project.summary_fields?.default_environment || null,
|
||||
}}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
|
||||
@ -27,7 +27,10 @@ function JobTemplateAdd() {
|
||||
try {
|
||||
const {
|
||||
data: { id, type },
|
||||
} = await JobTemplatesAPI.create(remainingValues);
|
||||
} = await JobTemplatesAPI.create({
|
||||
...remainingValues,
|
||||
execution_environment: values.execution_environment?.id,
|
||||
});
|
||||
await Promise.all([
|
||||
submitLabels(id, labels, values.project.summary_fields.organization.id),
|
||||
submitInstanceGroups(id, instanceGroups),
|
||||
|
||||
@ -58,6 +58,7 @@ const jobTemplateData = {
|
||||
timeout: 0,
|
||||
use_fact_cache: false,
|
||||
verbosity: '0',
|
||||
execution_environment: { id: 1, name: 'Foo' },
|
||||
};
|
||||
|
||||
describe('<JobTemplateAdd />', () => {
|
||||
@ -77,6 +78,12 @@ describe('<JobTemplateAdd />', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
LabelsAPI.read.mockResolvedValue({ data: { results: [] } });
|
||||
ProjectsAPI.readDetail.mockReturnValue({
|
||||
name: 'foo',
|
||||
id: 1,
|
||||
allow_override: true,
|
||||
organization: 1,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -126,12 +133,13 @@ describe('<JobTemplateAdd />', () => {
|
||||
...jobTemplateData,
|
||||
},
|
||||
});
|
||||
|
||||
let wrapper;
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<JobTemplateAdd />);
|
||||
});
|
||||
await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0);
|
||||
act(() => {
|
||||
await act(() => {
|
||||
wrapper.find('input#template-name').simulate('change', {
|
||||
target: { value: 'Bar', name: 'name' },
|
||||
});
|
||||
@ -144,6 +152,10 @@ describe('<JobTemplateAdd />', () => {
|
||||
name: 'project',
|
||||
summary_fields: { organization: { id: 1, name: 'Org Foo' } },
|
||||
});
|
||||
wrapper.find('ExecutionEnvironmentLookup').invoke('onChange')({
|
||||
id: 1,
|
||||
name: 'Foo',
|
||||
});
|
||||
wrapper.update();
|
||||
wrapper.find('Select#template-playbook').prop('onToggle')();
|
||||
wrapper.update();
|
||||
@ -170,6 +182,7 @@ describe('<JobTemplateAdd />', () => {
|
||||
inventory: 2,
|
||||
webhook_credential: undefined,
|
||||
webhook_service: '',
|
||||
execution_environment: 1,
|
||||
});
|
||||
});
|
||||
|
||||
@ -190,7 +203,7 @@ describe('<JobTemplateAdd />', () => {
|
||||
});
|
||||
});
|
||||
await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0);
|
||||
act(() => {
|
||||
await act(async () => {
|
||||
wrapper.find('input#template-name').simulate('change', {
|
||||
target: { value: 'Foo', name: 'name' },
|
||||
});
|
||||
|
||||
@ -206,6 +206,12 @@ function JobTemplateDetail({ i18n, template }) {
|
||||
) : (
|
||||
<DeletedDetail label={i18n._(t`Project`)} />
|
||||
)}
|
||||
{summary_fields?.execution_environment && (
|
||||
<Detail
|
||||
label={i18n._(t`Execution Environment`)}
|
||||
value={summary_fields.execution_environment.name}
|
||||
/>
|
||||
)}
|
||||
<Detail
|
||||
label={i18n._(t`Source Control Branch`)}
|
||||
value={template.scm_branch}
|
||||
|
||||
@ -142,6 +142,7 @@ describe('<JobTemplateDetail />', () => {
|
||||
el => el.length === 0
|
||||
);
|
||||
});
|
||||
|
||||
test('webhook fields should render properly', () => {
|
||||
expect(wrapper.find('Detail[label="Webhook Service"]').length).toBe(1);
|
||||
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 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.webhook_credential = webhook_credential?.id || null;
|
||||
try {
|
||||
await JobTemplatesAPI.update(template.id, remainingValues);
|
||||
await JobTemplatesAPI.update(template.id, {
|
||||
...remainingValues,
|
||||
execution_environment: values.execution_environment?.id,
|
||||
});
|
||||
await Promise.all([
|
||||
submitLabels(labels, template?.organization),
|
||||
submitInstanceGroups(instanceGroups, initialInstanceGroups),
|
||||
|
||||
@ -28,7 +28,10 @@ function WorkflowJobTemplateAdd() {
|
||||
try {
|
||||
const {
|
||||
data: { id },
|
||||
} = await WorkflowJobTemplatesAPI.create(templatePayload);
|
||||
} = await WorkflowJobTemplatesAPI.create({
|
||||
...templatePayload,
|
||||
execution_environment: values.execution_environment?.id,
|
||||
});
|
||||
await Promise.all(await submitLabels(id, labels, organizationId));
|
||||
history.push(`/templates/workflow_job_template/${id}/visualizer`);
|
||||
} 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)} />
|
||||
{summary_fields.inventory && (
|
||||
<Detail
|
||||
|
||||
@ -22,6 +22,12 @@ describe('<WorkflowJobTemplateDetail/>', () => {
|
||||
created_by: { id: 1, username: 'Athena' },
|
||||
modified_by: { id: 1, username: 'Apollo' },
|
||||
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' },
|
||||
labels: {
|
||||
results: [
|
||||
@ -40,6 +46,7 @@ describe('<WorkflowJobTemplateDetail/>', () => {
|
||||
},
|
||||
webhook_service: 'Github',
|
||||
webhook_key: 'Foo webhook key',
|
||||
execution_environment: 4,
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
@ -127,6 +134,11 @@ describe('<WorkflowJobTemplateDetail/>', () => {
|
||||
prop: 'value',
|
||||
value: 'Workflow Job Template',
|
||||
},
|
||||
{
|
||||
element: 'Detail[label="Execution Environment"]',
|
||||
prop: 'value',
|
||||
value: 'Demo EE',
|
||||
},
|
||||
];
|
||||
|
||||
const organization = wrapper
|
||||
|
||||
@ -20,7 +20,7 @@ function WorkflowJobTemplateEdit({ template }) {
|
||||
...templatePayload
|
||||
} = values;
|
||||
templatePayload.inventory = inventory?.id || null;
|
||||
templatePayload.organization = organization?.id;
|
||||
templatePayload.organization = organization?.id || null;
|
||||
templatePayload.webhook_credential = webhook_credential?.id || null;
|
||||
|
||||
const formOrgId =
|
||||
@ -29,7 +29,10 @@ function WorkflowJobTemplateEdit({ template }) {
|
||||
await Promise.all(
|
||||
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`);
|
||||
} catch (err) {
|
||||
setFormSubmitError(err);
|
||||
|
||||
@ -37,9 +37,10 @@ import {
|
||||
InstanceGroupsLookup,
|
||||
ProjectLookup,
|
||||
MultiCredentialsLookup,
|
||||
ExecutionEnvironmentLookup,
|
||||
} from '../../../components/Lookup';
|
||||
import Popover from '../../../components/Popover';
|
||||
import { JobTemplatesAPI } from '../../../api';
|
||||
import { JobTemplatesAPI, ProjectsAPI } from '../../../api';
|
||||
import LabelSelect from './LabelSelect';
|
||||
import PlaybookSelect from './PlaybookSelect';
|
||||
import WebhookSubForm from './WebhookSubForm';
|
||||
@ -101,10 +102,40 @@ function JobTemplateForm({
|
||||
'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 {
|
||||
request: loadRelatedInstanceGroups,
|
||||
error: instanceGroupError,
|
||||
contentLoading: instanceGroupLoading,
|
||||
isLoading: instanceGroupLoading,
|
||||
} = useRequest(
|
||||
useCallback(async () => {
|
||||
if (!template?.id) {
|
||||
@ -182,12 +213,16 @@ function JobTemplateForm({
|
||||
callbackUrl = `${origin}${path}`;
|
||||
}
|
||||
|
||||
if (instanceGroupLoading) {
|
||||
if (instanceGroupLoading || fetchProjectLoading) {
|
||||
return <ContentLoading />;
|
||||
}
|
||||
|
||||
if (contentError || instanceGroupError) {
|
||||
return <ContentError error={contentError || instanceGroupError} />;
|
||||
if (contentError || instanceGroupError || fetchProjectError) {
|
||||
return (
|
||||
<ContentError
|
||||
error={contentError || instanceGroupError || fetchProjectError}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@ -258,6 +293,7 @@ function JobTemplateForm({
|
||||
isOverrideDisabled={isOverrideDisabledLookup}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<ProjectLookup
|
||||
value={projectField.value}
|
||||
onBlur={() => projectHelpers.setTouched()}
|
||||
@ -270,6 +306,26 @@ function JobTemplateForm({
|
||||
autoPopulate={!template?.id}
|
||||
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 && (
|
||||
<FieldWithPrompt
|
||||
fieldId="template-scm-branch"
|
||||
@ -703,6 +759,8 @@ const FormikApp = withFormik({
|
||||
template.webhook_key ||
|
||||
i18n._(t`a new webhook key will be generated on save.`).toUpperCase(),
|
||||
webhook_credential: template?.summary_fields?.webhook_credential || null,
|
||||
execution_environment:
|
||||
template.summary_fields?.execution_environment || '',
|
||||
};
|
||||
},
|
||||
handleSubmit: async (values, { props, setErrors }) => {
|
||||
|
||||
@ -22,7 +22,10 @@ import {
|
||||
SubFormLayout,
|
||||
} from '../../../components/FormLayout';
|
||||
import OrganizationLookup from '../../../components/Lookup/OrganizationLookup';
|
||||
import { InventoryLookup } from '../../../components/Lookup';
|
||||
import {
|
||||
InventoryLookup,
|
||||
ExecutionEnvironmentLookup,
|
||||
} from '../../../components/Lookup';
|
||||
import { VariablesField } from '../../../components/CodeMirrorInput';
|
||||
import FormActionGroup from '../../../components/FormActionGroup';
|
||||
import ContentError from '../../../components/ContentError';
|
||||
@ -63,6 +66,14 @@ function WorkflowJobTemplateForm({
|
||||
'webhook_credential'
|
||||
);
|
||||
|
||||
const [
|
||||
executionEnvironmentField,
|
||||
executionEnvironmentMeta,
|
||||
executionEnvironmentHelpers,
|
||||
] = useField({
|
||||
name: 'execution_environment',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (enableWebhooks) {
|
||||
webhookServiceHelpers.setValue(webhookServiceMeta.initialValue);
|
||||
@ -178,6 +189,20 @@ function WorkflowJobTemplateForm({
|
||||
}}
|
||||
/>
|
||||
</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>
|
||||
<FormFullWidthLayout>
|
||||
<FormGroup
|
||||
@ -296,6 +321,8 @@ const FormikApp = withFormik({
|
||||
? `${urlOrigin}${template.related.webhook_receiver}`
|
||||
: '',
|
||||
webhook_key: template.webhook_key || '',
|
||||
execution_environment:
|
||||
template.summary_fields?.execution_environment || '',
|
||||
};
|
||||
},
|
||||
handleSubmit: async (values, { props, setErrors }) => {
|
||||
|
||||
@ -133,6 +133,12 @@
|
||||
"id": "1",
|
||||
"name": "Webhook Credential"
|
||||
|
||||
},
|
||||
"execution_environment": {
|
||||
"id": 1,
|
||||
"name": "Default EE",
|
||||
"description": "",
|
||||
"image": "quay.io/ansible/awx-ee"
|
||||
}
|
||||
},
|
||||
"created": "2019-09-30T16:18:34.564820Z",
|
||||
@ -177,5 +183,6 @@
|
||||
"job_slice_count": 1,
|
||||
"webhook_credential": 1,
|
||||
"webhook_key": "asertdyuhjkhgfd234567kjgfds",
|
||||
"webhook_service": "github"
|
||||
"webhook_service": "github",
|
||||
"execution_environment": 1
|
||||
}
|
||||
|
||||
@ -416,4 +416,5 @@ export const ExecutionEnvironment = shape({
|
||||
url: string,
|
||||
summary_fields: shape({}),
|
||||
description: string,
|
||||
pull: string,
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user