Add EE to inventory sources

Add EE to inventory sources

See: https://github.com/ansible/awx/issues/9189
This commit is contained in:
nixocio
2021-03-02 09:22:09 -05:00
committed by Shane McDonald
parent 62215ca432
commit 0a4a1bed0a
9 changed files with 104 additions and 26 deletions

View File

@@ -1,6 +1,6 @@
import React, { useCallback, useEffect } from 'react'; import React, { useCallback, useEffect } from 'react';
import { string, func, bool } from 'prop-types'; import { string, func, bool } from 'prop-types';
import { withRouter, useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { withI18n } from '@lingui/react'; import { withI18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { FormGroup, Tooltip } from '@patternfly/react-core'; import { FormGroup, Tooltip } from '@patternfly/react-core';
@@ -164,4 +164,4 @@ ExecutionEnvironmentLookup.defaultProps = {
value: null, value: null,
}; };
export default withI18n()(withRouter(ExecutionEnvironmentLookup)); export default withI18n()(ExecutionEnvironmentLookup);

View File

@@ -1,14 +1,14 @@
import React, { useCallback, useEffect } from 'react'; import React, { useCallback, useEffect } from 'react';
import { useHistory, useParams } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { Card } from '@patternfly/react-core'; import { Card } from '@patternfly/react-core';
import { InventorySourcesAPI } from '../../../api'; import { InventorySourcesAPI } from '../../../api';
import useRequest from '../../../util/useRequest'; import useRequest from '../../../util/useRequest';
import { CardBody } from '../../../components/Card'; import { CardBody } from '../../../components/Card';
import InventorySourceForm from '../shared/InventorySourceForm'; import InventorySourceForm from '../shared/InventorySourceForm';
function InventorySourceAdd() { function InventorySourceAdd({ inventory }) {
const history = useHistory(); const history = useHistory();
const { id } = useParams(); const { id, organization } = inventory;
const { error, request, result } = useRequest( const { error, request, result } = useRequest(
useCallback(async values => { useCallback(async values => {
@@ -31,6 +31,7 @@ function InventorySourceAdd() {
source_path, source_path,
source_project, source_project,
source_script, source_script,
execution_environment,
...remainingForm ...remainingForm
} = form; } = form;
@@ -46,6 +47,7 @@ function InventorySourceAdd() {
credential: credential?.id || null, credential: credential?.id || null,
inventory: id, inventory: id,
source_script: source_script?.id || null, source_script: source_script?.id || null,
execution_environment: execution_environment?.id || null,
...sourcePath, ...sourcePath,
...sourceProject, ...sourceProject,
...remainingForm, ...remainingForm,
@@ -63,6 +65,7 @@ function InventorySourceAdd() {
onCancel={handleCancel} onCancel={handleCancel}
onSubmit={handleSubmit} onSubmit={handleSubmit}
submitError={error} submitError={error}
organizationId={organization}
/> />
</CardBody> </CardBody>
</Card> </Card>

View File

@@ -35,6 +35,12 @@ describe('<InventorySourceAdd />', () => {
verbosity: 1, verbosity: 1,
}; };
const mockInventory = {
id: 111,
name: 'Foo',
organization: 2,
};
InventorySourcesAPI.readOptions.mockResolvedValue({ InventorySourcesAPI.readOptions.mockResolvedValue({
data: { data: {
actions: { actions: {
@@ -72,9 +78,12 @@ describe('<InventorySourceAdd />', () => {
custom_virtualenvs: ['venv/foo', 'venv/bar'], custom_virtualenvs: ['venv/foo', 'venv/bar'],
}; };
await act(async () => { await act(async () => {
wrapper = mountWithContexts(<InventorySourceAdd />, { wrapper = mountWithContexts(
context: { config }, <InventorySourceAdd inventory={mockInventory} />,
}); {
context: { config },
}
);
}); });
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
expect(wrapper.find('FormGroup[label="Name"]')).toHaveLength(1); expect(wrapper.find('FormGroup[label="Name"]')).toHaveLength(1);
@@ -88,9 +97,12 @@ describe('<InventorySourceAdd />', () => {
test('should navigate to inventory sources list when cancel is clicked', async () => { test('should navigate to inventory sources list when cancel is clicked', async () => {
const history = createMemoryHistory({}); const history = createMemoryHistory({});
await act(async () => { await act(async () => {
wrapper = mountWithContexts(<InventorySourceAdd />, { wrapper = mountWithContexts(
context: { router: { history } }, <InventorySourceAdd inventory={mockInventory} />,
}); {
context: { router: { history } },
}
);
}); });
await act(async () => { await act(async () => {
wrapper.find('InventorySourceForm').invoke('onCancel')(); wrapper.find('InventorySourceForm').invoke('onCancel')();
@@ -103,7 +115,9 @@ describe('<InventorySourceAdd />', () => {
test('should post to the api when submit is clicked', async () => { test('should post to the api when submit is clicked', async () => {
InventorySourcesAPI.create.mockResolvedValueOnce({ data: {} }); InventorySourcesAPI.create.mockResolvedValueOnce({ data: {} });
await act(async () => { await act(async () => {
wrapper = mountWithContexts(<InventorySourceAdd />); wrapper = mountWithContexts(
<InventorySourceAdd inventory={mockInventory} />
);
}); });
await act(async () => { await act(async () => {
wrapper.find('InventorySourceForm').invoke('onSubmit')(invSourceData); wrapper.find('InventorySourceForm').invoke('onSubmit')(invSourceData);
@@ -114,6 +128,7 @@ describe('<InventorySourceAdd />', () => {
credential: 222, credential: 222,
source_project: 999, source_project: 999,
source_script: null, source_script: null,
execution_environment: null,
}); });
}); });
@@ -123,9 +138,12 @@ describe('<InventorySourceAdd />', () => {
data: { id: 123, inventory: 111 }, data: { id: 123, inventory: 111 },
}); });
await act(async () => { await act(async () => {
wrapper = mountWithContexts(<InventorySourceAdd />, { wrapper = mountWithContexts(
context: { router: { history } }, <InventorySourceAdd inventory={mockInventory} />,
}); {
context: { router: { history } },
}
);
}); });
await act(async () => { await act(async () => {
wrapper.find('InventorySourceForm').invoke('onSubmit')(invSourceData); wrapper.find('InventorySourceForm').invoke('onSubmit')(invSourceData);
@@ -143,7 +161,9 @@ describe('<InventorySourceAdd />', () => {
}; };
InventorySourcesAPI.create.mockImplementation(() => Promise.reject(error)); InventorySourcesAPI.create.mockImplementation(() => Promise.reject(error));
await act(async () => { await act(async () => {
wrapper = mountWithContexts(<InventorySourceAdd />); wrapper = mountWithContexts(
<InventorySourceAdd inventory={mockInventory} />
);
}); });
expect(wrapper.find('FormSubmitError').length).toBe(0); expect(wrapper.find('FormSubmitError').length).toBe(0);
await act(async () => { await act(async () => {

View File

@@ -50,6 +50,7 @@ function InventorySourceDetail({ inventorySource, i18n }) {
organization, organization,
source_project, source_project,
user_capabilities, user_capabilities,
execution_environment,
}, },
} = inventorySource; } = inventorySource;
const [deletionError, setDeletionError] = useState(false); const [deletionError, setDeletionError] = useState(false);
@@ -214,6 +215,18 @@ function InventorySourceDetail({ inventorySource, i18n }) {
} }
/> />
)} )}
{execution_environment?.name && (
<Detail
label={i18n._(t`Execution Environment`)}
value={
<Link
to={`/execution_environments/${execution_environment.id}/details`}
>
{execution_environment.name}
</Link>
}
/>
)}
{source === 'scm' ? ( {source === 'scm' ? (
<Detail <Detail
label={i18n._(t`Inventory file`)} label={i18n._(t`Inventory file`)}

View File

@@ -1,14 +1,14 @@
import React, { useCallback, useEffect } from 'react'; import React, { useCallback, useEffect } from 'react';
import { useHistory, useParams } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { Card } from '@patternfly/react-core'; import { Card } from '@patternfly/react-core';
import { CardBody } from '../../../components/Card'; import { CardBody } from '../../../components/Card';
import useRequest from '../../../util/useRequest'; import useRequest from '../../../util/useRequest';
import { InventorySourcesAPI } from '../../../api'; import { InventorySourcesAPI } from '../../../api';
import InventorySourceForm from '../shared/InventorySourceForm'; import InventorySourceForm from '../shared/InventorySourceForm';
function InventorySourceEdit({ source }) { function InventorySourceEdit({ source, inventory }) {
const history = useHistory(); const history = useHistory();
const { id } = useParams(); const { id, organization } = inventory;
const detailsUrl = `/inventories/inventory/${id}/sources/${source.id}/details`; const detailsUrl = `/inventories/inventory/${id}/sources/${source.id}/details`;
const { error, request, result } = useRequest( const { error, request, result } = useRequest(
@@ -34,6 +34,7 @@ function InventorySourceEdit({ source }) {
source_path, source_path,
source_project, source_project,
source_script, source_script,
execution_environment,
...remainingForm ...remainingForm
} = form; } = form;
@@ -49,6 +50,7 @@ function InventorySourceEdit({ source }) {
credential: credential?.id || null, credential: credential?.id || null,
inventory: id, inventory: id,
source_script: source_script?.id || null, source_script: source_script?.id || null,
execution_environment: execution_environment?.id || null,
...sourcePath, ...sourcePath,
...sourceProject, ...sourceProject,
...remainingForm, ...remainingForm,
@@ -67,6 +69,7 @@ function InventorySourceEdit({ source }) {
onCancel={handleCancel} onCancel={handleCancel}
onSubmit={handleSubmit} onSubmit={handleSubmit}
submitError={error} submitError={error}
organizationId={organization}
/> />
</CardBody> </CardBody>
</Card> </Card>

View File

@@ -18,7 +18,7 @@ jest.mock('react-router-dom', () => ({
}), }),
})); }));
describe('<InventorySourceAdd />', () => { describe('<InventorySourceEdit />', () => {
let wrapper; let wrapper;
let history; let history;
const mockInvSrc = { const mockInvSrc = {
@@ -37,6 +37,11 @@ describe('<InventorySourceAdd />', () => {
update_on_project_update: false, update_on_project_update: false,
verbosity: 1, verbosity: 1,
}; };
const mockInventory = {
id: 1,
name: 'Foo',
organization: 1,
};
InventorySourcesAPI.readOptions.mockResolvedValue({ InventorySourcesAPI.readOptions.mockResolvedValue({
data: { data: {
actions: { actions: {
@@ -89,9 +94,12 @@ describe('<InventorySourceAdd />', () => {
beforeAll(async () => { beforeAll(async () => {
history = createMemoryHistory(); history = createMemoryHistory();
await act(async () => { await act(async () => {
wrapper = mountWithContexts(<InventorySourceEdit source={mockInvSrc} />, { wrapper = mountWithContexts(
context: { router: { history } }, <InventorySourceEdit inventory={mockInventory} source={mockInvSrc} />,
}); {
context: { router: { history } },
}
);
}); });
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0); await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
}); });
@@ -133,7 +141,9 @@ describe('<InventorySourceAdd />', () => {
}; };
InventorySourcesAPI.replace.mockImplementation(() => Promise.reject(error)); InventorySourcesAPI.replace.mockImplementation(() => Promise.reject(error));
await act(async () => { await act(async () => {
wrapper = mountWithContexts(<InventorySourceEdit source={mockInvSrc} />); wrapper = mountWithContexts(
<InventorySourceEdit inventory={mockInventory} source={mockInvSrc} />
);
}); });
expect(wrapper.find('FormSubmitError').length).toBe(0); expect(wrapper.find('FormSubmitError').length).toBe(0);
await act(async () => { await act(async () => {

View File

@@ -9,7 +9,7 @@ function InventorySources({ inventory, setBreadcrumb }) {
return ( return (
<Switch> <Switch>
<Route key="add" path="/inventories/inventory/:id/sources/add"> <Route key="add" path="/inventories/inventory/:id/sources/add">
<InventorySourceAdd /> <InventorySourceAdd inventory={inventory} />
</Route> </Route>
<Route path="/inventories/inventory/:id/sources/:sourceId"> <Route path="/inventories/inventory/:id/sources/:sourceId">
<Config> <Config>

View File

@@ -31,6 +31,7 @@ import {
VMwareSubForm, VMwareSubForm,
VirtualizationSubForm, VirtualizationSubForm,
} from './InventorySourceSubForms'; } from './InventorySourceSubForms';
import { ExecutionEnvironmentLookup } from '../../../components/Lookup';
const buildSourceChoiceOptions = options => { const buildSourceChoiceOptions = options => {
const sourceChoices = options.actions.GET.source.choices.map( const sourceChoices = options.actions.GET.source.choices.map(
@@ -39,7 +40,12 @@ const buildSourceChoiceOptions = options => {
return sourceChoices.filter(({ key }) => key !== 'file'); return sourceChoices.filter(({ key }) => key !== 'file');
}; };
const InventorySourceFormFields = ({ source, sourceOptions, i18n }) => { const InventorySourceFormFields = ({
source,
sourceOptions,
organizationId,
i18n,
}) => {
const { const {
values, values,
initialValues, initialValues,
@@ -51,6 +57,13 @@ const InventorySourceFormFields = ({ source, sourceOptions, i18n }) => {
name: 'source', name: 'source',
validate: required(i18n._(t`Set a value for this field`), i18n), validate: required(i18n._(t`Set a value for this field`), i18n),
}); });
const [
executionEnvironmentField,
executionEnvironmentMeta,
executionEnvironmentHelpers,
] = useField({
name: 'execution_environment',
});
const { custom_virtualenvs } = useContext(ConfigContext); const { custom_virtualenvs } = useContext(ConfigContext);
const [venvField] = useField('custom_virtualenv'); const [venvField] = useField('custom_virtualenv');
const defaultVenv = { const defaultVenv = {
@@ -111,6 +124,17 @@ const InventorySourceFormFields = ({ source, sourceOptions, i18n }) => {
name="description" name="description"
type="text" type="text"
/> />
<ExecutionEnvironmentLookup
helperTextInvalid={executionEnvironmentMeta.error}
isValid={
!executionEnvironmentMeta.touched || !executionEnvironmentMeta.error
}
onBlur={() => executionEnvironmentHelpers.setTouched()}
value={executionEnvironmentField.value}
onChange={value => executionEnvironmentHelpers.setValue(value)}
globallyAvailable
organizationId={organizationId}
/>
<FormGroup <FormGroup
fieldId="source" fieldId="source"
helperTextInvalid={sourceMeta.error} helperTextInvalid={sourceMeta.error}
@@ -244,6 +268,7 @@ const InventorySourceForm = ({
onSubmit, onSubmit,
source, source,
submitError = null, submitError = null,
organizationId,
}) => { }) => {
const initialValues = { const initialValues = {
credential: source?.summary_fields?.credential || null, credential: source?.summary_fields?.credential || null,
@@ -264,6 +289,8 @@ const InventorySourceForm = ({
enabled_var: source?.enabled_var || '', enabled_var: source?.enabled_var || '',
enabled_value: source?.enabled_value || '', enabled_value: source?.enabled_value || '',
host_filter: source?.host_filter || '', host_filter: source?.host_filter || '',
execution_environment:
source?.summary_fields?.execution_environment || null,
}; };
const { const {
@@ -306,6 +333,7 @@ const InventorySourceForm = ({
i18n={i18n} i18n={i18n}
source={source} source={source}
sourceOptions={sourceOptions} sourceOptions={sourceOptions}
organizationId={organizationId}
/> />
{submitError && <FormSubmitError error={submitError} />} {submitError && <FormSubmitError error={submitError} />}
<FormActionGroup <FormActionGroup

View File

@@ -72,6 +72,7 @@ describe('<InventorySourceForm />', () => {
expect( expect(
wrapper.find('FormGroup[label="Ansible Environment"]') wrapper.find('FormGroup[label="Ansible Environment"]')
).toHaveLength(1); ).toHaveLength(1);
expect(wrapper.find('ExecutionEnvironmentLookup')).toHaveLength(1);
}); });
test('should display subform when source dropdown has a value', async () => { test('should display subform when source dropdown has a value', async () => {