Skip to content

Commit 661befd

Browse files
nixocioshanemcd
authored andcommitted
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: ansible#9189
1 parent ff0301b commit 661befd

30 files changed

+558
-24
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import React, { useCallback, useEffect } from 'react';
2+
import { string, func, bool } from 'prop-types';
3+
import { withRouter, useLocation } from 'react-router-dom';
4+
import { withI18n } from '@lingui/react';
5+
import { t } from '@lingui/macro';
6+
import { FormGroup, Tooltip } from '@patternfly/react-core';
7+
8+
import { ExecutionEnvironmentsAPI } from '../../api';
9+
import { ExecutionEnvironment } from '../../types';
10+
import { getQSConfig, parseQueryString, mergeParams } from '../../util/qs';
11+
import Popover from '../Popover';
12+
import OptionsList from '../OptionsList';
13+
import useRequest from '../../util/useRequest';
14+
15+
import Lookup from './Lookup';
16+
import LookupErrorMessage from './shared/LookupErrorMessage';
17+
18+
const QS_CONFIG = getQSConfig('execution_environments', {
19+
page: 1,
20+
page_size: 5,
21+
order_by: 'name',
22+
});
23+
24+
function ExecutionEnvironmentLookup({
25+
globallyAvailable,
26+
i18n,
27+
isDefaultEnvironment,
28+
isDisabled,
29+
onChange,
30+
organizationId,
31+
popoverContent,
32+
tooltip,
33+
value,
34+
onBlur,
35+
}) {
36+
const location = useLocation();
37+
38+
const {
39+
result: {
40+
executionEnvironments,
41+
count,
42+
relatedSearchableKeys,
43+
searchableKeys,
44+
},
45+
request: fetchExecutionEnvironments,
46+
error,
47+
isLoading,
48+
} = useRequest(
49+
useCallback(async () => {
50+
const params = parseQueryString(QS_CONFIG, location.search);
51+
const globallyAvailableParams = globallyAvailable
52+
? { or__organization__isnull: 'True' }
53+
: {};
54+
const organizationIdParams = organizationId
55+
? { or__organization__id: organizationId }
56+
: {};
57+
const [{ data }, actionsResponse] = await Promise.all([
58+
ExecutionEnvironmentsAPI.read(
59+
mergeParams(params, {
60+
...globallyAvailableParams,
61+
...organizationIdParams,
62+
})
63+
),
64+
ExecutionEnvironmentsAPI.readOptions(),
65+
]);
66+
return {
67+
executionEnvironments: data.results,
68+
count: data.count,
69+
relatedSearchableKeys: (
70+
actionsResponse?.data?.related_search_fields || []
71+
).map(val => val.slice(0, -8)),
72+
searchableKeys: Object.keys(
73+
actionsResponse.data.actions?.GET || {}
74+
).filter(key => actionsResponse.data.actions?.GET[key].filterable),
75+
};
76+
}, [location, globallyAvailable, organizationId]),
77+
{
78+
executionEnvironments: [],
79+
count: 0,
80+
relatedSearchableKeys: [],
81+
searchableKeys: [],
82+
}
83+
);
84+
85+
useEffect(() => {
86+
fetchExecutionEnvironments();
87+
}, [fetchExecutionEnvironments]);
88+
89+
const renderLookup = () => (
90+
<>
91+
<Lookup
92+
id="execution-environments"
93+
header={i18n._(t`Execution Environments`)}
94+
value={value}
95+
onBlur={onBlur}
96+
onChange={onChange}
97+
qsConfig={QS_CONFIG}
98+
isLoading={isLoading}
99+
isDisabled={isDisabled}
100+
renderOptionsList={({ state, dispatch, canDelete }) => (
101+
<OptionsList
102+
value={state.selectedItems}
103+
options={executionEnvironments}
104+
optionCount={count}
105+
searchColumns={[
106+
{
107+
name: i18n._(t`Name`),
108+
key: 'name__icontains',
109+
isDefault: true,
110+
},
111+
]}
112+
sortColumns={[
113+
{
114+
name: i18n._(t`Name`),
115+
key: 'name',
116+
},
117+
]}
118+
searchableKeys={searchableKeys}
119+
relatedSearchableKeys={relatedSearchableKeys}
120+
multiple={state.multiple}
121+
header={i18n._(t`Execution Environment`)}
122+
name="executionEnvironments"
123+
qsConfig={QS_CONFIG}
124+
readOnly={!canDelete}
125+
selectItem={item => dispatch({ type: 'SELECT_ITEM', item })}
126+
deselectItem={item => dispatch({ type: 'DESELECT_ITEM', item })}
127+
/>
128+
)}
129+
/>
130+
</>
131+
);
132+
133+
return (
134+
<FormGroup
135+
fieldId="execution-environment-lookup"
136+
label={
137+
isDefaultEnvironment
138+
? i18n._(t`Default Execution Environment`)
139+
: i18n._(t`Execution Environment`)
140+
}
141+
labelIcon={popoverContent && <Popover content={popoverContent} />}
142+
>
143+
{isDisabled ? (
144+
<Tooltip content={tooltip}>{renderLookup()}</Tooltip>
145+
) : (
146+
renderLookup()
147+
)}
148+
149+
<LookupErrorMessage error={error} />
150+
</FormGroup>
151+
);
152+
}
153+
154+
ExecutionEnvironmentLookup.propTypes = {
155+
value: ExecutionEnvironment,
156+
popoverContent: string,
157+
onChange: func.isRequired,
158+
isDefaultEnvironment: bool,
159+
};
160+
161+
ExecutionEnvironmentLookup.defaultProps = {
162+
popoverContent: '',
163+
isDefaultEnvironment: false,
164+
value: null,
165+
};
166+
167+
export default withI18n()(withRouter(ExecutionEnvironmentLookup));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import React from 'react';
2+
import { act } from 'react-dom/test-utils';
3+
import { mountWithContexts } from '../../../testUtils/enzymeHelpers';
4+
import ExecutionEnvironmentLookup from './ExecutionEnvironmentLookup';
5+
import { ExecutionEnvironmentsAPI } from '../../api';
6+
7+
jest.mock('../../api');
8+
9+
const mockedExecutionEnvironments = {
10+
count: 1,
11+
results: [
12+
{
13+
id: 2,
14+
name: 'Foo',
15+
image: 'quay.io/ansible/awx-ee',
16+
pull: 'missing',
17+
},
18+
],
19+
};
20+
21+
const executionEnvironment = {
22+
id: 42,
23+
name: 'Bar',
24+
image: 'quay.io/ansible/bar',
25+
pull: 'missing',
26+
};
27+
28+
describe('ExecutionEnvironmentLookup', () => {
29+
let wrapper;
30+
31+
beforeEach(() => {
32+
ExecutionEnvironmentsAPI.read.mockResolvedValue(
33+
mockedExecutionEnvironments
34+
);
35+
});
36+
37+
afterEach(() => {
38+
jest.clearAllMocks();
39+
wrapper.unmount();
40+
});
41+
42+
test('should render successfully', async () => {
43+
ExecutionEnvironmentsAPI.readOptions.mockReturnValue({
44+
data: {
45+
actions: {
46+
GET: {},
47+
POST: {},
48+
},
49+
related_search_fields: [],
50+
},
51+
});
52+
await act(async () => {
53+
wrapper = mountWithContexts(
54+
<ExecutionEnvironmentLookup
55+
value={executionEnvironment}
56+
onChange={() => {}}
57+
/>
58+
);
59+
});
60+
wrapper.update();
61+
expect(ExecutionEnvironmentsAPI.read).toHaveBeenCalledTimes(1);
62+
expect(wrapper.find('ExecutionEnvironmentLookup')).toHaveLength(1);
63+
});
64+
65+
test('should fetch execution environments', async () => {
66+
await act(async () => {
67+
wrapper = mountWithContexts(
68+
<ExecutionEnvironmentLookup
69+
value={executionEnvironment}
70+
onChange={() => {}}
71+
/>
72+
);
73+
});
74+
expect(ExecutionEnvironmentsAPI.read).toHaveBeenCalledTimes(1);
75+
});
76+
});

