mirror of
https://github.com/ansible/awx.git
synced 2026-03-09 13:39:27 -02:30
Merge pull request #7028 from marshmalien/6576-inv-src-edit-form
Add inventory source edit form Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useCallback, useEffect } from 'react';
|
||||||
import { bool, func, node, number, string, oneOfType } from 'prop-types';
|
import { bool, func, node, number, string, oneOfType } from 'prop-types';
|
||||||
import { withRouter } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
@@ -10,6 +10,7 @@ import { getQSConfig, parseQueryString, mergeParams } from '../../util/qs';
|
|||||||
import { FieldTooltip } from '../FormField';
|
import { FieldTooltip } from '../FormField';
|
||||||
import Lookup from './Lookup';
|
import Lookup from './Lookup';
|
||||||
import OptionsList from '../OptionsList';
|
import OptionsList from '../OptionsList';
|
||||||
|
import useRequest from '../../util/useRequest';
|
||||||
import LookupErrorMessage from './shared/LookupErrorMessage';
|
import LookupErrorMessage from './shared/LookupErrorMessage';
|
||||||
|
|
||||||
const QS_CONFIG = getQSConfig('credentials', {
|
const QS_CONFIG = getQSConfig('credentials', {
|
||||||
@@ -32,11 +33,12 @@ function CredentialLookup({
|
|||||||
i18n,
|
i18n,
|
||||||
tooltip,
|
tooltip,
|
||||||
}) {
|
}) {
|
||||||
const [credentials, setCredentials] = useState([]);
|
const {
|
||||||
const [count, setCount] = useState(0);
|
result: { count, credentials },
|
||||||
const [error, setError] = useState(null);
|
error,
|
||||||
useEffect(() => {
|
request: fetchCredentials,
|
||||||
(async () => {
|
} = useRequest(
|
||||||
|
useCallback(async () => {
|
||||||
const params = parseQueryString(QS_CONFIG, history.location.search);
|
const params = parseQueryString(QS_CONFIG, history.location.search);
|
||||||
const typeIdParams = credentialTypeId
|
const typeIdParams = credentialTypeId
|
||||||
? { credential_type: credentialTypeId }
|
? { credential_type: credentialTypeId }
|
||||||
@@ -45,19 +47,23 @@ function CredentialLookup({
|
|||||||
? { credential_type__kind: credentialTypeKind }
|
? { credential_type__kind: credentialTypeKind }
|
||||||
: {};
|
: {};
|
||||||
|
|
||||||
try {
|
const { data } = await CredentialsAPI.read(
|
||||||
const { data } = await CredentialsAPI.read(
|
mergeParams(params, { ...typeIdParams, ...typeKindParams })
|
||||||
mergeParams(params, { ...typeIdParams, ...typeKindParams })
|
);
|
||||||
);
|
return {
|
||||||
setCredentials(data.results);
|
count: data.count,
|
||||||
setCount(data.count);
|
credentials: data.results,
|
||||||
} catch (err) {
|
};
|
||||||
if (setError) {
|
}, [credentialTypeId, credentialTypeKind, history.location.search]),
|
||||||
setError(err);
|
{
|
||||||
}
|
count: 0,
|
||||||
}
|
credentials: [],
|
||||||
})();
|
}
|
||||||
}, [credentialTypeId, credentialTypeKind, history.location.search]);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchCredentials();
|
||||||
|
}, [fetchCredentials]);
|
||||||
|
|
||||||
// TODO: replace credential type search with REST-based grabbing of cred types
|
// TODO: replace credential type search with REST-based grabbing of cred types
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useCallback, useEffect } from 'react';
|
||||||
import { func, bool } from 'prop-types';
|
import { func, bool } from 'prop-types';
|
||||||
import { withRouter } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
@@ -7,6 +7,7 @@ import { InventoriesAPI } from '../../api';
|
|||||||
import { Inventory } from '../../types';
|
import { Inventory } from '../../types';
|
||||||
import Lookup from './Lookup';
|
import Lookup from './Lookup';
|
||||||
import OptionsList from '../OptionsList';
|
import OptionsList from '../OptionsList';
|
||||||
|
import useRequest from '../../util/useRequest';
|
||||||
import { getQSConfig, parseQueryString } from '../../util/qs';
|
import { getQSConfig, parseQueryString } from '../../util/qs';
|
||||||
import LookupErrorMessage from './shared/LookupErrorMessage';
|
import LookupErrorMessage from './shared/LookupErrorMessage';
|
||||||
|
|
||||||
@@ -17,22 +18,28 @@ const QS_CONFIG = getQSConfig('inventory', {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function InventoryLookup({ value, onChange, onBlur, required, i18n, history }) {
|
function InventoryLookup({ value, onChange, onBlur, required, i18n, history }) {
|
||||||
const [inventories, setInventories] = useState([]);
|
const {
|
||||||
const [count, setCount] = useState(0);
|
result: { count, inventories },
|
||||||
const [error, setError] = useState(null);
|
error,
|
||||||
|
request: fetchInventories,
|
||||||
|
} = useRequest(
|
||||||
|
useCallback(async () => {
|
||||||
|
const params = parseQueryString(QS_CONFIG, history.location.search);
|
||||||
|
const { data } = await InventoriesAPI.read(params);
|
||||||
|
return {
|
||||||
|
count: data.count,
|
||||||
|
inventories: data.results,
|
||||||
|
};
|
||||||
|
}, [history.location.search]),
|
||||||
|
{
|
||||||
|
count: 0,
|
||||||
|
inventories: [],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
fetchInventories();
|
||||||
const params = parseQueryString(QS_CONFIG, history.location.search);
|
}, [fetchInventories]);
|
||||||
try {
|
|
||||||
const { data } = await InventoriesAPI.read(params);
|
|
||||||
setInventories(data.results);
|
|
||||||
setCount(data.count);
|
|
||||||
} catch (err) {
|
|
||||||
setError(err);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}, [history.location]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useCallback, useEffect } from 'react';
|
||||||
import { node, string, func, bool } from 'prop-types';
|
import { node, string, func, bool } from 'prop-types';
|
||||||
import { withRouter } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
@@ -8,6 +8,7 @@ import { ProjectsAPI } from '../../api';
|
|||||||
import { Project } from '../../types';
|
import { Project } from '../../types';
|
||||||
import { FieldTooltip } from '../FormField';
|
import { FieldTooltip } from '../FormField';
|
||||||
import OptionsList from '../OptionsList';
|
import OptionsList from '../OptionsList';
|
||||||
|
import useRequest from '../../util/useRequest';
|
||||||
import { getQSConfig, parseQueryString } from '../../util/qs';
|
import { getQSConfig, parseQueryString } from '../../util/qs';
|
||||||
import Lookup from './Lookup';
|
import Lookup from './Lookup';
|
||||||
import LookupErrorMessage from './shared/LookupErrorMessage';
|
import LookupErrorMessage from './shared/LookupErrorMessage';
|
||||||
@@ -20,6 +21,7 @@ const QS_CONFIG = getQSConfig('project', {
|
|||||||
|
|
||||||
function ProjectLookup({
|
function ProjectLookup({
|
||||||
helperTextInvalid,
|
helperTextInvalid,
|
||||||
|
autocomplete,
|
||||||
i18n,
|
i18n,
|
||||||
isValid,
|
isValid,
|
||||||
onChange,
|
onChange,
|
||||||
@@ -29,25 +31,31 @@ function ProjectLookup({
|
|||||||
onBlur,
|
onBlur,
|
||||||
history,
|
history,
|
||||||
}) {
|
}) {
|
||||||
const [projects, setProjects] = useState([]);
|
const {
|
||||||
const [count, setCount] = useState(0);
|
result: { count, projects },
|
||||||
const [error, setError] = useState(null);
|
error,
|
||||||
|
request: fetchProjects,
|
||||||
|
} = useRequest(
|
||||||
|
useCallback(async () => {
|
||||||
|
const params = parseQueryString(QS_CONFIG, history.location.search);
|
||||||
|
const { data } = await ProjectsAPI.read(params);
|
||||||
|
if (data.count === 1 && autocomplete) {
|
||||||
|
autocomplete(data.results[0]);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
count: data.count,
|
||||||
|
projects: data.results,
|
||||||
|
};
|
||||||
|
}, [history.location.search, autocomplete]),
|
||||||
|
{
|
||||||
|
count: 0,
|
||||||
|
projects: [],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
fetchProjects();
|
||||||
const params = parseQueryString(QS_CONFIG, history.location.search);
|
}, [fetchProjects]);
|
||||||
try {
|
|
||||||
const { data } = await ProjectsAPI.read(params);
|
|
||||||
setProjects(data.results);
|
|
||||||
setCount(data.count);
|
|
||||||
if (data.count === 1) {
|
|
||||||
onChange(data.results[0]);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
setError(err);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}, [onChange, history.location]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormGroup
|
<FormGroup
|
||||||
@@ -124,22 +132,24 @@ function ProjectLookup({
|
|||||||
}
|
}
|
||||||
|
|
||||||
ProjectLookup.propTypes = {
|
ProjectLookup.propTypes = {
|
||||||
value: Project,
|
autocomplete: func,
|
||||||
helperTextInvalid: node,
|
helperTextInvalid: node,
|
||||||
isValid: bool,
|
isValid: bool,
|
||||||
onBlur: func,
|
onBlur: func,
|
||||||
onChange: func.isRequired,
|
onChange: func.isRequired,
|
||||||
required: bool,
|
required: bool,
|
||||||
tooltip: string,
|
tooltip: string,
|
||||||
|
value: Project,
|
||||||
};
|
};
|
||||||
|
|
||||||
ProjectLookup.defaultProps = {
|
ProjectLookup.defaultProps = {
|
||||||
|
autocomplete: () => {},
|
||||||
helperTextInvalid: '',
|
helperTextInvalid: '',
|
||||||
isValid: true,
|
isValid: true,
|
||||||
|
onBlur: () => {},
|
||||||
required: false,
|
required: false,
|
||||||
tooltip: '',
|
tooltip: '',
|
||||||
value: null,
|
value: null,
|
||||||
onBlur: () => {},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export { ProjectLookup as _ProjectLookup };
|
export { ProjectLookup as _ProjectLookup };
|
||||||
|
|||||||
@@ -15,12 +15,14 @@ describe('<ProjectLookup />', () => {
|
|||||||
count: 1,
|
count: 1,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const onChange = jest.fn();
|
const autocomplete = jest.fn();
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
mountWithContexts(<ProjectLookup onChange={onChange} />);
|
mountWithContexts(
|
||||||
|
<ProjectLookup autocomplete={autocomplete} onChange={() => {}} />
|
||||||
|
);
|
||||||
});
|
});
|
||||||
await sleep(0);
|
await sleep(0);
|
||||||
expect(onChange).toHaveBeenCalledWith({ id: 1 });
|
expect(autocomplete).toHaveBeenCalledWith({ id: 1 });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not auto-select project when multiple available', async () => {
|
test('should not auto-select project when multiple available', async () => {
|
||||||
@@ -30,11 +32,13 @@ describe('<ProjectLookup />', () => {
|
|||||||
count: 2,
|
count: 2,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const onChange = jest.fn();
|
const autocomplete = jest.fn();
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
mountWithContexts(<ProjectLookup onChange={onChange} />);
|
mountWithContexts(
|
||||||
|
<ProjectLookup autocomplete={autocomplete} onChange={() => {}} />
|
||||||
|
);
|
||||||
});
|
});
|
||||||
await sleep(0);
|
await sleep(0);
|
||||||
expect(onChange).not.toHaveBeenCalled();
|
expect(autocomplete).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ class Inventories extends Component {
|
|||||||
this.state = {
|
this.state = {
|
||||||
breadcrumbConfig: {
|
breadcrumbConfig: {
|
||||||
'/inventories': i18n._(t`Inventories`),
|
'/inventories': i18n._(t`Inventories`),
|
||||||
'/inventories/inventory/add': i18n._(t`Create New Inventory`),
|
'/inventories/inventory/add': i18n._(t`Create new inventory`),
|
||||||
'/inventories/smart_inventory/add': i18n._(
|
'/inventories/smart_inventory/add': i18n._(
|
||||||
t`Create New Smart Inventory`
|
t`Create new smart inventory`
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -43,42 +43,43 @@ class Inventories extends Component {
|
|||||||
|
|
||||||
const breadcrumbConfig = {
|
const breadcrumbConfig = {
|
||||||
'/inventories': i18n._(t`Inventories`),
|
'/inventories': i18n._(t`Inventories`),
|
||||||
'/inventories/inventory/add': i18n._(t`Create New Inventory`),
|
'/inventories/inventory/add': i18n._(t`Create new inventory`),
|
||||||
'/inventories/smart_inventory/add': i18n._(t`Create New Smart Inventory`),
|
'/inventories/smart_inventory/add': i18n._(t`Create new smart inventory`),
|
||||||
|
|
||||||
[inventoryPath]: `${inventory.name}`,
|
[inventoryPath]: `${inventory.name}`,
|
||||||
[`${inventoryPath}/access`]: i18n._(t`Access`),
|
[`${inventoryPath}/access`]: i18n._(t`Access`),
|
||||||
[`${inventoryPath}/completed_jobs`]: i18n._(t`Completed Jobs`),
|
[`${inventoryPath}/completed_jobs`]: i18n._(t`Completed jobs`),
|
||||||
[`${inventoryPath}/details`]: i18n._(t`Details`),
|
[`${inventoryPath}/details`]: i18n._(t`Details`),
|
||||||
[`${inventoryPath}/edit`]: i18n._(t`Edit Details`),
|
[`${inventoryPath}/edit`]: i18n._(t`Edit details`),
|
||||||
|
|
||||||
[inventoryHostsPath]: i18n._(t`Hosts`),
|
[inventoryHostsPath]: i18n._(t`Hosts`),
|
||||||
[`${inventoryHostsPath}/add`]: i18n._(t`Create New Host`),
|
[`${inventoryHostsPath}/add`]: i18n._(t`Create new host`),
|
||||||
[`${inventoryHostsPath}/${nested?.id}`]: `${nested?.name}`,
|
[`${inventoryHostsPath}/${nested?.id}`]: `${nested?.name}`,
|
||||||
[`${inventoryHostsPath}/${nested?.id}/edit`]: i18n._(t`Edit Details`),
|
[`${inventoryHostsPath}/${nested?.id}/edit`]: i18n._(t`Edit details`),
|
||||||
[`${inventoryHostsPath}/${nested?.id}/details`]: i18n._(t`Host Details`),
|
[`${inventoryHostsPath}/${nested?.id}/details`]: i18n._(t`Host Details`),
|
||||||
[`${inventoryHostsPath}/${nested?.id}/completed_jobs`]: i18n._(
|
[`${inventoryHostsPath}/${nested?.id}/completed_jobs`]: i18n._(
|
||||||
t`Completed Jobs`
|
t`Completed jobs`
|
||||||
),
|
),
|
||||||
[`${inventoryHostsPath}/${nested?.id}/facts`]: i18n._(t`Facts`),
|
[`${inventoryHostsPath}/${nested?.id}/facts`]: i18n._(t`Facts`),
|
||||||
[`${inventoryHostsPath}/${nested?.id}/groups`]: i18n._(t`Groups`),
|
[`${inventoryHostsPath}/${nested?.id}/groups`]: i18n._(t`Groups`),
|
||||||
|
|
||||||
[inventoryGroupsPath]: i18n._(t`Groups`),
|
[inventoryGroupsPath]: i18n._(t`Groups`),
|
||||||
[`${inventoryGroupsPath}/add`]: i18n._(t`Create New Group`),
|
[`${inventoryGroupsPath}/add`]: i18n._(t`Create new group`),
|
||||||
[`${inventoryGroupsPath}/${nested?.id}`]: `${nested?.name}`,
|
[`${inventoryGroupsPath}/${nested?.id}`]: `${nested?.name}`,
|
||||||
[`${inventoryGroupsPath}/${nested?.id}/edit`]: i18n._(t`Edit Details`),
|
[`${inventoryGroupsPath}/${nested?.id}/edit`]: i18n._(t`Edit details`),
|
||||||
[`${inventoryGroupsPath}/${nested?.id}/details`]: i18n._(
|
[`${inventoryGroupsPath}/${nested?.id}/details`]: i18n._(
|
||||||
t`Group Details`
|
t`Group details`
|
||||||
),
|
),
|
||||||
[`${inventoryGroupsPath}/${nested?.id}/nested_hosts`]: i18n._(t`Hosts`),
|
[`${inventoryGroupsPath}/${nested?.id}/nested_hosts`]: i18n._(t`Hosts`),
|
||||||
[`${inventoryGroupsPath}/${nested?.id}/nested_hosts/add`]: i18n._(
|
[`${inventoryGroupsPath}/${nested?.id}/nested_hosts/add`]: i18n._(
|
||||||
t`Create New Host`
|
t`Create new host`
|
||||||
),
|
),
|
||||||
|
|
||||||
[`${inventorySourcesPath}`]: i18n._(t`Sources`),
|
[`${inventorySourcesPath}`]: i18n._(t`Sources`),
|
||||||
[`${inventorySourcesPath}/add`]: i18n._(t`Create New Source`),
|
[`${inventorySourcesPath}/add`]: i18n._(t`Create new source`),
|
||||||
[`${inventorySourcesPath}/${nested?.id}`]: `${nested?.name}`,
|
[`${inventorySourcesPath}/${nested?.id}`]: `${nested?.name}`,
|
||||||
[`${inventorySourcesPath}/${nested?.id}/details`]: i18n._(t`Details`),
|
[`${inventorySourcesPath}/${nested?.id}/details`]: i18n._(t`Details`),
|
||||||
|
[`${inventorySourcesPath}/${nested?.id}/edit`]: i18n._(t`Edit details`),
|
||||||
};
|
};
|
||||||
this.setState({ breadcrumbConfig });
|
this.setState({ breadcrumbConfig });
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import ContentError from '../../../components/ContentError';
|
|||||||
import ContentLoading from '../../../components/ContentLoading';
|
import ContentLoading from '../../../components/ContentLoading';
|
||||||
import RoutedTabs from '../../../components/RoutedTabs';
|
import RoutedTabs from '../../../components/RoutedTabs';
|
||||||
import InventorySourceDetail from '../InventorySourceDetail';
|
import InventorySourceDetail from '../InventorySourceDetail';
|
||||||
|
import InventorySourceEdit from '../InventorySourceEdit';
|
||||||
|
|
||||||
function InventorySource({ i18n, inventory, setBreadcrumb }) {
|
function InventorySource({ i18n, inventory, setBreadcrumb }) {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@@ -38,7 +39,7 @@ function InventorySource({ i18n, inventory, setBreadcrumb }) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchSource();
|
fetchSource();
|
||||||
}, [fetchSource, match.params.sourceId]);
|
}, [fetchSource, location.pathname]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (inventory && source) {
|
if (inventory && source) {
|
||||||
@@ -104,6 +105,12 @@ function InventorySource({ i18n, inventory, setBreadcrumb }) {
|
|||||||
>
|
>
|
||||||
<InventorySourceDetail inventorySource={source} />
|
<InventorySourceDetail inventorySource={source} />
|
||||||
</Route>
|
</Route>
|
||||||
|
<Route
|
||||||
|
key="edit"
|
||||||
|
path="/inventories/inventory/:id/sources/:sourceId/edit"
|
||||||
|
>
|
||||||
|
<InventorySourceEdit source={source} inventory={inventory} />
|
||||||
|
</Route>
|
||||||
<Route key="not-found" path="*">
|
<Route key="not-found" path="*">
|
||||||
<ContentError isNotFound>
|
<ContentError isNotFound>
|
||||||
<Link to={`${match.url}/details`}>
|
<Link to={`${match.url}/details`}>
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { act } from 'react-dom/test-utils';
|
import { act } from 'react-dom/test-utils';
|
||||||
import { createMemoryHistory } from 'history';
|
import { createMemoryHistory } from 'history';
|
||||||
import { mountWithContexts } from '../../../../testUtils/enzymeHelpers';
|
import {
|
||||||
|
mountWithContexts,
|
||||||
|
waitForElement,
|
||||||
|
} from '../../../../testUtils/enzymeHelpers';
|
||||||
import InventorySourceAdd from './InventorySourceAdd';
|
import InventorySourceAdd from './InventorySourceAdd';
|
||||||
import { InventorySourcesAPI, ProjectsAPI } from '../../../api';
|
import { InventorySourcesAPI, ProjectsAPI } from '../../../api';
|
||||||
|
|
||||||
@@ -75,6 +78,7 @@ describe('<InventorySourceAdd />', () => {
|
|||||||
context: { config },
|
context: { config },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
expect(wrapper.find('FormGroup[label="Name"]')).toHaveLength(1);
|
expect(wrapper.find('FormGroup[label="Name"]')).toHaveLength(1);
|
||||||
expect(wrapper.find('FormGroup[label="Description"]')).toHaveLength(1);
|
expect(wrapper.find('FormGroup[label="Description"]')).toHaveLength(1);
|
||||||
expect(wrapper.find('FormGroup[label="Source"]')).toHaveLength(1);
|
expect(wrapper.find('FormGroup[label="Source"]')).toHaveLength(1);
|
||||||
|
|||||||
@@ -228,7 +228,7 @@ function InventorySourceDetail({ inventorySource, i18n }) {
|
|||||||
<Button
|
<Button
|
||||||
component={Link}
|
component={Link}
|
||||||
aria-label={i18n._(t`edit`)}
|
aria-label={i18n._(t`edit`)}
|
||||||
to={`/inventories/inventory/${inventory.id}/source/${id}/edit`}
|
to={`/inventories/inventory/${inventory.id}/sources/${id}/edit`}
|
||||||
>
|
>
|
||||||
{i18n._(t`Edit`)}
|
{i18n._(t`Edit`)}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ describe('InventorySourceDetail', () => {
|
|||||||
const editButton = wrapper.find('Button[aria-label="edit"]');
|
const editButton = wrapper.find('Button[aria-label="edit"]');
|
||||||
expect(editButton.text()).toEqual('Edit');
|
expect(editButton.text()).toEqual('Edit');
|
||||||
expect(editButton.prop('to')).toBe(
|
expect(editButton.prop('to')).toBe(
|
||||||
'/inventories/inventory/2/source/123/edit'
|
'/inventories/inventory/2/sources/123/edit'
|
||||||
);
|
);
|
||||||
expect(wrapper.find('DeleteButton')).toHaveLength(1);
|
expect(wrapper.find('DeleteButton')).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import React, { useCallback, useEffect } from 'react';
|
||||||
|
import { useHistory, useParams } from 'react-router-dom';
|
||||||
|
import { Card } from '@patternfly/react-core';
|
||||||
|
import { CardBody } from '../../../components/Card';
|
||||||
|
import useRequest from '../../../util/useRequest';
|
||||||
|
import { InventorySourcesAPI } from '../../../api';
|
||||||
|
import InventorySourceForm from '../shared/InventorySourceForm';
|
||||||
|
|
||||||
|
function InventorySourceEdit({ source }) {
|
||||||
|
const history = useHistory();
|
||||||
|
const { id } = useParams();
|
||||||
|
const detailsUrl = `/inventories/inventory/${id}/sources/${source.id}/details`;
|
||||||
|
|
||||||
|
const { error, request, result } = useRequest(
|
||||||
|
useCallback(
|
||||||
|
async values => {
|
||||||
|
const { data } = await InventorySourcesAPI.replace(source.id, values);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
[source.id]
|
||||||
|
),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (result) {
|
||||||
|
history.push(detailsUrl);
|
||||||
|
}
|
||||||
|
}, [result, detailsUrl, history]);
|
||||||
|
|
||||||
|
const handleSubmit = async form => {
|
||||||
|
const { credential, source_path, source_project, ...remainingForm } = form;
|
||||||
|
|
||||||
|
const sourcePath = {};
|
||||||
|
const sourceProject = {};
|
||||||
|
if (form.source === 'scm') {
|
||||||
|
sourcePath.source_path =
|
||||||
|
source_path === '/ (project root)' ? '' : source_path;
|
||||||
|
sourceProject.source_project = source_project.id;
|
||||||
|
}
|
||||||
|
await request({
|
||||||
|
credential: credential?.id || null,
|
||||||
|
inventory: id,
|
||||||
|
...sourcePath,
|
||||||
|
...sourceProject,
|
||||||
|
...remainingForm,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
history.push(detailsUrl);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardBody>
|
||||||
|
<InventorySourceForm
|
||||||
|
source={source}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
submitError={error}
|
||||||
|
/>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default InventorySourceEdit;
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { act } from 'react-dom/test-utils';
|
||||||
|
import { createMemoryHistory } from 'history';
|
||||||
|
import {
|
||||||
|
mountWithContexts,
|
||||||
|
waitForElement,
|
||||||
|
} from '../../../../testUtils/enzymeHelpers';
|
||||||
|
import InventorySourceEdit from './InventorySourceEdit';
|
||||||
|
import { CredentialsAPI, InventorySourcesAPI, ProjectsAPI } from '../../../api';
|
||||||
|
|
||||||
|
jest.mock('../../../api/models/Projects');
|
||||||
|
jest.mock('../../../api/models/Credentials');
|
||||||
|
jest.mock('../../../api/models/InventorySources');
|
||||||
|
jest.mock('react-router-dom', () => ({
|
||||||
|
...jest.requireActual('react-router-dom'),
|
||||||
|
useParams: () => ({
|
||||||
|
id: 1,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('<InventorySourceAdd />', () => {
|
||||||
|
let wrapper;
|
||||||
|
let history;
|
||||||
|
const mockInvSrc = {
|
||||||
|
id: 23,
|
||||||
|
description: 'bar',
|
||||||
|
inventory: 1,
|
||||||
|
name: 'foo',
|
||||||
|
overwrite: false,
|
||||||
|
overwrite_vars: false,
|
||||||
|
source: 'scm',
|
||||||
|
source_path: 'mock/file.sh',
|
||||||
|
source_project: { id: 999 },
|
||||||
|
source_vars: '---↵',
|
||||||
|
update_cache_timeout: 0,
|
||||||
|
update_on_launch: false,
|
||||||
|
update_on_project_update: false,
|
||||||
|
verbosity: 1,
|
||||||
|
};
|
||||||
|
InventorySourcesAPI.readOptions.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
actions: {
|
||||||
|
GET: {
|
||||||
|
source: {
|
||||||
|
choices: [
|
||||||
|
['file', 'File, Directory or Script'],
|
||||||
|
['scm', 'Sourced from a Project'],
|
||||||
|
['ec2', 'Amazon EC2'],
|
||||||
|
['gce', 'Google Compute Engine'],
|
||||||
|
['azure_rm', 'Microsoft Azure Resource Manager'],
|
||||||
|
['vmware', 'VMware vCenter'],
|
||||||
|
['satellite6', 'Red Hat Satellite 6'],
|
||||||
|
['cloudforms', 'Red Hat CloudForms'],
|
||||||
|
['openstack', 'OpenStack'],
|
||||||
|
['rhv', 'Red Hat Virtualization'],
|
||||||
|
['tower', 'Ansible Tower'],
|
||||||
|
['custom', 'Custom Script'],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
InventorySourcesAPI.replace.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
...mockInvSrc,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
ProjectsAPI.readInventories.mockResolvedValue({
|
||||||
|
data: [],
|
||||||
|
});
|
||||||
|
CredentialsAPI.read.mockResolvedValue({
|
||||||
|
data: { count: 0, results: [] },
|
||||||
|
});
|
||||||
|
ProjectsAPI.read.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
count: 2,
|
||||||
|
results: [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'mock proj one',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'mock proj two',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
history = createMemoryHistory();
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<InventorySourceEdit source={mockInvSrc} />, {
|
||||||
|
context: { router: { history } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handleSubmit should call api update', async () => {
|
||||||
|
expect(InventorySourcesAPI.replace).toHaveBeenCalledTimes(0);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('InventorySourceForm').invoke('onSubmit')(mockInvSrc);
|
||||||
|
});
|
||||||
|
expect(InventorySourcesAPI.replace).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should navigate to inventory source detail after successful submission', () => {
|
||||||
|
expect(wrapper.find('FormSubmitError').length).toBe(0);
|
||||||
|
expect(history.location.pathname).toEqual(
|
||||||
|
'/inventories/inventory/1/sources/23/details'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should navigate to inventory sources list when cancel is clicked', async () => {
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('button[aria-label="Cancel"]').invoke('onClick')();
|
||||||
|
});
|
||||||
|
expect(history.location.pathname).toEqual(
|
||||||
|
'/inventories/inventory/1/sources/23/details'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('unsuccessful form submission should show an error message', async () => {
|
||||||
|
const error = {
|
||||||
|
response: {
|
||||||
|
data: { detail: 'An error occurred' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
InventorySourcesAPI.replace.mockImplementation(() => Promise.reject(error));
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<InventorySourceEdit source={mockInvSrc} />);
|
||||||
|
});
|
||||||
|
expect(wrapper.find('FormSubmitError').length).toBe(0);
|
||||||
|
await act(async () => {
|
||||||
|
wrapper.find('InventorySourceForm').invoke('onSubmit')({});
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(wrapper.find('FormSubmitError').length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default } from './InventorySourceEdit';
|
||||||
@@ -133,23 +133,24 @@ const InventorySourceForm = ({
|
|||||||
i18n,
|
i18n,
|
||||||
onCancel,
|
onCancel,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
|
source,
|
||||||
submitError = null,
|
submitError = null,
|
||||||
}) => {
|
}) => {
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
credential: null,
|
credential: source?.summary_fields?.credential || null,
|
||||||
custom_virtualenv: '',
|
custom_virtualenv: source?.custom_virtualenv || '',
|
||||||
description: '',
|
description: source?.description || '',
|
||||||
name: '',
|
name: source?.name || '',
|
||||||
overwrite: false,
|
overwrite: source?.overwrite || false,
|
||||||
overwrite_vars: false,
|
overwrite_vars: source?.overwrite_vars || false,
|
||||||
source: '',
|
source: source?.source || '',
|
||||||
source_path: '',
|
source_path: source?.source_path === '' ? '/ (project root)' : '',
|
||||||
source_project: null,
|
source_project: source?.summary_fields?.source_project || null,
|
||||||
source_vars: '---\n',
|
source_vars: source?.source_vars || '---\n',
|
||||||
update_cache_timeout: 0,
|
update_cache_timeout: source?.update_cache_timeout || 0,
|
||||||
update_on_launch: false,
|
update_on_launch: source?.update_on_launch || false,
|
||||||
update_on_project_update: false,
|
update_on_project_update: source?.update_on_project_update || false,
|
||||||
verbosity: 1,
|
verbosity: source?.verbosity || 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -172,21 +173,21 @@ const InventorySourceForm = ({
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
}, []),
|
}, []),
|
||||||
[]
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchSourceOptions();
|
fetchSourceOptions();
|
||||||
}, [fetchSourceOptions]);
|
}, [fetchSourceOptions]);
|
||||||
|
|
||||||
if (isSourceOptionsLoading) {
|
|
||||||
return <ContentLoading />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sourceOptionsError) {
|
if (sourceOptionsError) {
|
||||||
return <ContentError error={sourceOptionsError} />;
|
return <ContentError error={sourceOptionsError} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!sourceOptions || isSourceOptionsLoading) {
|
||||||
|
return <ContentLoading />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback } from 'react';
|
import React, { useCallback, useEffect } from 'react';
|
||||||
import { useField } from 'formik';
|
import { useField } from 'formik';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
@@ -36,6 +36,12 @@ const SCMSubForm = ({ i18n }) => {
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (projectMeta.initialValue) {
|
||||||
|
fetchSourcePath(projectMeta.initialValue.id);
|
||||||
|
}
|
||||||
|
}, [fetchSourcePath, projectMeta.initialValue]);
|
||||||
|
|
||||||
const handleProjectUpdate = useCallback(
|
const handleProjectUpdate = useCallback(
|
||||||
value => {
|
value => {
|
||||||
sourcePathHelpers.setValue('');
|
sourcePathHelpers.setValue('');
|
||||||
@@ -45,6 +51,16 @@ const SCMSubForm = ({ i18n }) => {
|
|||||||
[] // eslint-disable-line react-hooks/exhaustive-deps
|
[] // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleProjectAutocomplete = useCallback(
|
||||||
|
val => {
|
||||||
|
projectHelpers.setValue(val);
|
||||||
|
if (!projectMeta.initialValue) {
|
||||||
|
fetchSourcePath(val.id);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[] // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CredentialLookup
|
<CredentialLookup
|
||||||
@@ -56,6 +72,7 @@ const SCMSubForm = ({ i18n }) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ProjectLookup
|
<ProjectLookup
|
||||||
|
autocomplete={handleProjectAutocomplete}
|
||||||
value={projectField.value}
|
value={projectField.value}
|
||||||
isValid={!projectMeta.touched || !projectMeta.error}
|
isValid={!projectMeta.touched || !projectMeta.error}
|
||||||
helperTextInvalid={projectMeta.error}
|
helperTextInvalid={projectMeta.error}
|
||||||
|
|||||||
@@ -6,9 +6,25 @@ import {
|
|||||||
waitForElement,
|
waitForElement,
|
||||||
} from '../../../../testUtils/enzymeHelpers';
|
} from '../../../../testUtils/enzymeHelpers';
|
||||||
import JobTemplateAdd from './JobTemplateAdd';
|
import JobTemplateAdd from './JobTemplateAdd';
|
||||||
import { JobTemplatesAPI, LabelsAPI } from '../../../api';
|
import {
|
||||||
|
CredentialsAPI,
|
||||||
|
CredentialTypesAPI,
|
||||||
|
JobTemplatesAPI,
|
||||||
|
LabelsAPI,
|
||||||
|
ProjectsAPI,
|
||||||
|
} from '../../../api';
|
||||||
|
|
||||||
jest.mock('../../../api');
|
jest.mock('../../../api');
|
||||||
|
CredentialsAPI.read.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
results: [],
|
||||||
|
count: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
CredentialTypesAPI.loadAllTypes.mockResolvedValue([]);
|
||||||
|
ProjectsAPI.readPlaybooks.mockResolvedValue({
|
||||||
|
data: [],
|
||||||
|
});
|
||||||
|
|
||||||
const jobTemplateData = {
|
const jobTemplateData = {
|
||||||
allow_callbacks: false,
|
allow_callbacks: false,
|
||||||
|
|||||||
@@ -6,7 +6,13 @@ import {
|
|||||||
mountWithContexts,
|
mountWithContexts,
|
||||||
waitForElement,
|
waitForElement,
|
||||||
} from '../../../../testUtils/enzymeHelpers';
|
} from '../../../../testUtils/enzymeHelpers';
|
||||||
import { JobTemplatesAPI, LabelsAPI, ProjectsAPI } from '../../../api';
|
import {
|
||||||
|
CredentialsAPI,
|
||||||
|
CredentialTypesAPI,
|
||||||
|
JobTemplatesAPI,
|
||||||
|
LabelsAPI,
|
||||||
|
ProjectsAPI,
|
||||||
|
} from '../../../api';
|
||||||
import JobTemplateEdit from './JobTemplateEdit';
|
import JobTemplateEdit from './JobTemplateEdit';
|
||||||
|
|
||||||
jest.mock('../../../api');
|
jest.mock('../../../api');
|
||||||
@@ -60,7 +66,7 @@ const mockJobTemplate = {
|
|||||||
{ id: 2, kind: 'ssh', name: 'Bar' },
|
{ id: 2, kind: 'ssh', name: 'Bar' },
|
||||||
],
|
],
|
||||||
project: {
|
project: {
|
||||||
id: 15,
|
id: 3,
|
||||||
name: 'Boo',
|
name: 'Boo',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -176,6 +182,13 @@ ProjectsAPI.readPlaybooks.mockResolvedValue({
|
|||||||
data: mockRelatedProjectPlaybooks,
|
data: mockRelatedProjectPlaybooks,
|
||||||
});
|
});
|
||||||
LabelsAPI.read.mockResolvedValue({ data: { results: [] } });
|
LabelsAPI.read.mockResolvedValue({ data: { results: [] } });
|
||||||
|
CredentialsAPI.read.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
results: [],
|
||||||
|
count: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
CredentialTypesAPI.loadAllTypes.mockResolvedValue([]);
|
||||||
|
|
||||||
describe('<JobTemplateEdit />', () => {
|
describe('<JobTemplateEdit />', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -251,7 +264,7 @@ describe('<JobTemplateEdit />', () => {
|
|||||||
|
|
||||||
const expected = {
|
const expected = {
|
||||||
...mockJobTemplate,
|
...mockJobTemplate,
|
||||||
project: mockJobTemplate.project.id,
|
project: mockJobTemplate.project,
|
||||||
...updatedTemplateData,
|
...updatedTemplateData,
|
||||||
};
|
};
|
||||||
delete expected.summary_fields;
|
delete expected.summary_fields;
|
||||||
|
|||||||
@@ -46,27 +46,25 @@ const { origin } = document.location;
|
|||||||
|
|
||||||
function JobTemplateForm({
|
function JobTemplateForm({
|
||||||
template,
|
template,
|
||||||
validateField,
|
|
||||||
handleCancel,
|
handleCancel,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
setFieldValue,
|
setFieldValue,
|
||||||
submitError,
|
submitError,
|
||||||
i18n,
|
i18n,
|
||||||
}) {
|
}) {
|
||||||
|
const { values: formikValues } = useFormikContext();
|
||||||
|
|
||||||
const [contentError, setContentError] = useState(false);
|
const [contentError, setContentError] = useState(false);
|
||||||
const [project, setProject] = useState(template?.summary_fields?.project);
|
|
||||||
const [inventory, setInventory] = useState(
|
const [inventory, setInventory] = useState(
|
||||||
template?.summary_fields?.inventory
|
template?.summary_fields?.inventory
|
||||||
);
|
);
|
||||||
const [allowCallbacks, setAllowCallbacks] = useState(
|
const [allowCallbacks, setAllowCallbacks] = useState(
|
||||||
Boolean(template?.host_config_key)
|
Boolean(template?.host_config_key)
|
||||||
);
|
);
|
||||||
|
|
||||||
const [enableWebhooks, setEnableWebhooks] = useState(
|
const [enableWebhooks, setEnableWebhooks] = useState(
|
||||||
Boolean(template.webhook_service)
|
Boolean(template.webhook_service)
|
||||||
);
|
);
|
||||||
|
|
||||||
const { values: formikValues } = useFormikContext();
|
|
||||||
const [jobTypeField, jobTypeMeta, jobTypeHelpers] = useField({
|
const [jobTypeField, jobTypeMeta, jobTypeHelpers] = useField({
|
||||||
name: 'job_type',
|
name: 'job_type',
|
||||||
validate: required(null, i18n),
|
validate: required(null, i18n),
|
||||||
@@ -74,16 +72,13 @@ function JobTemplateForm({
|
|||||||
const [, inventoryMeta, inventoryHelpers] = useField('inventory');
|
const [, inventoryMeta, inventoryHelpers] = useField('inventory');
|
||||||
const [projectField, projectMeta, projectHelpers] = useField({
|
const [projectField, projectMeta, projectHelpers] = useField({
|
||||||
name: 'project',
|
name: 'project',
|
||||||
validate: () => handleProjectValidation(),
|
validate: project => handleProjectValidation(project),
|
||||||
});
|
});
|
||||||
|
|
||||||
const [scmField, , scmHelpers] = useField('scm_branch');
|
const [scmField, , scmHelpers] = useField('scm_branch');
|
||||||
|
|
||||||
const [playbookField, playbookMeta, playbookHelpers] = useField({
|
const [playbookField, playbookMeta, playbookHelpers] = useField({
|
||||||
name: 'playbook',
|
name: 'playbook',
|
||||||
validate: required(i18n._(t`Select a value for this field`), i18n),
|
validate: required(i18n._(t`Select a value for this field`), i18n),
|
||||||
});
|
});
|
||||||
|
|
||||||
const [credentialField, , credentialHelpers] = useField('credentials');
|
const [credentialField, , credentialHelpers] = useField('credentials');
|
||||||
const [labelsField, , labelsHelpers] = useField('labels');
|
const [labelsField, , labelsHelpers] = useField('labels');
|
||||||
const [limitField, limitMeta] = useField('limit');
|
const [limitField, limitMeta] = useField('limit');
|
||||||
@@ -101,13 +96,10 @@ function JobTemplateForm({
|
|||||||
contentLoading: hasProjectLoading,
|
contentLoading: hasProjectLoading,
|
||||||
} = useRequest(
|
} = useRequest(
|
||||||
useCallback(async () => {
|
useCallback(async () => {
|
||||||
let projectData;
|
|
||||||
if (template?.project) {
|
if (template?.project) {
|
||||||
projectData = await ProjectsAPI.readDetail(template?.project);
|
await ProjectsAPI.readDetail(template?.project);
|
||||||
validateField('project');
|
|
||||||
setProject(projectData.data);
|
|
||||||
}
|
}
|
||||||
}, [template, validateField])
|
}, [template])
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -133,26 +125,28 @@ function JobTemplateForm({
|
|||||||
loadRelatedInstanceGroups();
|
loadRelatedInstanceGroups();
|
||||||
}, [loadRelatedInstanceGroups]);
|
}, [loadRelatedInstanceGroups]);
|
||||||
|
|
||||||
const handleProjectValidation = () => {
|
const handleProjectValidation = project => {
|
||||||
if (!project && projectMeta.touched) {
|
if (!project && projectMeta.touched) {
|
||||||
return i18n._(t`Select a value for this field`);
|
return i18n._(t`Select a value for this field`);
|
||||||
}
|
}
|
||||||
if (project && project.status === 'never updated') {
|
if (project?.value?.status === 'never updated') {
|
||||||
return i18n._(t`This project needs to be updated`);
|
return i18n._(t`This project needs to be updated`);
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleProjectUpdate = useCallback(
|
const handleProjectUpdate = useCallback(
|
||||||
newProject => {
|
value => {
|
||||||
if (project?.id !== newProject?.id) {
|
playbookHelpers.setValue(0);
|
||||||
// Clear the selected playbook value when a different project is selected or
|
|
||||||
// when the project is deselected.
|
|
||||||
playbookHelpers.setValue(0);
|
|
||||||
}
|
|
||||||
setProject(newProject);
|
|
||||||
projectHelpers.setValue(newProject);
|
|
||||||
scmHelpers.setValue('');
|
scmHelpers.setValue('');
|
||||||
|
projectHelpers.setValue(value);
|
||||||
|
},
|
||||||
|
[] // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleProjectAutocomplete = useCallback(
|
||||||
|
val => {
|
||||||
|
projectHelpers.setValue(val);
|
||||||
},
|
},
|
||||||
[] // eslint-disable-line react-hooks/exhaustive-deps
|
[] // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
);
|
);
|
||||||
@@ -189,8 +183,12 @@ function JobTemplateForm({
|
|||||||
return <ContentLoading />;
|
return <ContentLoading />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (instanceGroupError || projectContentError) {
|
if (contentError || instanceGroupError || projectContentError) {
|
||||||
return <ContentError error={contentError} />;
|
return (
|
||||||
|
<ContentError
|
||||||
|
error={contentError || instanceGroupError || projectContentError}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -261,18 +259,18 @@ function JobTemplateForm({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</FieldWithPrompt>
|
</FieldWithPrompt>
|
||||||
|
|
||||||
<ProjectLookup
|
<ProjectLookup
|
||||||
value={project}
|
value={projectField.value}
|
||||||
onBlur={() => projectHelpers.setTouched()}
|
onBlur={() => projectHelpers.setTouched()}
|
||||||
tooltip={i18n._(t`Select the project containing the playbook
|
tooltip={i18n._(t`Select the project containing the playbook
|
||||||
you want this job to execute.`)}
|
you want this job to execute.`)}
|
||||||
isValid={!projectMeta.touched || !projectMeta.error}
|
isValid={!projectMeta.touched || !projectMeta.error}
|
||||||
helperTextInvalid={projectMeta.error}
|
helperTextInvalid={projectMeta.error}
|
||||||
onChange={handleProjectUpdate}
|
onChange={handleProjectUpdate}
|
||||||
|
autocomplete={handleProjectAutocomplete}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
{project?.allow_override && (
|
{projectField.value?.allow_override && (
|
||||||
<FieldWithPrompt
|
<FieldWithPrompt
|
||||||
fieldId="template-scm-branch"
|
fieldId="template-scm-branch"
|
||||||
label={i18n._(t`Source Control Branch`)}
|
label={i18n._(t`Source Control Branch`)}
|
||||||
@@ -302,7 +300,7 @@ function JobTemplateForm({
|
|||||||
content={i18n._(t`Select the playbook to be executed by this job.`)}
|
content={i18n._(t`Select the playbook to be executed by this job.`)}
|
||||||
/>
|
/>
|
||||||
<PlaybookSelect
|
<PlaybookSelect
|
||||||
projectId={project?.id || projectField.value?.id}
|
projectId={projectField.value?.id}
|
||||||
isValid={!playbookMeta.touched || !playbookMeta.error}
|
isValid={!playbookMeta.touched || !playbookMeta.error}
|
||||||
field={playbookField}
|
field={playbookField}
|
||||||
onBlur={() => playbookHelpers.setTouched()}
|
onBlur={() => playbookHelpers.setTouched()}
|
||||||
@@ -615,6 +613,8 @@ const FormikApp = withFormik({
|
|||||||
} = template;
|
} = template;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
allow_callbacks: template.allow_callbacks || false,
|
||||||
|
allow_simultaneous: template.allow_simultaneous || false,
|
||||||
ask_credential_on_launch: template.ask_credential_on_launch || false,
|
ask_credential_on_launch: template.ask_credential_on_launch || false,
|
||||||
ask_diff_mode_on_launch: template.ask_diff_mode_on_launch || false,
|
ask_diff_mode_on_launch: template.ask_diff_mode_on_launch || false,
|
||||||
ask_inventory_on_launch: template.ask_inventory_on_launch || false,
|
ask_inventory_on_launch: template.ask_inventory_on_launch || false,
|
||||||
@@ -625,31 +625,29 @@ const FormikApp = withFormik({
|
|||||||
ask_tags_on_launch: template.ask_tags_on_launch || false,
|
ask_tags_on_launch: template.ask_tags_on_launch || false,
|
||||||
ask_variables_on_launch: template.ask_variables_on_launch || false,
|
ask_variables_on_launch: template.ask_variables_on_launch || false,
|
||||||
ask_verbosity_on_launch: template.ask_verbosity_on_launch || false,
|
ask_verbosity_on_launch: template.ask_verbosity_on_launch || false,
|
||||||
name: template.name || '',
|
|
||||||
description: template.description || '',
|
|
||||||
job_type: template.job_type || 'run',
|
|
||||||
inventory: template.inventory || null,
|
|
||||||
project: template.project || null,
|
|
||||||
scm_branch: template.scm_branch || '',
|
|
||||||
playbook: template.playbook || '',
|
|
||||||
labels: summary_fields.labels.results || [],
|
|
||||||
forks: template.forks || 0,
|
|
||||||
limit: template.limit || '',
|
|
||||||
verbosity: template.verbosity || '0',
|
|
||||||
job_slice_count: template.job_slice_count || 1,
|
|
||||||
timeout: template.timeout || 0,
|
|
||||||
diff_mode: template.diff_mode || false,
|
|
||||||
job_tags: template.job_tags || '',
|
|
||||||
skip_tags: template.skip_tags || '',
|
|
||||||
become_enabled: template.become_enabled || false,
|
become_enabled: template.become_enabled || false,
|
||||||
allow_callbacks: template.allow_callbacks || false,
|
credentials: summary_fields.credentials || [],
|
||||||
allow_simultaneous: template.allow_simultaneous || false,
|
description: template.description || '',
|
||||||
use_fact_cache: template.use_fact_cache || false,
|
diff_mode: template.diff_mode || false,
|
||||||
|
extra_vars: template.extra_vars || '---\n',
|
||||||
|
forks: template.forks || 0,
|
||||||
host_config_key: template.host_config_key || '',
|
host_config_key: template.host_config_key || '',
|
||||||
initialInstanceGroups: [],
|
initialInstanceGroups: [],
|
||||||
instanceGroups: [],
|
instanceGroups: [],
|
||||||
credentials: summary_fields.credentials || [],
|
inventory: template.inventory || null,
|
||||||
extra_vars: template.extra_vars || '---\n',
|
job_slice_count: template.job_slice_count || 1,
|
||||||
|
job_tags: template.job_tags || '',
|
||||||
|
job_type: template.job_type || 'run',
|
||||||
|
labels: summary_fields.labels.results || [],
|
||||||
|
limit: template.limit || '',
|
||||||
|
name: template.name || '',
|
||||||
|
playbook: template.playbook || '',
|
||||||
|
project: summary_fields?.project || null,
|
||||||
|
scm_branch: template.scm_branch || '',
|
||||||
|
skip_tags: template.skip_tags || '',
|
||||||
|
timeout: template.timeout || 0,
|
||||||
|
use_fact_cache: template.use_fact_cache || false,
|
||||||
|
verbosity: template.verbosity || '0',
|
||||||
webhook_service: template.webhook_service || '',
|
webhook_service: template.webhook_service || '',
|
||||||
webhook_url: template?.related?.webhook_receiver
|
webhook_url: template?.related?.webhook_receiver
|
||||||
? `${origin}${template.related.webhook_receiver}`
|
? `${origin}${template.related.webhook_receiver}`
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
JobTemplatesAPI,
|
JobTemplatesAPI,
|
||||||
ProjectsAPI,
|
ProjectsAPI,
|
||||||
CredentialsAPI,
|
CredentialsAPI,
|
||||||
|
CredentialTypesAPI,
|
||||||
} from '../../../api';
|
} from '../../../api';
|
||||||
|
|
||||||
jest.mock('../../../api');
|
jest.mock('../../../api');
|
||||||
@@ -99,6 +100,7 @@ describe('<JobTemplateForm />', () => {
|
|||||||
LabelsAPI.read.mockReturnValue({
|
LabelsAPI.read.mockReturnValue({
|
||||||
data: mockData.summary_fields.labels,
|
data: mockData.summary_fields.labels,
|
||||||
});
|
});
|
||||||
|
CredentialTypesAPI.loadAllTypes.mockResolvedValue([]);
|
||||||
CredentialsAPI.read.mockReturnValue({
|
CredentialsAPI.read.mockReturnValue({
|
||||||
data: { results: mockCredentials },
|
data: { results: mockCredentials },
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user