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:
nixocio 2022-05-27 16:15:17 -04:00
parent 39b8fd433b
commit 8095adb945
8 changed files with 95 additions and 70 deletions

View File

@ -8,6 +8,7 @@ import {
oneOfType,
shape,
node,
object,
} from 'prop-types';
import { withRouter } from 'react-router-dom';
import { useField } from 'formik';
@ -222,7 +223,7 @@ Lookup.propTypes = {
header: string,
modalDescription: oneOfType([string, node]),
onChange: func.isRequired,
value: oneOfType([Item, arrayOf(Item)]),
value: oneOfType([Item, arrayOf(Item), object]),
multiple: bool,
required: bool,
onBlur: func,

View File

@ -1,5 +1,5 @@
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 { t } from '@lingui/macro';
import { FormGroup } from '@patternfly/react-core';
@ -184,7 +184,7 @@ ProjectLookup.propTypes = {
onChange: func.isRequired,
required: bool,
tooltip: string,
value: Project,
value: oneOfType([Project, object]),
isOverrideDisabled: bool,
validate: func,
fieldName: string,

View File

@ -1,5 +1,5 @@
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 { Card } from '@patternfly/react-core';
@ -14,7 +14,12 @@ import PaginatedTable, {
ToolbarDeleteButton,
getSearchableKeys,
} from 'components/PaginatedTable';
import { getQSConfig, parseQueryString, mergeParams } from 'util/qs';
import {
getQSConfig,
parseQueryString,
mergeParams,
encodeQueryString,
} from 'util/qs';
import useWsTemplates from 'hooks/useWsTemplates';
import useSelected from 'hooks/useSelected';
import useExpanded from 'hooks/useExpanded';
@ -29,7 +34,8 @@ const QS_CONFIG = getQSConfig('template', {
order_by: 'name',
});
function RelatedTemplateList({ searchParams }) {
function RelatedTemplateList({ searchParams, projectName = null }) {
const { id: projectId } = useParams();
const location = useLocation();
const { addToast, Toast, toastProps } = useToast();
@ -122,9 +128,18 @@ function RelatedTemplateList({ searchParams }) {
const canAddJT =
actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
const addButton = (
<ToolbarAddButton key="add" linkTo="/templates/job_template/add/" />
);
let linkTo = '';
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(
selected[0]

View File

@ -174,7 +174,12 @@ function Project({ setBreadcrumb }) {
</Route>
)}
<Route path="/projects/:id/job_templates">
<RelatedTemplateList searchParams={{ project__id: project.id }} />
<RelatedTemplateList
searchParams={{
project__id: project.id,
}}
projectName={project.name}
/>
</Route>
{project?.scm_type && project.scm_type !== '' && (
<Route path="/projects/:id/schedules">

View File

@ -9,6 +9,32 @@ function JobTemplateAdd() {
const [formSubmitError, setFormSubmitError] = useState(null);
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 {
labels,
@ -35,7 +61,11 @@ function JobTemplateAdd() {
execution_environment: values.execution_environment?.id,
});
await Promise.all([
submitLabels(id, values.project.summary_fields.organization.id, labels),
submitLabels(
id,
values.project.summary_fields?.organization.id,
labels
),
submitInstanceGroups(id, instanceGroups),
submitCredentials(id, credentials),
]);
@ -92,6 +122,7 @@ function JobTemplateAdd() {
handleCancel={handleCancel}
handleSubmit={handleSubmit}
submitError={formSubmitError}
projectValues={projectValues}
isOverrideDisabledLookup
/>
</CardBody>

View File

@ -257,4 +257,33 @@ describe('<JobTemplateAdd />', () => {
});
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();
});
});

View File

@ -392,60 +392,4 @@ describe('<JobTemplateEdit />', () => {
'/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();
});
});

View File

@ -64,7 +64,7 @@ function JobTemplateForm({
Boolean(template?.host_config_key)
);
const [enableWebhooks, setEnableWebhooks] = useState(
Boolean(template.webhook_service)
Boolean(template?.webhook_service)
);
const isMounted = useIsMounted();
const brandName = useBrandName();
@ -646,7 +646,7 @@ JobTemplateForm.defaultProps = {
};
const FormikApp = withFormik({
mapPropsToValues({ template = {} }) {
mapPropsToValues({ projectValues = {}, template = {} }) {
const {
summary_fields = {
labels: { results: [] },
@ -684,7 +684,7 @@ const FormikApp = withFormik({
limit: template.limit || '',
name: template.name || '',
playbook: template.playbook || '',
project: summary_fields?.project || null,
project: summary_fields?.project || projectValues || null,
scm_branch: template.scm_branch || '',
skip_tags: template.skip_tags || '',
timeout: template.timeout || 0,