Skip to content

Commit

Permalink
Add Execution Environments into a few screens
Browse files Browse the repository at this point in the history
Add EE to the following screens:

* Job Template
* Organization
* Project
* Workflow Job Template

Also, add a new lookup component - ExecutionEnvironmentLoookup.

See: ansible#9189
  • Loading branch information
nixocio committed Feb 24, 2021
1 parent b42a39a commit 29da1dd
Show file tree
Hide file tree
Showing 30 changed files with 559 additions and 25 deletions.
167 changes: 167 additions & 0 deletions awx/ui_next/src/components/Lookup/ExecutionEnvironmentLookup.jsx
Original file line number Diff line number Diff line change
@@ -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));
Original file line number Diff line number Diff line change
@@ -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);
});
});
1 change: 1 addition & 0 deletions awx/ui_next/src/components/Lookup/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ jest.mock('../../../api');

describe('<OrganizationDetail />', () => {
const mockOrganization = {
id: 12,
name: 'Foo',
description: 'Bar',
custom_virtualenv: 'Fizz',
Expand All @@ -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: {
Expand All @@ -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} />);
});
Expand Down Expand Up @@ -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];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -31,6 +38,7 @@ describe('<OrganizationEdit />', () => {
name: 'new name',
description: 'new description',
custom_virtualenv: 'Buzz',
default_environment: null,
};
wrapper.find('OrganizationForm').prop('onSubmit')(updatedOrgData, [], []);

Expand Down
Loading

0 comments on commit 29da1dd

Please sign in to comment.