Prepopulates job template form with related resource

This commit is contained in:
Alex Corey 2022-12-01 11:24:14 -05:00
parent 11fbfc2063
commit 51ef1e808d
9 changed files with 100 additions and 37 deletions

View File

@ -34,8 +34,14 @@ const QS_CONFIG = getQSConfig('template', {
order_by: 'name',
});
function RelatedTemplateList({ searchParams, projectName = null }) {
const { id: projectId } = useParams();
const resources = {
projects: 'project',
inventories: 'inventory',
credentials: 'credentials',
};
function RelatedTemplateList({ searchParams, resourceName = null }) {
const { id } = useParams();
const location = useLocation();
const { addToast, Toast, toastProps } = useToast();
@ -129,12 +135,19 @@ function RelatedTemplateList({ searchParams, projectName = null }) {
actions && Object.prototype.hasOwnProperty.call(actions, 'POST');
let linkTo = '';
if (projectName) {
const qs = encodeQueryString({
project_id: projectId,
project_name: projectName,
});
if (resourceName) {
const queryString = {
resource_id: id,
resource_name: resourceName,
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}`;
} else {
linkTo = '/templates/job_template/add';

View File

@ -0,0 +1 @@
/* eslint-disable import/prefer-default-export */

View File

@ -22,6 +22,16 @@ import { CredentialsAPI } from 'api';
import CredentialDetail from './CredentialDetail';
import CredentialEdit from './CredentialEdit';
const jobTemplateCredentialTypes = [
'machine',
'cloud',
'net',
'ssh',
'vault',
'kubernetes',
'cryptography',
];
function Credential({ setBreadcrumb }) {
const { pathname } = useLocation();
@ -75,13 +85,14 @@ function Credential({ setBreadcrumb }) {
link: `/credentials/${id}/access`,
id: 1,
},
{
];
if (jobTemplateCredentialTypes.includes(credential?.kind)) {
tabsArray.push({
name: t`Job Templates`,
link: `/credentials/${id}/job_templates`,
id: 2,
},
];
});
}
let showCardHeader = true;
if (pathname.endsWith('edit') || pathname.endsWith('add')) {
@ -133,6 +144,7 @@ function Credential({ setBreadcrumb }) {
<Route key="job_templates" path="/credentials/:id/job_templates">
<RelatedTemplateList
searchParams={{ credentials__id: credential.id }}
resourceName={[credential.name, credential.kind]}
/>
</Route>,
<Route key="not-found" path="*">

View File

@ -6,7 +6,8 @@ import {
mountWithContexts,
waitForElement,
} 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';
jest.mock('../../api');
@ -21,13 +22,10 @@ jest.mock('react-router-dom', () => ({
describe('<Credential />', () => {
let wrapper;
beforeEach(() => {
test('initially renders user-based machine credential successfully', async () => {
CredentialsAPI.readDetail.mockResolvedValueOnce({
data: mockCredential,
data: mockMachineCredential,
});
});
test('initially renders user-based credential successfully', async () => {
await act(async () => {
wrapper = mountWithContexts(<Credential setBreadcrumb={() => {}} />);
});
@ -36,6 +34,18 @@ describe('<Credential />', () => {
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 () => {
const expectedTabs = [
'Back to Credentials',

View File

@ -181,6 +181,7 @@ function Inventory({ setBreadcrumb }) {
>
<RelatedTemplateList
searchParams={{ inventory__id: inventory.id }}
resourceName={inventory.name}
/>
</Route>,
<Route path="*" key="not-found">

View File

@ -179,7 +179,7 @@ function Project({ setBreadcrumb }) {
searchParams={{
project__id: project.id,
}}
projectName={project.name}
resourceName={project.name}
/>
</Route>
{project?.scm_type && project.scm_type !== '' && (

View File

@ -9,29 +9,31 @@ function JobTemplateAdd() {
const [formSubmitError, setFormSubmitError] = useState(null);
const history = useHistory();
const projectParams = {
project_id: null,
project_name: null,
const resourceParams = {
resource_id: null,
resource_name: null,
resource_type: null,
resource_kind: null,
};
history.location.search
.replace(/^\?/, '')
.split('&')
.map((s) => s.split('='))
.forEach(([key, val]) => {
if (!(key in projectParams)) {
if (!(key in resourceParams)) {
return;
}
projectParams[key] = decodeURIComponent(val);
resourceParams[key] = decodeURIComponent(val);
});
let projectValues = null;
let resourceValues = null;
if (
Object.values(projectParams).filter((item) => item !== null).length === 2
) {
projectValues = {
id: projectParams.project_id,
name: projectParams.project_name,
if (history.location.search.includes('resource_id' && 'resource_name')) {
resourceValues = {
id: resourceParams.resource_id,
name: resourceParams.resource_name,
type: resourceParams.resource_type,
kind: resourceParams.resource_kind, // refers to credential kind
};
}
@ -122,7 +124,7 @@ function JobTemplateAdd() {
handleCancel={handleCancel}
handleSubmit={handleSubmit}
submitError={formSubmitError}
projectValues={projectValues}
resourceValues={resourceValues}
isOverrideDisabledLookup
/>
</CardBody>

View File

@ -274,9 +274,14 @@ describe('<JobTemplateAdd />', () => {
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',
'/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;
await act(async () => {
wrapper = mountWithContexts(<JobTemplateAdd />, {
@ -284,8 +289,9 @@ describe('<JobTemplateAdd />', () => {
});
});
await waitForElement(wrapper, 'EmptyStateBody', (el) => el.length === 0);
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 () => {

View File

@ -690,7 +690,7 @@ JobTemplateForm.defaultProps = {
};
const FormikApp = withFormik({
mapPropsToValues({ projectValues = {}, template = {} }) {
mapPropsToValues({ resourceValues = null, template = {} }) {
const {
summary_fields = {
labels: { results: [] },
@ -698,7 +698,7 @@ const FormikApp = withFormik({
},
} = template;
return {
const initialValues = {
allow_callbacks: template.allow_callbacks || false,
allow_simultaneous: template.allow_simultaneous || false,
ask_credential_on_launch: template.ask_credential_on_launch || false,
@ -739,7 +739,7 @@ const FormikApp = withFormik({
playbook: template.playbook || '',
prevent_instance_group_fallback:
template.prevent_instance_group_fallback || false,
project: summary_fields?.project || projectValues || null,
project: summary_fields?.project || null,
scm_branch: template.scm_branch || '',
skip_tags: template.skip_tags || '',
timeout: template.timeout || 0,
@ -756,6 +756,24 @@ const FormikApp = withFormik({
execution_environment:
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 }) => {
try {