mirror of
https://github.com/ansible/awx.git
synced 2026-05-08 18:07:36 -02:30
Pre-fill project for job template from query params
Pre-fill project when creating JT from Project -> Job Templates List
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
|||||||
oneOfType,
|
oneOfType,
|
||||||
shape,
|
shape,
|
||||||
node,
|
node,
|
||||||
|
object,
|
||||||
} from 'prop-types';
|
} from 'prop-types';
|
||||||
import { withRouter } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
import { useField } from 'formik';
|
import { useField } from 'formik';
|
||||||
@@ -222,7 +223,7 @@ Lookup.propTypes = {
|
|||||||
header: string,
|
header: string,
|
||||||
modalDescription: oneOfType([string, node]),
|
modalDescription: oneOfType([string, node]),
|
||||||
onChange: func.isRequired,
|
onChange: func.isRequired,
|
||||||
value: oneOfType([Item, arrayOf(Item)]),
|
value: oneOfType([Item, arrayOf(Item), object]),
|
||||||
multiple: bool,
|
multiple: bool,
|
||||||
required: bool,
|
required: bool,
|
||||||
onBlur: func,
|
onBlur: func,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useCallback, useEffect } from 'react';
|
import React, { useCallback, useEffect } from 'react';
|
||||||
import { node, string, func, bool } from 'prop-types';
|
import { node, string, func, bool, object, oneOfType } from 'prop-types';
|
||||||
import { withRouter } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { FormGroup } from '@patternfly/react-core';
|
import { FormGroup } from '@patternfly/react-core';
|
||||||
@@ -184,7 +184,7 @@ ProjectLookup.propTypes = {
|
|||||||
onChange: func.isRequired,
|
onChange: func.isRequired,
|
||||||
required: bool,
|
required: bool,
|
||||||
tooltip: string,
|
tooltip: string,
|
||||||
value: Project,
|
value: oneOfType([Project, object]),
|
||||||
isOverrideDisabled: bool,
|
isOverrideDisabled: bool,
|
||||||
validate: func,
|
validate: func,
|
||||||
fieldName: string,
|
fieldName: string,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useCallback, useEffect } from 'react';
|
import React, { useCallback, useEffect } from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useParams, useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import { t, Plural } from '@lingui/macro';
|
import { t, Plural } from '@lingui/macro';
|
||||||
import { Card } from '@patternfly/react-core';
|
import { Card } from '@patternfly/react-core';
|
||||||
@@ -14,7 +14,12 @@ import PaginatedTable, {
|
|||||||
ToolbarDeleteButton,
|
ToolbarDeleteButton,
|
||||||
getSearchableKeys,
|
getSearchableKeys,
|
||||||
} from 'components/PaginatedTable';
|
} from 'components/PaginatedTable';
|
||||||
import { getQSConfig, parseQueryString, mergeParams } from 'util/qs';
|
import {
|
||||||
|
getQSConfig,
|
||||||
|
parseQueryString,
|
||||||
|
mergeParams,
|
||||||
|
encodeQueryString,
|
||||||
|
} from 'util/qs';
|
||||||
import useWsTemplates from 'hooks/useWsTemplates';
|
import useWsTemplates from 'hooks/useWsTemplates';
|
||||||
import useSelected from 'hooks/useSelected';
|
import useSelected from 'hooks/useSelected';
|
||||||
import useExpanded from 'hooks/useExpanded';
|
import useExpanded from 'hooks/useExpanded';
|
||||||
@@ -29,7 +34,8 @@ const QS_CONFIG = getQSConfig('template', {
|
|||||||
order_by: 'name',
|
order_by: 'name',
|
||||||
});
|
});
|
||||||
|
|
||||||
function RelatedTemplateList({ searchParams }) {
|
function RelatedTemplateList({ searchParams, projectName = null }) {
|
||||||
|
const { id: projectId } = useParams();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { addToast, Toast, toastProps } = useToast();
|
const { addToast, Toast, toastProps } = useToast();
|
||||||
|
|
||||||
@@ -122,9 +128,18 @@ function RelatedTemplateList({ searchParams }) {
|
|||||||
const canAddJT =
|
const canAddJT =
|
||||||
actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
|
actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
|
||||||
|
|
||||||
const addButton = (
|
let linkTo = '';
|
||||||
<ToolbarAddButton key="add" linkTo="/templates/job_template/add/" />
|
|
||||||
);
|
if (projectName) {
|
||||||
|
const qs = encodeQueryString({
|
||||||
|
project_id: projectId,
|
||||||
|
project_name: projectName,
|
||||||
|
});
|
||||||
|
linkTo = `/templates/job_template/add/?${qs}`;
|
||||||
|
} else {
|
||||||
|
linkTo = '/templates/job_template/add';
|
||||||
|
}
|
||||||
|
const addButton = <ToolbarAddButton key="add" linkTo={linkTo} />;
|
||||||
|
|
||||||
const deleteDetailsRequests = relatedResourceDeleteRequests.template(
|
const deleteDetailsRequests = relatedResourceDeleteRequests.template(
|
||||||
selected[0]
|
selected[0]
|
||||||
|
|||||||
@@ -174,7 +174,12 @@ function Project({ setBreadcrumb }) {
|
|||||||
</Route>
|
</Route>
|
||||||
)}
|
)}
|
||||||
<Route path="/projects/:id/job_templates">
|
<Route path="/projects/:id/job_templates">
|
||||||
<RelatedTemplateList searchParams={{ project__id: project.id }} />
|
<RelatedTemplateList
|
||||||
|
searchParams={{
|
||||||
|
project__id: project.id,
|
||||||
|
}}
|
||||||
|
projectName={project.name}
|
||||||
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
{project?.scm_type && project.scm_type !== '' && (
|
{project?.scm_type && project.scm_type !== '' && (
|
||||||
<Route path="/projects/:id/schedules">
|
<Route path="/projects/:id/schedules">
|
||||||
|
|||||||
@@ -9,6 +9,32 @@ function JobTemplateAdd() {
|
|||||||
const [formSubmitError, setFormSubmitError] = useState(null);
|
const [formSubmitError, setFormSubmitError] = useState(null);
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
|
const projectParams = {
|
||||||
|
project_id: null,
|
||||||
|
project_name: null,
|
||||||
|
};
|
||||||
|
history.location.search
|
||||||
|
.replace(/^\?/, '')
|
||||||
|
.split('&')
|
||||||
|
.map((s) => s.split('='))
|
||||||
|
.forEach(([key, val]) => {
|
||||||
|
if (!(key in projectParams)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
projectParams[key] = decodeURIComponent(val);
|
||||||
|
});
|
||||||
|
|
||||||
|
let projectValues = null;
|
||||||
|
|
||||||
|
if (
|
||||||
|
Object.values(projectParams).filter((item) => item !== null).length === 2
|
||||||
|
) {
|
||||||
|
projectValues = {
|
||||||
|
id: projectParams.project_id,
|
||||||
|
name: projectParams.project_name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const handleSubmit = async (values) => {
|
const handleSubmit = async (values) => {
|
||||||
const {
|
const {
|
||||||
labels,
|
labels,
|
||||||
@@ -35,7 +61,11 @@ function JobTemplateAdd() {
|
|||||||
execution_environment: values.execution_environment?.id,
|
execution_environment: values.execution_environment?.id,
|
||||||
});
|
});
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
submitLabels(id, values.project.summary_fields.organization.id, labels),
|
submitLabels(
|
||||||
|
id,
|
||||||
|
values.project.summary_fields?.organization.id,
|
||||||
|
labels
|
||||||
|
),
|
||||||
submitInstanceGroups(id, instanceGroups),
|
submitInstanceGroups(id, instanceGroups),
|
||||||
submitCredentials(id, credentials),
|
submitCredentials(id, credentials),
|
||||||
]);
|
]);
|
||||||
@@ -92,6 +122,7 @@ function JobTemplateAdd() {
|
|||||||
handleCancel={handleCancel}
|
handleCancel={handleCancel}
|
||||||
handleSubmit={handleSubmit}
|
handleSubmit={handleSubmit}
|
||||||
submitError={formSubmitError}
|
submitError={formSubmitError}
|
||||||
|
projectValues={projectValues}
|
||||||
isOverrideDisabledLookup
|
isOverrideDisabledLookup
|
||||||
/>
|
/>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
|
|||||||
@@ -257,4 +257,33 @@ describe('<JobTemplateAdd />', () => {
|
|||||||
});
|
});
|
||||||
expect(history.location.pathname).toEqual('/templates');
|
expect(history.location.pathname).toEqual('/templates');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should parse and pre-fill project field from query params', async () => {
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: [
|
||||||
|
'/templates/job_template/add/add?project_id=6&project_name=Demo%20Project',
|
||||||
|
],
|
||||||
|
});
|
||||||
|
let wrapper;
|
||||||
|
await act(async () => {
|
||||||
|
wrapper = mountWithContexts(<JobTemplateAdd />, {
|
||||||
|
context: { router: { history } },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await waitForElement(wrapper, 'EmptyStateBody', (el) => el.length === 0);
|
||||||
|
expect(wrapper.find('input#project').prop('value')).toEqual('Demo Project');
|
||||||
|
expect(ProjectsAPI.readPlaybooks).toBeCalledWith('6');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not call ProjectsAPI.readPlaybooks if there is no project', async () => {
|
||||||
|
const history = createMemoryHistory({
|
||||||
|
initialEntries: ['/templates/job_template/add'],
|
||||||
|
});
|
||||||
|
await act(async () =>
|
||||||
|
mountWithContexts(<JobTemplateAdd />, {
|
||||||
|
context: { router: history },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
expect(ProjectsAPI.readPlaybooks).not.toBeCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -392,60 +392,4 @@ describe('<JobTemplateEdit />', () => {
|
|||||||
'/templates/job_template/1/details'
|
'/templates/job_template/1/details'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
test('should not call ProjectsAPI.readPlaybooks if there is no project', async () => {
|
|
||||||
const history = createMemoryHistory({});
|
|
||||||
const noProjectTemplate = {
|
|
||||||
id: 1,
|
|
||||||
name: 'Foo',
|
|
||||||
description: 'Bar',
|
|
||||||
job_type: 'run',
|
|
||||||
inventory: 2,
|
|
||||||
playbook: 'Baz',
|
|
||||||
type: 'job_template',
|
|
||||||
forks: 0,
|
|
||||||
limit: '',
|
|
||||||
verbosity: '0',
|
|
||||||
job_slice_count: 1,
|
|
||||||
timeout: 0,
|
|
||||||
job_tags: '',
|
|
||||||
skip_tags: '',
|
|
||||||
diff_mode: false,
|
|
||||||
allow_callbacks: false,
|
|
||||||
allow_simultaneous: false,
|
|
||||||
use_fact_cache: false,
|
|
||||||
host_config_key: '',
|
|
||||||
summary_fields: {
|
|
||||||
user_capabilities: {
|
|
||||||
edit: true,
|
|
||||||
},
|
|
||||||
labels: {
|
|
||||||
results: [
|
|
||||||
{ name: 'Sushi', id: 1 },
|
|
||||||
{ name: 'Major', id: 2 },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
inventory: {
|
|
||||||
id: 2,
|
|
||||||
name: 'Demo Inventory',
|
|
||||||
organization_id: 1,
|
|
||||||
},
|
|
||||||
credentials: [
|
|
||||||
{ id: 1, kind: 'cloud', name: 'Foo' },
|
|
||||||
{ id: 2, kind: 'ssh', name: 'Bar' },
|
|
||||||
],
|
|
||||||
webhook_credential: {
|
|
||||||
id: 7,
|
|
||||||
name: 'webhook credential',
|
|
||||||
kind: 'github_token',
|
|
||||||
credential_type_id: 12,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
await act(async () =>
|
|
||||||
mountWithContexts(<JobTemplateEdit template={noProjectTemplate} />, {
|
|
||||||
context: { router: { history } },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
expect(ProjectsAPI.readPlaybooks).not.toBeCalled();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ function JobTemplateForm({
|
|||||||
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 isMounted = useIsMounted();
|
const isMounted = useIsMounted();
|
||||||
const brandName = useBrandName();
|
const brandName = useBrandName();
|
||||||
@@ -646,7 +646,7 @@ JobTemplateForm.defaultProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const FormikApp = withFormik({
|
const FormikApp = withFormik({
|
||||||
mapPropsToValues({ template = {} }) {
|
mapPropsToValues({ projectValues = {}, template = {} }) {
|
||||||
const {
|
const {
|
||||||
summary_fields = {
|
summary_fields = {
|
||||||
labels: { results: [] },
|
labels: { results: [] },
|
||||||
@@ -684,7 +684,7 @@ const FormikApp = withFormik({
|
|||||||
limit: template.limit || '',
|
limit: template.limit || '',
|
||||||
name: template.name || '',
|
name: template.name || '',
|
||||||
playbook: template.playbook || '',
|
playbook: template.playbook || '',
|
||||||
project: summary_fields?.project || null,
|
project: summary_fields?.project || projectValues || 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,
|
||||||
|
|||||||
Reference in New Issue
Block a user