mirror of
https://github.com/ansible/awx.git
synced 2026-05-23 08:37:48 -02:30
Prepopulates job template form with related resource
This commit is contained in:
@@ -34,8 +34,14 @@ const QS_CONFIG = getQSConfig('template', {
|
|||||||
order_by: 'name',
|
order_by: 'name',
|
||||||
});
|
});
|
||||||
|
|
||||||
function RelatedTemplateList({ searchParams, projectName = null }) {
|
const resources = {
|
||||||
const { id: projectId } = useParams();
|
projects: 'project',
|
||||||
|
inventories: 'inventory',
|
||||||
|
credentials: 'credentials',
|
||||||
|
};
|
||||||
|
|
||||||
|
function RelatedTemplateList({ searchParams, resourceName = null }) {
|
||||||
|
const { id } = useParams();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { addToast, Toast, toastProps } = useToast();
|
const { addToast, Toast, toastProps } = useToast();
|
||||||
|
|
||||||
@@ -129,12 +135,19 @@ function RelatedTemplateList({ searchParams, projectName = null }) {
|
|||||||
actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
|
actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
|
||||||
|
|
||||||
let linkTo = '';
|
let linkTo = '';
|
||||||
|
if (resourceName) {
|
||||||
if (projectName) {
|
const queryString = {
|
||||||
const qs = encodeQueryString({
|
resource_id: id,
|
||||||
project_id: projectId,
|
resource_name: resourceName,
|
||||||
project_name: projectName,
|
resource_type: resources[location.pathname.split('/')[1]],
|
||||||
});
|
resource_kind: null,
|
||||||
|
};
|
||||||
|
if (Array.isArray(resourceName)) {
|
||||||
|
const [name, kind] = resourceName;
|
||||||
|
queryString.resource_name = name;
|
||||||
|
queryString.resource_kind = kind;
|
||||||
|
}
|
||||||
|
const qs = encodeQueryString(queryString);
|
||||||
linkTo = `/templates/job_template/add/?${qs}`;
|
linkTo = `/templates/job_template/add/?${qs}`;
|
||||||
} else {
|
} else {
|
||||||
linkTo = '/templates/job_template/add';
|
linkTo = '/templates/job_template/add';
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
/* eslint-disable import/prefer-default-export */
|
||||||
@@ -22,6 +22,16 @@ import { CredentialsAPI } from 'api';
|
|||||||
import CredentialDetail from './CredentialDetail';
|
import CredentialDetail from './CredentialDetail';
|
||||||
import CredentialEdit from './CredentialEdit';
|
import CredentialEdit from './CredentialEdit';
|
||||||
|
|
||||||
|
const jobTemplateCredentialTypes = [
|
||||||
|
'machine',
|
||||||
|
'cloud',
|
||||||
|
'net',
|
||||||
|
'ssh',
|
||||||
|
'vault',
|
||||||
|
'kubernetes',
|
||||||
|
'cryptography',
|
||||||
|
];
|
||||||
|
|
||||||
function Credential({ setBreadcrumb }) {
|
function Credential({ setBreadcrumb }) {
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
|
|
||||||
@@ -75,13 +85,14 @@ function Credential({ setBreadcrumb }) {
|
|||||||
link: `/credentials/${id}/access`,
|
link: `/credentials/${id}/access`,
|
||||||
id: 1,
|
id: 1,
|
||||||
},
|
},
|
||||||
{
|
];
|
||||||
|
if (jobTemplateCredentialTypes.includes(credential?.kind)) {
|
||||||
|
tabsArray.push({
|
||||||
name: t`Job Templates`,
|
name: t`Job Templates`,
|
||||||
link: `/credentials/${id}/job_templates`,
|
link: `/credentials/${id}/job_templates`,
|
||||||
id: 2,
|
id: 2,
|
||||||
},
|
});
|
||||||
];
|
}
|
||||||
|
|
||||||
let showCardHeader = true;
|
let showCardHeader = true;
|
||||||
|
|
||||||
if (pathname.endsWith('edit') || pathname.endsWith('add')) {
|
if (pathname.endsWith('edit') || pathname.endsWith('add')) {
|
||||||
@@ -133,6 +144,7 @@ function Credential({ setBreadcrumb }) {
|
|||||||
<Route key="job_templates" path="/credentials/:id/job_templates">
|
<Route key="job_templates" path="/credentials/:id/job_templates">
|
||||||
<RelatedTemplateList
|
<RelatedTemplateList
|
||||||
searchParams={{ credentials__id: credential.id }}
|
searchParams={{ credentials__id: credential.id }}
|
||||||
|
resourceName={[credential.name, credential.kind]}
|
||||||
/>
|
/>
|
||||||
</Route>,
|
</Route>,
|
||||||
<Route key="not-found" path="*">
|
<Route key="not-found" path="*">
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import {
|
|||||||
mountWithContexts,
|
mountWithContexts,
|
||||||
waitForElement,
|
waitForElement,
|
||||||
} from '../../../testUtils/enzymeHelpers';
|
} from '../../../testUtils/enzymeHelpers';
|
||||||
import mockCredential from './shared/data.scmCredential.json';
|
import mockMachineCredential from './shared/data.machineCredential.json';
|
||||||
|
import mockSCMCredential from './shared/data.scmCredential.json';
|
||||||
import Credential from './Credential';
|
import Credential from './Credential';
|
||||||
|
|
||||||
jest.mock('../../api');
|
jest.mock('../../api');
|
||||||
@@ -21,13 +22,10 @@ jest.mock('react-router-dom', () => ({
|
|||||||
describe('<Credential />', () => {
|
describe('<Credential />', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
|
||||||
beforeEach(() => {
|
test('initially renders user-based machine credential successfully', async () => {
|
||||||
CredentialsAPI.readDetail.mockResolvedValueOnce({
|
CredentialsAPI.readDetail.mockResolvedValueOnce({
|
||||||
data: mockCredential,
|
data: mockMachineCredential,
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
test('initially renders user-based credential successfully', async () => {
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(<Credential setBreadcrumb={() => {}} />);
|
wrapper = mountWithContexts(<Credential setBreadcrumb={() => {}} />);
|
||||||
});
|
});
|
||||||
@@ -36,6 +34,18 @@ describe('<Credential />', () => {
|
|||||||
expect(wrapper.find('RoutedTabs li').length).toBe(4);
|
expect(wrapper.find('RoutedTabs li').length).toBe(4);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('initially renders user-based SCM credential successfully', async () => {
|
||||||
|
CredentialsAPI.readDetail.mockResolvedValueOnce({
|
||||||
|
data: mockSCMCredential,
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<Credential setBreadcrumb={() => {}} />);
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
expect(wrapper.find('Credential').length).toBe(1);
|
||||||
|
expect(wrapper.find('RoutedTabs li').length).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
test('should render expected tabs', async () => {
|
test('should render expected tabs', async () => {
|
||||||
const expectedTabs = [
|
const expectedTabs = [
|
||||||
'Back to Credentials',
|
'Back to Credentials',
|
||||||
|
|||||||
@@ -181,6 +181,7 @@ function Inventory({ setBreadcrumb }) {
|
|||||||
>
|
>
|
||||||
<RelatedTemplateList
|
<RelatedTemplateList
|
||||||
searchParams={{ inventory__id: inventory.id }}
|
searchParams={{ inventory__id: inventory.id }}
|
||||||
|
resourceName={inventory.name}
|
||||||
/>
|
/>
|
||||||
</Route>,
|
</Route>,
|
||||||
<Route path="*" key="not-found">
|
<Route path="*" key="not-found">
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ function Project({ setBreadcrumb }) {
|
|||||||
searchParams={{
|
searchParams={{
|
||||||
project__id: project.id,
|
project__id: project.id,
|
||||||
}}
|
}}
|
||||||
projectName={project.name}
|
resourceName={project.name}
|
||||||
/>
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
{project?.scm_type && project.scm_type !== '' && (
|
{project?.scm_type && project.scm_type !== '' && (
|
||||||
|
|||||||
@@ -9,29 +9,31 @@ function JobTemplateAdd() {
|
|||||||
const [formSubmitError, setFormSubmitError] = useState(null);
|
const [formSubmitError, setFormSubmitError] = useState(null);
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
const projectParams = {
|
const resourceParams = {
|
||||||
project_id: null,
|
resource_id: null,
|
||||||
project_name: null,
|
resource_name: null,
|
||||||
|
resource_type: null,
|
||||||
|
resource_kind: null,
|
||||||
};
|
};
|
||||||
history.location.search
|
history.location.search
|
||||||
.replace(/^\?/, '')
|
.replace(/^\?/, '')
|
||||||
.split('&')
|
.split('&')
|
||||||
.map((s) => s.split('='))
|
.map((s) => s.split('='))
|
||||||
.forEach(([key, val]) => {
|
.forEach(([key, val]) => {
|
||||||
if (!(key in projectParams)) {
|
if (!(key in resourceParams)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
projectParams[key] = decodeURIComponent(val);
|
resourceParams[key] = decodeURIComponent(val);
|
||||||
});
|
});
|
||||||
|
|
||||||
let projectValues = null;
|
let resourceValues = null;
|
||||||
|
|
||||||
if (
|
if (history.location.search.includes('resource_id' && 'resource_name')) {
|
||||||
Object.values(projectParams).filter((item) => item !== null).length === 2
|
resourceValues = {
|
||||||
) {
|
id: resourceParams.resource_id,
|
||||||
projectValues = {
|
name: resourceParams.resource_name,
|
||||||
id: projectParams.project_id,
|
type: resourceParams.resource_type,
|
||||||
name: projectParams.project_name,
|
kind: resourceParams.resource_kind, // refers to credential kind
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +124,7 @@ function JobTemplateAdd() {
|
|||||||
handleCancel={handleCancel}
|
handleCancel={handleCancel}
|
||||||
handleSubmit={handleSubmit}
|
handleSubmit={handleSubmit}
|
||||||
submitError={formSubmitError}
|
submitError={formSubmitError}
|
||||||
projectValues={projectValues}
|
resourceValues={resourceValues}
|
||||||
isOverrideDisabledLookup
|
isOverrideDisabledLookup
|
||||||
/>
|
/>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
|
|||||||
@@ -274,9 +274,14 @@ describe('<JobTemplateAdd />', () => {
|
|||||||
test('should parse and pre-fill project field from query params', async () => {
|
test('should parse and pre-fill project field from query params', async () => {
|
||||||
const history = createMemoryHistory({
|
const history = createMemoryHistory({
|
||||||
initialEntries: [
|
initialEntries: [
|
||||||
'/templates/job_template/add/add?project_id=6&project_name=Demo%20Project',
|
'/templates/job_template/add?resource_id=6&resource_name=Demo%20Project&resource_type=project',
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
ProjectsAPI.read.mockResolvedValueOnce({
|
||||||
|
count: 1,
|
||||||
|
results: [{ name: 'foo', id: 1, allow_override: true, organization: 1 }],
|
||||||
|
});
|
||||||
|
ProjectsAPI.readOptions.mockResolvedValueOnce({});
|
||||||
let wrapper;
|
let wrapper;
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
wrapper = mountWithContexts(<JobTemplateAdd />, {
|
wrapper = mountWithContexts(<JobTemplateAdd />, {
|
||||||
@@ -284,8 +289,9 @@ describe('<JobTemplateAdd />', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
await waitForElement(wrapper, 'EmptyStateBody', (el) => el.length === 0);
|
await waitForElement(wrapper, 'EmptyStateBody', (el) => el.length === 0);
|
||||||
|
|
||||||
expect(wrapper.find('input#project').prop('value')).toEqual('Demo Project');
|
expect(wrapper.find('input#project').prop('value')).toEqual('Demo Project');
|
||||||
expect(ProjectsAPI.readPlaybooks).toBeCalledWith('6');
|
expect(ProjectsAPI.readPlaybooks).toBeCalledWith(6);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not call ProjectsAPI.readPlaybooks if there is no project', async () => {
|
test('should not call ProjectsAPI.readPlaybooks if there is no project', async () => {
|
||||||
|
|||||||
@@ -690,7 +690,7 @@ JobTemplateForm.defaultProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const FormikApp = withFormik({
|
const FormikApp = withFormik({
|
||||||
mapPropsToValues({ projectValues = {}, template = {} }) {
|
mapPropsToValues({ resourceValues = null, template = {} }) {
|
||||||
const {
|
const {
|
||||||
summary_fields = {
|
summary_fields = {
|
||||||
labels: { results: [] },
|
labels: { results: [] },
|
||||||
@@ -698,7 +698,7 @@ const FormikApp = withFormik({
|
|||||||
},
|
},
|
||||||
} = template;
|
} = template;
|
||||||
|
|
||||||
return {
|
const initialValues = {
|
||||||
allow_callbacks: template.allow_callbacks || false,
|
allow_callbacks: template.allow_callbacks || false,
|
||||||
allow_simultaneous: template.allow_simultaneous || 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,
|
||||||
@@ -739,7 +739,7 @@ const FormikApp = withFormik({
|
|||||||
playbook: template.playbook || '',
|
playbook: template.playbook || '',
|
||||||
prevent_instance_group_fallback:
|
prevent_instance_group_fallback:
|
||||||
template.prevent_instance_group_fallback || false,
|
template.prevent_instance_group_fallback || false,
|
||||||
project: summary_fields?.project || projectValues || null,
|
project: summary_fields?.project || null,
|
||||||
scm_branch: template.scm_branch || '',
|
scm_branch: template.scm_branch || '',
|
||||||
skip_tags: template.skip_tags || '',
|
skip_tags: template.skip_tags || '',
|
||||||
timeout: template.timeout || 0,
|
timeout: template.timeout || 0,
|
||||||
@@ -756,6 +756,24 @@ const FormikApp = withFormik({
|
|||||||
execution_environment:
|
execution_environment:
|
||||||
template.summary_fields?.execution_environment || null,
|
template.summary_fields?.execution_environment || null,
|
||||||
};
|
};
|
||||||
|
if (resourceValues !== null) {
|
||||||
|
if (resourceValues.type === 'credentials') {
|
||||||
|
initialValues[resourceValues.type] = [
|
||||||
|
{
|
||||||
|
id: parseInt(resourceValues.id, 10),
|
||||||
|
name: resourceValues.name,
|
||||||
|
kind: resourceValues.kind,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
initialValues[resourceValues.type] = {
|
||||||
|
id: parseInt(resourceValues.id, 10),
|
||||||
|
name: resourceValues.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return initialValues;
|
||||||
},
|
},
|
||||||
handleSubmit: async (values, { props, setErrors }) => {
|
handleSubmit: async (values, { props, setErrors }) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user