awx/ui_next/src/components/Lookup/index.js

+1
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ export { default as CredentialLookup } from './CredentialLookup';
77
export { default as ApplicationLookup } from './ApplicationLookup';
88
export { default as HostFilterLookup } from './HostFilterLookup';
99
export { default as OrganizationLookup } from './OrganizationLookup';
10+
export { default as ExecutionEnvironmentLookup } from './ExecutionEnvironmentLookup';

awx/ui_next/src/screens/Organization/OrganizationAdd/OrganizationAdd.jsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ function OrganizationAdd() {
1313

1414
const handleSubmit = async (values, groupsToAssociate) => {
1515
try {
16-
const { data: response } = await OrganizationsAPI.create(values);
16+
const { data: response } = await OrganizationsAPI.create({
17+
...values,
18+
default_environment: values.default_environment?.id,
19+
});
1720
await Promise.all(
1821
groupsToAssociate
1922
.map(id => OrganizationsAPI.associateInstanceGroup(response.id, id))

awx/ui_next/src/screens/Organization/OrganizationAdd/OrganizationAdd.test.jsx

+6-1
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,18 @@ describe('<OrganizationAdd />', () => {
1717
description: 'new description',
1818
custom_virtualenv: 'Buzz',
1919
galaxy_credentials: [],
20+
default_environment: { id: 1, name: 'Foo' },
2021
};
2122
OrganizationsAPI.create.mockResolvedValueOnce({ data: {} });
2223
await act(async () => {
2324
const wrapper = mountWithContexts(<OrganizationAdd />);
2425
wrapper.find('OrganizationForm').prop('onSubmit')(updatedOrgData, []);
2526
});
26-
expect(OrganizationsAPI.create).toHaveBeenCalledWith(updatedOrgData);
27+
expect(OrganizationsAPI.create).toHaveBeenCalledWith({
28+
...updatedOrgData,
29+
default_environment: 1,
30+
});
31+
expect(OrganizationsAPI.create).toHaveBeenCalledTimes(1);
2732
});
2833

2934
test('should navigate to organizations list when cancel is clicked', async () => {

awx/ui_next/src/screens/Organization/OrganizationDetail/OrganizationDetail.jsx

+6
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,12 @@ function OrganizationDetail({ i18n, organization }) {
9494
label={i18n._(t`Ansible Environment`)}
9595
value={custom_virtualenv}
9696
/>
97+
{summary_fields?.default_environment?.name && (
98+
<Detail
99+
label={i18n._(t`Default Execution Environment`)}
100+
value={summary_fields.default_environment.name}
101+
/>
102+
)}
97103
<UserDateDetail
98104
label={i18n._(t`Created`)}
99105
date={created}

awx/ui_next/src/screens/Organization/OrganizationDetail/OrganizationDetail.test.jsx

+10-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ jest.mock('../../../api');
1313

1414
describe('<OrganizationDetail />', () => {
1515
const mockOrganization = {
16+
id: 12,
1617
name: 'Foo',
1718
description: 'Bar',
1819
custom_virtualenv: 'Fizz',
@@ -24,7 +25,14 @@ describe('<OrganizationDetail />', () => {
2425
edit: true,
2526
delete: true,
2627
},
28+
default_environment: {
29+
id: 1,
30+
name: 'Default EE',
31+
description: '',
32+
image: 'quay.io/ansible/awx-ee',
33+
},
2734
},
35+
default_environment: 1,
2836
};
2937
const mockInstanceGroups = {
3038
data: {
@@ -43,7 +51,7 @@ describe('<OrganizationDetail />', () => {
4351
jest.clearAllMocks();
4452
});
4553

46-
test('initially renders succesfully', async () => {
54+
test('initially renders successfully', async () => {
4755
await act(async () => {
4856
mountWithContexts(<OrganizationDetail organization={mockOrganization} />);
4957
});
@@ -86,6 +94,7 @@ describe('<OrganizationDetail />', () => {
8694
{ label: 'Created', value: '7/7/2015, 5:21:26 PM' },
8795
{ label: 'Last Modified', value: '8/11/2019, 7:47:37 PM' },
8896
{ label: 'Max Hosts', value: '0' },
97+
{ label: 'Default Execution Environment', value: 'Default EE' },
8998
];
9099
for (let i = 0; i < testParams.length; i++) {
91100
const { label, value } = testParams[i];

awx/ui_next/src/screens/Organization/OrganizationEdit/OrganizationEdit.jsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@ function OrganizationEdit({ organization }) {
2828
const addedCredentialIds = addedCredentials.map(({ id }) => id);
2929
const removedCredentialIds = removedCredentials.map(({ id }) => id);
3030

31-
await OrganizationsAPI.update(organization.id, values);
31+
await OrganizationsAPI.update(organization.id, {
32+
...values,
33+
default_environment: values.default_environment?.id || null,
34+
});
3235
await Promise.all(
3336
groupsToAssociate
3437
.map(id =>

awx/ui_next/src/screens/Organization/OrganizationEdit/OrganizationEdit.test.jsx

+8
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@ describe('<OrganizationEdit />', () => {
1919
related: {
2020
instance_groups: '/api/v2/organizations/1/instance_groups',
2121
},
22+
default_environment: 1,
23+
summary_fields: {
24+
default_environment: {
25+
id: 1,
26+
name: 'Baz',
27+
},
28+
},
2229
};
2330

2431
test('onSubmit should call api update', async () => {
@@ -31,6 +38,7 @@ describe('<OrganizationEdit />', () => {
3138
name: 'new name',
3239
description: 'new description',
3340
custom_virtualenv: 'Buzz',
41+
default_environment: null,
3442
};
3543
wrapper.find('OrganizationForm').prop('onSubmit')(updatedOrgData, [], []);
3644

0 commit comments

Comments
 (0)