mirror of
https://github.com/ansible/awx.git
synced 2026-01-14 03:10:42 -03:30
Merge pull request #5127 from marshmalien/project-add-form
Project add form Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
commit
8cb32045f0
@ -10,7 +10,16 @@ const QuestionCircleIcon = styled(PFQuestionCircleIcon)`
|
||||
`;
|
||||
|
||||
function FormField(props) {
|
||||
const { id, name, label, tooltip, validate, isRequired, ...rest } = props;
|
||||
const {
|
||||
id,
|
||||
name,
|
||||
label,
|
||||
tooltip,
|
||||
tooltipMaxWidth,
|
||||
validate,
|
||||
isRequired,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Field
|
||||
@ -29,7 +38,11 @@ function FormField(props) {
|
||||
label={label}
|
||||
>
|
||||
{tooltip && (
|
||||
<Tooltip position="right" content={tooltip}>
|
||||
<Tooltip
|
||||
content={tooltip}
|
||||
maxWidth={tooltipMaxWidth}
|
||||
position="right"
|
||||
>
|
||||
<QuestionCircleIcon />
|
||||
</Tooltip>
|
||||
)}
|
||||
@ -58,6 +71,7 @@ FormField.propTypes = {
|
||||
validate: PropTypes.func,
|
||||
isRequired: PropTypes.bool,
|
||||
tooltip: PropTypes.node,
|
||||
tooltipMaxWidth: PropTypes.string,
|
||||
};
|
||||
|
||||
FormField.defaultProps = {
|
||||
@ -65,6 +79,7 @@ FormField.defaultProps = {
|
||||
validate: () => {},
|
||||
isRequired: false,
|
||||
tooltip: null,
|
||||
tooltipMaxWidth: '',
|
||||
};
|
||||
|
||||
export default FormField;
|
||||
|
||||
@ -6,6 +6,6 @@ const Row = styled.div`
|
||||
grid-gap: 20px;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
`;
|
||||
export default function FormRow({ children }) {
|
||||
return <Row>{children}</Row>;
|
||||
export default function FormRow({ children, className }) {
|
||||
return <Row className={className}>{children}</Row>;
|
||||
}
|
||||
|
||||
68
awx/ui_next/src/components/Lookup/CredentialLookup.jsx
Normal file
68
awx/ui_next/src/components/Lookup/CredentialLookup.jsx
Normal file
@ -0,0 +1,68 @@
|
||||
import React from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { bool, func, number, string, oneOfType } from 'prop-types';
|
||||
import { CredentialsAPI } from '@api';
|
||||
import { Credential } from '@types';
|
||||
import { mergeParams } from '@util/qs';
|
||||
import { FormGroup } from '@patternfly/react-core';
|
||||
import Lookup from '@components/Lookup';
|
||||
|
||||
function CredentialLookup({
|
||||
helperTextInvalid,
|
||||
label,
|
||||
isValid,
|
||||
onBlur,
|
||||
onChange,
|
||||
required,
|
||||
credentialTypeId,
|
||||
value,
|
||||
}) {
|
||||
const getCredentials = async params =>
|
||||
CredentialsAPI.read(
|
||||
mergeParams(params, { credential_type: credentialTypeId })
|
||||
);
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
fieldId="credential"
|
||||
isRequired={required}
|
||||
isValid={isValid}
|
||||
label={label}
|
||||
helperTextInvalid={helperTextInvalid}
|
||||
>
|
||||
<Lookup
|
||||
id="credential"
|
||||
lookupHeader={label}
|
||||
name="credential"
|
||||
value={value}
|
||||
onBlur={onBlur}
|
||||
onLookupSave={onChange}
|
||||
getItems={getCredentials}
|
||||
required={required}
|
||||
sortedColumnKey="name"
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
}
|
||||
|
||||
CredentialLookup.propTypes = {
|
||||
credentialTypeId: oneOfType([number, string]).isRequired,
|
||||
helperTextInvalid: string,
|
||||
isValid: bool,
|
||||
label: string.isRequired,
|
||||
onBlur: func,
|
||||
onChange: func.isRequired,
|
||||
required: bool,
|
||||
value: Credential,
|
||||
};
|
||||
|
||||
CredentialLookup.defaultProps = {
|
||||
helperTextInvalid: '',
|
||||
isValid: true,
|
||||
onBlur: () => {},
|
||||
required: false,
|
||||
value: null,
|
||||
};
|
||||
|
||||
export { CredentialLookup as _CredentialLookup };
|
||||
export default withI18n()(CredentialLookup);
|
||||
41
awx/ui_next/src/components/Lookup/CredentialLookup.test.jsx
Normal file
41
awx/ui_next/src/components/Lookup/CredentialLookup.test.jsx
Normal file
@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||
import CredentialLookup, { _CredentialLookup } from './CredentialLookup';
|
||||
import { CredentialsAPI } from '@api';
|
||||
|
||||
jest.mock('@api');
|
||||
|
||||
describe('CredentialLookup', () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = mountWithContexts(
|
||||
<CredentialLookup credentialTypeId={1} label="Foo" onChange={() => {}} />
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('initially renders successfully', () => {
|
||||
expect(wrapper.find('CredentialLookup')).toHaveLength(1);
|
||||
});
|
||||
test('should fetch credentials', () => {
|
||||
expect(CredentialsAPI.read).toHaveBeenCalledTimes(1);
|
||||
expect(CredentialsAPI.read).toHaveBeenCalledWith({
|
||||
credential_type: 1,
|
||||
order_by: 'name',
|
||||
page: 1,
|
||||
page_size: 5,
|
||||
});
|
||||
});
|
||||
test('should display label', () => {
|
||||
const title = wrapper.find('FormGroup .pf-c-form__label-text');
|
||||
expect(title.text()).toEqual('Foo');
|
||||
});
|
||||
test('should define default value for function props', () => {
|
||||
expect(_CredentialLookup.defaultProps.onBlur).toBeInstanceOf(Function);
|
||||
expect(_CredentialLookup.defaultProps.onBlur).not.toThrow();
|
||||
});
|
||||
});
|
||||
@ -101,7 +101,7 @@ class MultiCredentialsLookup extends React.Component {
|
||||
const { selectedCredentialType, credentialTypes } = this.state;
|
||||
const { tooltip, i18n, credentials } = this.props;
|
||||
return (
|
||||
<FormGroup label={i18n._(t`Credentials`)} fieldId="org-credentials">
|
||||
<FormGroup label={i18n._(t`Credentials`)} fieldId="multiCredential">
|
||||
{tooltip && (
|
||||
<Tooltip position="right" content={tooltip}>
|
||||
<QuestionCircleIcon />
|
||||
@ -114,7 +114,7 @@ class MultiCredentialsLookup extends React.Component {
|
||||
selectedCategory={selectedCredentialType}
|
||||
onToggleItem={this.toggleCredentialSelection}
|
||||
onloadCategories={this.loadCredentialTypes}
|
||||
id="org-credentials"
|
||||
id="multiCredential"
|
||||
lookupHeader={i18n._(t`Credentials`)}
|
||||
name="credentials"
|
||||
value={credentials}
|
||||
|
||||
62
awx/ui_next/src/components/Lookup/OrganizationLookup.jsx
Normal file
62
awx/ui_next/src/components/Lookup/OrganizationLookup.jsx
Normal file
@ -0,0 +1,62 @@
|
||||
import React from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { string, func, bool } from 'prop-types';
|
||||
import { OrganizationsAPI } from '@api';
|
||||
import { Organization } from '@types';
|
||||
import { FormGroup } from '@patternfly/react-core';
|
||||
import Lookup from '@components/Lookup';
|
||||
|
||||
const getOrganizations = async params => OrganizationsAPI.read(params);
|
||||
|
||||
function OrganizationLookup({
|
||||
helperTextInvalid,
|
||||
i18n,
|
||||
isValid,
|
||||
onBlur,
|
||||
onChange,
|
||||
required,
|
||||
value,
|
||||
}) {
|
||||
return (
|
||||
<FormGroup
|
||||
fieldId="organization"
|
||||
helperTextInvalid={helperTextInvalid}
|
||||
isRequired={required}
|
||||
isValid={isValid}
|
||||
label={i18n._(t`Organization`)}
|
||||
>
|
||||
<Lookup
|
||||
id="organization"
|
||||
lookupHeader={i18n._(t`Organization`)}
|
||||
name="organization"
|
||||
value={value}
|
||||
onBlur={onBlur}
|
||||
onLookupSave={onChange}
|
||||
getItems={getOrganizations}
|
||||
required={required}
|
||||
sortedColumnKey="name"
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
}
|
||||
|
||||
OrganizationLookup.propTypes = {
|
||||
helperTextInvalid: string,
|
||||
isValid: bool,
|
||||
onBlur: func,
|
||||
onChange: func.isRequired,
|
||||
required: bool,
|
||||
value: Organization,
|
||||
};
|
||||
|
||||
OrganizationLookup.defaultProps = {
|
||||
helperTextInvalid: '',
|
||||
isValid: true,
|
||||
onBlur: () => {},
|
||||
required: false,
|
||||
value: null,
|
||||
};
|
||||
|
||||
export default withI18n()(OrganizationLookup);
|
||||
export { OrganizationLookup as _OrganizationLookup };
|
||||
@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import { mountWithContexts } from '@testUtils/enzymeHelpers';
|
||||
import OrganizationLookup, { _OrganizationLookup } from './OrganizationLookup';
|
||||
import { OrganizationsAPI } from '@api';
|
||||
|
||||
jest.mock('@api');
|
||||
|
||||
describe('OrganizationLookup', () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = mountWithContexts(<OrganizationLookup onChange={() => {}} />);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('initially renders successfully', () => {
|
||||
expect(wrapper).toHaveLength(1);
|
||||
});
|
||||
test('should fetch organizations', () => {
|
||||
expect(OrganizationsAPI.read).toHaveBeenCalledTimes(1);
|
||||
expect(OrganizationsAPI.read).toHaveBeenCalledWith({
|
||||
order_by: 'name',
|
||||
page: 1,
|
||||
page_size: 5,
|
||||
});
|
||||
});
|
||||
test('should display "Organization" label', () => {
|
||||
const title = wrapper.find('FormGroup .pf-c-form__label-text');
|
||||
expect(title.text()).toEqual('Organization');
|
||||
});
|
||||
test('should define default value for function props', () => {
|
||||
expect(_OrganizationLookup.defaultProps.onBlur).toBeInstanceOf(Function);
|
||||
expect(_OrganizationLookup.defaultProps.onBlur).not.toThrow();
|
||||
});
|
||||
});
|
||||
@ -1,10 +1,65 @@
|
||||
import React, { Component } from 'react';
|
||||
import { PageSection } from '@patternfly/react-core';
|
||||
import React, { useState } from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
Card as _Card,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
PageSection,
|
||||
Tooltip,
|
||||
} from '@patternfly/react-core';
|
||||
import CardCloseButton from '@components/CardCloseButton';
|
||||
import ProjectForm from '../shared/ProjectForm';
|
||||
import { ProjectsAPI } from '@api';
|
||||
|
||||
class ProjectAdd extends Component {
|
||||
render() {
|
||||
return <PageSection>Coming soon :)</PageSection>;
|
||||
}
|
||||
const Card = styled(_Card)`
|
||||
--pf-c-card--child--PaddingLeft: 0;
|
||||
--pf-c-card--child--PaddingRight: 0;
|
||||
`;
|
||||
|
||||
function ProjectAdd({ history, i18n }) {
|
||||
const [formSubmitError, setFormSubmitError] = useState(null);
|
||||
|
||||
const handleSubmit = async values => {
|
||||
setFormSubmitError(null);
|
||||
try {
|
||||
const {
|
||||
data: { id },
|
||||
} = await ProjectsAPI.create(values);
|
||||
history.push(`/projects/${id}/details`);
|
||||
} catch (error) {
|
||||
setFormSubmitError(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
history.push(`/projects`);
|
||||
};
|
||||
|
||||
return (
|
||||
<PageSection>
|
||||
<Card>
|
||||
<CardHeader css="text-align: right">
|
||||
<Tooltip content={i18n._(t`Close`)} position="top">
|
||||
<CardCloseButton onClick={handleCancel} />
|
||||
</Tooltip>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<ProjectForm
|
||||
handleCancel={handleCancel}
|
||||
handleSubmit={handleSubmit}
|
||||
/>
|
||||
</CardBody>
|
||||
{formSubmitError ? (
|
||||
<div className="formSubmitError">formSubmitError</div>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</Card>
|
||||
</PageSection>
|
||||
);
|
||||
}
|
||||
|
||||
export default ProjectAdd;
|
||||
export default withI18n()(withRouter(ProjectAdd));
|
||||
|
||||
161
awx/ui_next/src/screens/Project/ProjectAdd/ProjectAdd.test.jsx
Normal file
161
awx/ui_next/src/screens/Project/ProjectAdd/ProjectAdd.test.jsx
Normal file
@ -0,0 +1,161 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
|
||||
import ProjectAdd from './ProjectAdd';
|
||||
import { ProjectsAPI, CredentialTypesAPI } from '@api';
|
||||
|
||||
jest.mock('@api');
|
||||
|
||||
describe('<ProjectAdd />', () => {
|
||||
let wrapper;
|
||||
const projectData = {
|
||||
name: 'foo',
|
||||
description: 'bar',
|
||||
scm_type: 'git',
|
||||
scm_url: 'https://foo.bar',
|
||||
scm_clean: true,
|
||||
credential: 100,
|
||||
organization: 2,
|
||||
scm_update_on_launch: true,
|
||||
scm_update_cache_timeout: 3,
|
||||
allow_override: false,
|
||||
custom_virtualenv: '/venv/custom-env',
|
||||
};
|
||||
|
||||
const projectOptionsResolve = {
|
||||
data: {
|
||||
actions: {
|
||||
GET: {
|
||||
scm_type: {
|
||||
choices: [
|
||||
['', 'Manual'],
|
||||
['git', 'Git'],
|
||||
['hg', 'Mercurial'],
|
||||
['svn', 'Subversion'],
|
||||
['insights', 'Red Hat Insights'],
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const scmCredentialResolve = {
|
||||
data: {
|
||||
results: [
|
||||
{
|
||||
id: 4,
|
||||
name: 'Source Control',
|
||||
kind: 'scm',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const insightsCredentialResolve = {
|
||||
data: {
|
||||
results: [
|
||||
{
|
||||
id: 5,
|
||||
name: 'Insights',
|
||||
kind: 'insights',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
await ProjectsAPI.readOptions.mockImplementation(
|
||||
() => projectOptionsResolve
|
||||
);
|
||||
await CredentialTypesAPI.read.mockImplementationOnce(
|
||||
() => scmCredentialResolve
|
||||
);
|
||||
await CredentialTypesAPI.read.mockImplementationOnce(
|
||||
() => insightsCredentialResolve
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('initially renders successfully', async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<ProjectAdd />);
|
||||
});
|
||||
expect(wrapper.length).toBe(1);
|
||||
});
|
||||
|
||||
test('handleSubmit should post to the api', async () => {
|
||||
ProjectsAPI.create.mockResolvedValueOnce({
|
||||
data: { ...projectData },
|
||||
});
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<ProjectAdd />);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
const formik = wrapper.find('Formik').instance();
|
||||
const changeState = new Promise(resolve => {
|
||||
formik.setState(
|
||||
{
|
||||
values: {
|
||||
...projectData,
|
||||
},
|
||||
},
|
||||
() => resolve()
|
||||
);
|
||||
});
|
||||
await changeState;
|
||||
wrapper.find('form').simulate('submit');
|
||||
});
|
||||
|
||||
test('handleSubmit should throw an error', async () => {
|
||||
ProjectsAPI.create.mockImplementation(() => Promise.reject(new Error()));
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<ProjectAdd />);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
const formik = wrapper.find('Formik').instance();
|
||||
const changeState = new Promise(resolve => {
|
||||
formik.setState(
|
||||
{
|
||||
values: {
|
||||
...projectData,
|
||||
},
|
||||
},
|
||||
() => resolve()
|
||||
);
|
||||
});
|
||||
await changeState;
|
||||
await act(async () => {
|
||||
wrapper.find('form').simulate('submit');
|
||||
});
|
||||
wrapper.update();
|
||||
expect(wrapper.find('ProjectAdd .formSubmitError').length).toBe(1);
|
||||
});
|
||||
|
||||
test('CardHeader close button should navigate to projects list', async () => {
|
||||
const history = createMemoryHistory();
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<ProjectAdd />, {
|
||||
context: { router: { history } },
|
||||
}).find('ProjectAdd CardHeader');
|
||||
});
|
||||
wrapper.find('CardCloseButton').simulate('click');
|
||||
expect(history.location.pathname).toEqual('/projects');
|
||||
});
|
||||
|
||||
test('CardBody cancel button should navigate to projects list', async () => {
|
||||
const history = createMemoryHistory();
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(<ProjectAdd />, {
|
||||
context: { router: { history } },
|
||||
});
|
||||
});
|
||||
await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0);
|
||||
wrapper.find('ProjectAdd button[aria-label="Cancel"]').simulate('click');
|
||||
expect(history.location.pathname).toEqual('/projects');
|
||||
});
|
||||
});
|
||||
@ -30,6 +30,7 @@ function ProjectDetail({ project, i18n }) {
|
||||
scm_branch,
|
||||
scm_clean,
|
||||
scm_delete_on_update,
|
||||
scm_refspec,
|
||||
scm_type,
|
||||
scm_update_on_launch,
|
||||
scm_update_cache_timeout,
|
||||
@ -98,6 +99,7 @@ function ProjectDetail({ project, i18n }) {
|
||||
<Detail label={i18n._(t`SCM Type`)} value={scm_type} />
|
||||
<Detail label={i18n._(t`SCM URL`)} value={scm_url} />
|
||||
<Detail label={i18n._(t`SCM Branch`)} value={scm_branch} />
|
||||
<Detail label={i18n._(t`SCM Refspec`)} value={scm_refspec} />
|
||||
{summary_fields.credential && (
|
||||
<Detail
|
||||
label={i18n._(t`SCM Credential`)}
|
||||
|
||||
@ -88,6 +88,7 @@ describe('<ProjectDetail />', () => {
|
||||
assertDetail('SCM Type', mockProject.scm_type);
|
||||
assertDetail('SCM URL', mockProject.scm_url);
|
||||
assertDetail('SCM Branch', mockProject.scm_branch);
|
||||
assertDetail('SCM Refspec', mockProject.scm_refspec);
|
||||
assertDetail(
|
||||
'SCM Credential',
|
||||
`Scm: ${mockProject.summary_fields.credential.name}`
|
||||
|
||||
@ -91,7 +91,7 @@ class ProjectListItem extends React.Component {
|
||||
<Link
|
||||
id={labelId}
|
||||
to={`${detailUrl}`}
|
||||
style={{ marginLeft: '10px' }}
|
||||
css={{ marginLeft: '10px' }}
|
||||
>
|
||||
<b>{project.name}</b>
|
||||
</Link>
|
||||
|
||||
319
awx/ui_next/src/screens/Project/shared/ProjectForm.jsx
Normal file
319
awx/ui_next/src/screens/Project/shared/ProjectForm.jsx
Normal file
@ -0,0 +1,319 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { Formik, Field } from 'formik';
|
||||
import { Config } from '@contexts/Config';
|
||||
import { Form, FormGroup } from '@patternfly/react-core';
|
||||
import AnsibleSelect from '@components/AnsibleSelect';
|
||||
import ContentError from '@components/ContentError';
|
||||
import ContentLoading from '@components/ContentLoading';
|
||||
import FormActionGroup from '@components/FormActionGroup/FormActionGroup';
|
||||
import FormField, { FieldTooltip } from '@components/FormField';
|
||||
import FormRow from '@components/FormRow';
|
||||
import OrganizationLookup from '@components/Lookup/OrganizationLookup';
|
||||
import { CredentialTypesAPI, ProjectsAPI } from '@api';
|
||||
import { required } from '@util/validators';
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
GitSubForm,
|
||||
HgSubForm,
|
||||
SvnSubForm,
|
||||
InsightsSubForm,
|
||||
SubFormTitle,
|
||||
} from './ProjectSubForms';
|
||||
|
||||
const ScmTypeFormRow = styled(FormRow)`
|
||||
background-color: #f5f5f5;
|
||||
grid-column: 1 / -1;
|
||||
margin: 0 -24px;
|
||||
padding: 24px;
|
||||
`;
|
||||
|
||||
function ProjectForm(props) {
|
||||
const { project, handleCancel, handleSubmit, i18n } = props;
|
||||
const [contentError, setContentError] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [organization, setOrganization] = useState(null);
|
||||
const [scmTypeOptions, setScmTypeOptions] = useState(null);
|
||||
const [scmCredential, setScmCredential] = useState({
|
||||
typeId: null,
|
||||
value: null,
|
||||
});
|
||||
const [insightsCredential, setInsightsCredential] = useState({
|
||||
typeId: null,
|
||||
value: null,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
try {
|
||||
const [
|
||||
{
|
||||
data: {
|
||||
results: [scmCredentialType],
|
||||
},
|
||||
},
|
||||
{
|
||||
data: {
|
||||
results: [insightsCredentialType],
|
||||
},
|
||||
},
|
||||
{
|
||||
data: {
|
||||
actions: {
|
||||
GET: {
|
||||
scm_type: { choices },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
] = await Promise.all([
|
||||
CredentialTypesAPI.read({ kind: 'scm' }),
|
||||
CredentialTypesAPI.read({ name: 'Insights' }),
|
||||
ProjectsAPI.readOptions(),
|
||||
]);
|
||||
|
||||
setScmCredential({ typeId: scmCredentialType.id });
|
||||
setInsightsCredential({ typeId: insightsCredentialType.id });
|
||||
setScmTypeOptions(choices);
|
||||
} catch (error) {
|
||||
setContentError(error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
const resetScmTypeFields = form => {
|
||||
const scmFormFields = [
|
||||
'scm_url',
|
||||
'scm_branch',
|
||||
'scm_refspec',
|
||||
'credential',
|
||||
'scm_clean',
|
||||
'scm_delete_on_update',
|
||||
'scm_update_on_launch',
|
||||
'allow_override',
|
||||
'scm_update_cache_timeout',
|
||||
];
|
||||
|
||||
scmFormFields.forEach(field => {
|
||||
form.setFieldValue(field, form.initialValues[field]);
|
||||
form.setFieldTouched(field, false);
|
||||
});
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return <ContentLoading />;
|
||||
}
|
||||
|
||||
if (contentError) {
|
||||
return <ContentError error={contentError} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Formik
|
||||
initialValues={{
|
||||
allow_override: project.allow_override || false,
|
||||
credential: project.credential || '',
|
||||
custom_virtualenv: project.custom_virtualenv || '',
|
||||
description: project.description || '',
|
||||
name: project.name || '',
|
||||
organization: project.organization || '',
|
||||
scm_branch: project.scm_branch || '',
|
||||
scm_clean: project.scm_clean || false,
|
||||
scm_delete_on_update: project.scm_delete_on_update || false,
|
||||
scm_refspec: project.scm_refspec || '',
|
||||
scm_type: project.scm_type || '',
|
||||
scm_update_cache_timeout: project.scm_update_cache_timeout || 0,
|
||||
scm_update_on_launch: project.scm_update_on_launch || false,
|
||||
scm_url: project.scm_url || '',
|
||||
}}
|
||||
onSubmit={handleSubmit}
|
||||
render={formik => (
|
||||
<Form
|
||||
autoComplete="off"
|
||||
onSubmit={formik.handleSubmit}
|
||||
css="padding: 0 24px"
|
||||
>
|
||||
<FormRow>
|
||||
<FormField
|
||||
id="project-name"
|
||||
label={i18n._(t`Name`)}
|
||||
name="name"
|
||||
type="text"
|
||||
validate={required(null, i18n)}
|
||||
isRequired
|
||||
/>
|
||||
<FormField
|
||||
id="project-description"
|
||||
label={i18n._(t`Description`)}
|
||||
name="description"
|
||||
type="text"
|
||||
/>
|
||||
<Field
|
||||
name="organization"
|
||||
validate={required(
|
||||
i18n._(t`Select a value for this field`),
|
||||
i18n
|
||||
)}
|
||||
render={({ form }) => (
|
||||
<OrganizationLookup
|
||||
helperTextInvalid={form.errors.organization}
|
||||
isValid={
|
||||
!form.touched.organization || !form.errors.organization
|
||||
}
|
||||
onBlur={() => form.setFieldTouched('organization')}
|
||||
onChange={value => {
|
||||
form.setFieldValue('organization', value.id);
|
||||
setOrganization(value);
|
||||
}}
|
||||
value={organization}
|
||||
required
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Field
|
||||
name="scm_type"
|
||||
validate={required(
|
||||
i18n._(t`Select a value for this field`),
|
||||
i18n
|
||||
)}
|
||||
render={({ field, form }) => (
|
||||
<FormGroup
|
||||
fieldId="project-scm-type"
|
||||
helperTextInvalid={form.errors.scm_type}
|
||||
isRequired
|
||||
isValid={!form.touched.scm_type || !form.errors.scm_type}
|
||||
label={i18n._(t`SCM Type`)}
|
||||
>
|
||||
<AnsibleSelect
|
||||
{...field}
|
||||
id="scm_type"
|
||||
data={[
|
||||
{
|
||||
value: '',
|
||||
key: '',
|
||||
label: i18n._(t`Choose an SCM Type`),
|
||||
isDisabled: true,
|
||||
},
|
||||
...scmTypeOptions.map(([value, label]) => {
|
||||
if (label === 'Manual') {
|
||||
value = 'manual';
|
||||
}
|
||||
return {
|
||||
label,
|
||||
value,
|
||||
key: value,
|
||||
};
|
||||
}),
|
||||
]}
|
||||
onChange={(event, value) => {
|
||||
form.setFieldValue('scm_type', value);
|
||||
resetScmTypeFields(form);
|
||||
}}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
/>
|
||||
{formik.values.scm_type !== '' && (
|
||||
<ScmTypeFormRow>
|
||||
<SubFormTitle size="md">{i18n._(t`Type Details`)}</SubFormTitle>
|
||||
{
|
||||
{
|
||||
git: (
|
||||
<GitSubForm
|
||||
setScmCredential={setScmCredential}
|
||||
scmCredential={scmCredential}
|
||||
scmUpdateOnLaunch={formik.values.scm_update_on_launch}
|
||||
/>
|
||||
),
|
||||
hg: (
|
||||
<HgSubForm
|
||||
setScmCredential={setScmCredential}
|
||||
scmCredential={scmCredential}
|
||||
scmUpdateOnLaunch={formik.values.scm_update_on_launch}
|
||||
/>
|
||||
),
|
||||
svn: (
|
||||
<SvnSubForm
|
||||
setScmCredential={setScmCredential}
|
||||
scmCredential={scmCredential}
|
||||
scmUpdateOnLaunch={formik.values.scm_update_on_launch}
|
||||
/>
|
||||
),
|
||||
insights: (
|
||||
<InsightsSubForm
|
||||
setInsightsCredential={setInsightsCredential}
|
||||
insightsCredential={insightsCredential}
|
||||
scmUpdateOnLaunch={formik.values.scm_update_on_launch}
|
||||
/>
|
||||
),
|
||||
}[formik.values.scm_type]
|
||||
}
|
||||
</ScmTypeFormRow>
|
||||
)}
|
||||
<Config>
|
||||
{({ custom_virtualenvs }) =>
|
||||
custom_virtualenvs &&
|
||||
custom_virtualenvs.length > 1 && (
|
||||
<Field
|
||||
name="custom_virtualenv"
|
||||
render={({ field }) => (
|
||||
<FormGroup
|
||||
fieldId="project-custom-virtualenv"
|
||||
label={i18n._(t`Ansible Environment`)}
|
||||
>
|
||||
<FieldTooltip
|
||||
content={i18n._(t`Select the playbook to be executed by
|
||||
this job.`)}
|
||||
/>
|
||||
<AnsibleSelect
|
||||
id="project-custom-virtualenv"
|
||||
data={[
|
||||
{
|
||||
label: i18n._(t`Use Default Ansible Environment`),
|
||||
value: '/venv/ansible/',
|
||||
key: 'default',
|
||||
},
|
||||
...custom_virtualenvs
|
||||
.filter(datum => datum !== '/venv/ansible/')
|
||||
.map(datum => ({
|
||||
label: datum,
|
||||
value: datum,
|
||||
key: datum,
|
||||
})),
|
||||
]}
|
||||
{...field}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</Config>
|
||||
</FormRow>
|
||||
<FormActionGroup
|
||||
onCancel={handleCancel}
|
||||
onSubmit={formik.handleSubmit}
|
||||
/>
|
||||
</Form>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
ProjectForm.propTypes = {
|
||||
handleCancel: PropTypes.func.isRequired,
|
||||
handleSubmit: PropTypes.func.isRequired,
|
||||
project: PropTypes.shape({}),
|
||||
};
|
||||
|
||||
ProjectForm.defaultProps = {
|
||||
project: {},
|
||||
};
|
||||
|
||||
export default withI18n()(ProjectForm);
|
||||
297
awx/ui_next/src/screens/Project/shared/ProjectForm.test.jsx
Normal file
297
awx/ui_next/src/screens/Project/shared/ProjectForm.test.jsx
Normal file
@ -0,0 +1,297 @@
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
|
||||
import { sleep } from '@testUtils/testUtils';
|
||||
import ProjectForm from './ProjectForm';
|
||||
import { CredentialTypesAPI, ProjectsAPI } from '@api';
|
||||
|
||||
jest.mock('@api');
|
||||
|
||||
describe('<ProjectAdd />', () => {
|
||||
let wrapper;
|
||||
const mockData = {
|
||||
name: 'foo',
|
||||
description: 'bar',
|
||||
scm_type: 'git',
|
||||
scm_url: 'https://foo.bar',
|
||||
scm_clean: true,
|
||||
credential: 100,
|
||||
organization: 2,
|
||||
scm_update_on_launch: true,
|
||||
scm_update_cache_timeout: 3,
|
||||
allow_override: false,
|
||||
custom_virtualenv: '/venv/custom-env',
|
||||
};
|
||||
|
||||
const projectOptionsResolve = {
|
||||
data: {
|
||||
actions: {
|
||||
GET: {
|
||||
scm_type: {
|
||||
choices: [
|
||||
['', 'Manual'],
|
||||
['git', 'Git'],
|
||||
['hg', 'Mercurial'],
|
||||
['svn', 'Subversion'],
|
||||
['insights', 'Red Hat Insights'],
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const scmCredentialResolve = {
|
||||
data: {
|
||||
results: [
|
||||
{
|
||||
id: 4,
|
||||
name: 'Source Control',
|
||||
kind: 'scm',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const insightsCredentialResolve = {
|
||||
data: {
|
||||
results: [
|
||||
{
|
||||
id: 5,
|
||||
name: 'Insights',
|
||||
kind: 'insights',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
await ProjectsAPI.readOptions.mockImplementation(
|
||||
() => projectOptionsResolve
|
||||
);
|
||||
await CredentialTypesAPI.read.mockImplementationOnce(
|
||||
() => scmCredentialResolve
|
||||
);
|
||||
await CredentialTypesAPI.read.mockImplementationOnce(
|
||||
() => insightsCredentialResolve
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.unmount();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('initially renders successfully', async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<ProjectForm handleSubmit={jest.fn()} handleCancel={jest.fn()} />
|
||||
);
|
||||
});
|
||||
|
||||
expect(wrapper.find('ProjectForm').length).toBe(1);
|
||||
});
|
||||
|
||||
test('new form displays primary form fields', async () => {
|
||||
const config = {
|
||||
custom_virtualenvs: ['venv/foo', 'venv/bar'],
|
||||
};
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<ProjectForm handleSubmit={jest.fn()} handleCancel={jest.fn()} />,
|
||||
{
|
||||
context: { config },
|
||||
}
|
||||
);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
expect(wrapper.find('FormGroup[label="Name"]').length).toBe(1);
|
||||
expect(wrapper.find('FormGroup[label="Description"]').length).toBe(1);
|
||||
expect(wrapper.find('FormGroup[label="Organization"]').length).toBe(1);
|
||||
expect(wrapper.find('FormGroup[label="SCM Type"]').length).toBe(1);
|
||||
expect(wrapper.find('FormGroup[label="Ansible Environment"]').length).toBe(
|
||||
1
|
||||
);
|
||||
expect(wrapper.find('FormGroup[label="Options"]').length).toBe(0);
|
||||
});
|
||||
|
||||
test('should display scm subform when scm type select has a value', async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<ProjectForm handleSubmit={jest.fn()} handleCancel={jest.fn()} />
|
||||
);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
const formik = wrapper.find('Formik').instance();
|
||||
const changeState = new Promise(resolve => {
|
||||
formik.setState(
|
||||
{
|
||||
values: {
|
||||
...mockData,
|
||||
},
|
||||
},
|
||||
() => resolve()
|
||||
);
|
||||
});
|
||||
await changeState;
|
||||
wrapper.update();
|
||||
expect(wrapper.find('FormGroup[label="SCM URL"]').length).toBe(1);
|
||||
expect(
|
||||
wrapper.find('FormGroup[label="SCM Branch/Tag/Commit"]').length
|
||||
).toBe(1);
|
||||
expect(wrapper.find('FormGroup[label="SCM Refspec"]').length).toBe(1);
|
||||
expect(wrapper.find('FormGroup[label="SCM Credential"]').length).toBe(1);
|
||||
expect(wrapper.find('FormGroup[label="Options"]').length).toBe(1);
|
||||
});
|
||||
|
||||
test('inputs should update form value on change', async () => {
|
||||
const project = { ...mockData };
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<ProjectForm
|
||||
handleSubmit={jest.fn()}
|
||||
handleCancel={jest.fn()}
|
||||
project={project}
|
||||
/>
|
||||
);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
const form = wrapper.find('Formik');
|
||||
act(() => {
|
||||
wrapper.find('OrganizationLookup').invoke('onBlur')();
|
||||
wrapper.find('OrganizationLookup').invoke('onChange')({
|
||||
id: 1,
|
||||
name: 'organization',
|
||||
});
|
||||
});
|
||||
expect(form.state('values').organization).toEqual(1);
|
||||
act(() => {
|
||||
wrapper.find('CredentialLookup').invoke('onBlur')();
|
||||
wrapper.find('CredentialLookup').invoke('onChange')({
|
||||
id: 10,
|
||||
name: 'credential',
|
||||
});
|
||||
});
|
||||
expect(form.state('values').credential).toEqual(10);
|
||||
});
|
||||
|
||||
test('should display insights credential lookup when scm type is "Insights"', async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<ProjectForm handleSubmit={jest.fn()} handleCancel={jest.fn()} />
|
||||
);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
const formik = wrapper.find('Formik').instance();
|
||||
const changeState = new Promise(resolve => {
|
||||
formik.setState(
|
||||
{
|
||||
values: {
|
||||
...mockData,
|
||||
scm_type: 'insights',
|
||||
},
|
||||
},
|
||||
() => resolve()
|
||||
);
|
||||
});
|
||||
await changeState;
|
||||
wrapper.update();
|
||||
expect(wrapper.find('FormGroup[label="Insights Credential"]').length).toBe(
|
||||
1
|
||||
);
|
||||
act(() => {
|
||||
wrapper.find('CredentialLookup').invoke('onBlur')();
|
||||
wrapper.find('CredentialLookup').invoke('onChange')({
|
||||
id: 123,
|
||||
name: 'credential',
|
||||
});
|
||||
});
|
||||
expect(formik.state.values.credential).toEqual(123);
|
||||
});
|
||||
|
||||
test('should reset scm subform values when scm type changes', async () => {
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<ProjectForm
|
||||
handleSubmit={jest.fn()}
|
||||
handleCancel={jest.fn()}
|
||||
project={{ scm_type: 'insights' }}
|
||||
/>
|
||||
);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
|
||||
const scmTypeSelect = wrapper.find(
|
||||
'FormGroup[label="SCM Type"] FormSelect'
|
||||
);
|
||||
const formik = wrapper.find('Formik').instance();
|
||||
expect(formik.state.values.scm_url).toEqual('');
|
||||
await act(async () => {
|
||||
scmTypeSelect.props().onChange('hg', { target: { name: 'Mercurial' } });
|
||||
});
|
||||
wrapper.update();
|
||||
await act(async () => {
|
||||
wrapper.find('FormGroup[label="SCM URL"] input').simulate('change', {
|
||||
target: { value: 'baz', name: 'scm_url' },
|
||||
});
|
||||
});
|
||||
expect(formik.state.values.scm_url).toEqual('baz');
|
||||
await act(async () => {
|
||||
scmTypeSelect
|
||||
.props()
|
||||
.onChange('insights', { target: { name: 'insights' } });
|
||||
});
|
||||
wrapper.update();
|
||||
await act(async () => {
|
||||
scmTypeSelect.props().onChange('svn', { target: { name: 'Subversion' } });
|
||||
});
|
||||
wrapper.update();
|
||||
expect(formik.state.values.scm_url).toEqual('');
|
||||
});
|
||||
|
||||
test('should call handleSubmit when Submit button is clicked', async () => {
|
||||
const handleSubmit = jest.fn();
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<ProjectForm
|
||||
project={mockData}
|
||||
handleSubmit={handleSubmit}
|
||||
handleCancel={jest.fn()}
|
||||
/>
|
||||
);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
expect(handleSubmit).not.toHaveBeenCalled();
|
||||
wrapper.find('button[aria-label="Save"]').simulate('click');
|
||||
await sleep(1);
|
||||
expect(handleSubmit).toBeCalled();
|
||||
});
|
||||
|
||||
test('should call handleCancel when Cancel button is clicked', async () => {
|
||||
const handleCancel = jest.fn();
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<ProjectForm
|
||||
project={mockData}
|
||||
handleSubmit={jest.fn()}
|
||||
handleCancel={handleCancel}
|
||||
/>
|
||||
);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
expect(handleCancel).not.toHaveBeenCalled();
|
||||
wrapper.find('button[aria-label="Cancel"]').invoke('onClick')();
|
||||
expect(handleCancel).toBeCalled();
|
||||
});
|
||||
|
||||
test('should display ContentError on throw', async () => {
|
||||
CredentialTypesAPI.read = () => Promise.reject(new Error());
|
||||
await act(async () => {
|
||||
wrapper = mountWithContexts(
|
||||
<ProjectForm handleSubmit={jest.fn()} handleCancel={jest.fn()} />
|
||||
);
|
||||
});
|
||||
await waitForElement(wrapper, 'ContentLoading', el => el.length === 0);
|
||||
expect(wrapper.find('ContentError').length).toBe(1);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,84 @@
|
||||
import React from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import FormField from '@components/FormField';
|
||||
import {
|
||||
UrlFormField,
|
||||
BranchFormField,
|
||||
ScmCredentialFormField,
|
||||
ScmTypeOptions,
|
||||
} from './SharedFields';
|
||||
|
||||
const GitSubForm = ({
|
||||
i18n,
|
||||
scmCredential,
|
||||
setScmCredential,
|
||||
scmUpdateOnLaunch,
|
||||
}) => (
|
||||
<>
|
||||
<UrlFormField
|
||||
i18n={i18n}
|
||||
tooltip={
|
||||
<span>
|
||||
{i18n._(t`Example URLs for GIT SCM include:`)}
|
||||
<ul css="margin: 10px 0 10px 20px">
|
||||
<li>https://github.com/ansible/ansible.git</li>
|
||||
<li>git@github.com:ansible/ansible.git</li>
|
||||
<li>git://servername.example.com/ansible.git</li>
|
||||
</ul>
|
||||
{i18n._(t`Note: When using SSH protocol for GitHub or
|
||||
Bitbucket, enter an SSH key only, do not enter a username
|
||||
(other than git). Additionally, GitHub and Bitbucket do
|
||||
not support password authentication when using SSH. GIT
|
||||
read only protocol (git://) does not use username or
|
||||
password information.`)}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<BranchFormField i18n={i18n} label={i18n._(t`SCM Branch/Tag/Commit`)} />
|
||||
<FormField
|
||||
id="project-scm-refspec"
|
||||
label={i18n._(t`SCM Refspec`)}
|
||||
name="scm_refspec"
|
||||
type="text"
|
||||
tooltipMaxWidth="400px"
|
||||
tooltip={
|
||||
<span>
|
||||
{i18n._(t`A refspec to fetch (passed to the Ansible git
|
||||
module). This parameter allows access to references via
|
||||
the branch field not otherwise available.`)}
|
||||
<br />
|
||||
<br />
|
||||
{i18n._(t`Note: This field assumes the remote name is "origin".`)}
|
||||
<br />
|
||||
<br />
|
||||
{i18n._(t`Examples include:`)}
|
||||
<ul css={{ margin: '10px 0 10px 20px' }}>
|
||||
<li>refs/*:refs/remotes/origin/*</li>
|
||||
<li>refs/pull/62/head:refs/remotes/origin/pull/62/head</li>
|
||||
</ul>
|
||||
{i18n._(t`The first fetches all references. The second
|
||||
fetches the Github pull request number 62, in this example
|
||||
the branch needs to be "pull/62/head".`)}
|
||||
<br />
|
||||
<br />
|
||||
{i18n._(t`For more information, refer to the`)}{' '}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://docs.ansible.com/ansible-tower/latest/html/userguide/projects.html#manage-playbooks-using-source-control"
|
||||
>
|
||||
{i18n._(t`Ansible Tower Documentation.`)}
|
||||
</a>
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<ScmCredentialFormField
|
||||
setScmCredential={setScmCredential}
|
||||
scmCredential={scmCredential}
|
||||
/>
|
||||
<ScmTypeOptions scmUpdateOnLaunch={scmUpdateOnLaunch} />
|
||||
</>
|
||||
);
|
||||
|
||||
export default withI18n()(GitSubForm);
|
||||
@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import {
|
||||
UrlFormField,
|
||||
BranchFormField,
|
||||
ScmCredentialFormField,
|
||||
ScmTypeOptions,
|
||||
} from './SharedFields';
|
||||
|
||||
const HgSubForm = ({
|
||||
i18n,
|
||||
scmCredential,
|
||||
setScmCredential,
|
||||
scmUpdateOnLaunch,
|
||||
}) => (
|
||||
<>
|
||||
<UrlFormField
|
||||
i18n={i18n}
|
||||
tooltip={
|
||||
<span>
|
||||
{i18n._(t`Example URLs for Mercurial SCM include:`)}
|
||||
<ul css={{ margin: '10px 0 10px 20px' }}>
|
||||
<li>https://bitbucket.org/username/project</li>
|
||||
<li>ssh://hg@bitbucket.org/username/project</li>
|
||||
<li>ssh://server.example.com/path</li>
|
||||
</ul>
|
||||
{i18n._(t`Note: Mercurial does not support password authentication
|
||||
for SSH. Do not put the username and key in the URL. If using
|
||||
Bitbucket and SSH, do not supply your Bitbucket username.
|
||||
`)}
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<BranchFormField i18n={i18n} label={i18n._(t`SCM Branch/Tag/Revision`)} />
|
||||
<ScmCredentialFormField
|
||||
setScmCredential={setScmCredential}
|
||||
scmCredential={scmCredential}
|
||||
/>
|
||||
<ScmTypeOptions scmUpdateOnLaunch={scmUpdateOnLaunch} />
|
||||
</>
|
||||
);
|
||||
|
||||
export default withI18n()(HgSubForm);
|
||||
@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { Field } from 'formik';
|
||||
import CredentialLookup from '@components/Lookup/CredentialLookup';
|
||||
import { required } from '@util/validators';
|
||||
import { ScmTypeOptions } from './SharedFields';
|
||||
|
||||
const InsightsSubForm = ({
|
||||
i18n,
|
||||
setInsightsCredential,
|
||||
insightsCredential,
|
||||
scmUpdateOnLaunch,
|
||||
}) => (
|
||||
<>
|
||||
<Field
|
||||
name="credential"
|
||||
validate={required(i18n._(t`Select a value for this field`), i18n)}
|
||||
render={({ form }) => (
|
||||
<CredentialLookup
|
||||
credentialTypeId={insightsCredential.typeId}
|
||||
label={i18n._(t`Insights Credential`)}
|
||||
helperTextInvalid={form.errors.credential}
|
||||
isValid={!form.touched.credential || !form.errors.credential}
|
||||
onBlur={() => form.setFieldTouched('credential')}
|
||||
onChange={credential => {
|
||||
form.setFieldValue('credential', credential.id);
|
||||
setInsightsCredential({
|
||||
...insightsCredential,
|
||||
value: credential,
|
||||
});
|
||||
}}
|
||||
value={insightsCredential.value}
|
||||
required
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<ScmTypeOptions hideAllowOverride scmUpdateOnLaunch={scmUpdateOnLaunch} />
|
||||
</>
|
||||
);
|
||||
|
||||
export default withI18n()(InsightsSubForm);
|
||||
@ -0,0 +1,135 @@
|
||||
import React from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import { Field } from 'formik';
|
||||
import CredentialLookup from '@components/Lookup/CredentialLookup';
|
||||
import FormField, { CheckboxField } from '@components/FormField';
|
||||
import { required } from '@util/validators';
|
||||
import FormRow from '@components/FormRow';
|
||||
import { FormGroup, Title } from '@patternfly/react-core';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const SubFormTitle = styled(Title)`
|
||||
--pf-c-title--m-md--FontWeight: 700;
|
||||
grid-column: 1 / -1;
|
||||
`;
|
||||
|
||||
export const UrlFormField = withI18n()(({ i18n, tooltip }) => (
|
||||
<FormField
|
||||
id="project-scm-url"
|
||||
isRequired
|
||||
label={i18n._(t`SCM URL`)}
|
||||
name="scm_url"
|
||||
tooltip={tooltip}
|
||||
tooltipMaxWidth="350px"
|
||||
type="text"
|
||||
validate={required(null, i18n)}
|
||||
/>
|
||||
));
|
||||
|
||||
export const BranchFormField = withI18n()(({ i18n, label }) => (
|
||||
<FormField
|
||||
id="project-scm-branch"
|
||||
name="scm_branch"
|
||||
type="text"
|
||||
label={label}
|
||||
tooltip={i18n._(t`Branch to checkout. In addition to branches,
|
||||
you can input tags, commit hashes, and arbitrary refs. Some
|
||||
commit hashes and refs may not be availble unless you also
|
||||
provide a custom refspec.`)}
|
||||
/>
|
||||
));
|
||||
|
||||
export const ScmCredentialFormField = withI18n()(
|
||||
({ i18n, setScmCredential, scmCredential }) => (
|
||||
<Field
|
||||
name="credential"
|
||||
render={({ form }) => (
|
||||
<CredentialLookup
|
||||
credentialTypeId={scmCredential.typeId}
|
||||
label={i18n._(t`SCM Credential`)}
|
||||
value={scmCredential.value}
|
||||
onChange={credential => {
|
||||
form.setFieldValue('credential', credential.id);
|
||||
setScmCredential({
|
||||
...scmCredential,
|
||||
value: credential,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
);
|
||||
|
||||
export const ScmTypeOptions = withI18n()(
|
||||
({ i18n, scmUpdateOnLaunch, hideAllowOverride }) => (
|
||||
<>
|
||||
<FormGroup
|
||||
css="grid-column: 1/-1"
|
||||
fieldId="project-option-checkboxes"
|
||||
label={i18n._(t`Options`)}
|
||||
>
|
||||
<FormRow>
|
||||
<CheckboxField
|
||||
id="option-scm-clean"
|
||||
name="scm_clean"
|
||||
label={i18n._(t`Clean`)}
|
||||
tooltip={i18n._(
|
||||
t`Remove any local modifications prior to performing an update.`
|
||||
)}
|
||||
/>
|
||||
<CheckboxField
|
||||
id="option-scm-delete-on-update"
|
||||
name="scm_delete_on_update"
|
||||
label={i18n._(t`Delete`)}
|
||||
tooltip={i18n._(
|
||||
t`Delete the local repository in its entirety prior to
|
||||
performing an update. Depending on the size of the
|
||||
repository this may significantly increase the amount
|
||||
of time required to complete an update.`
|
||||
)}
|
||||
/>
|
||||
<CheckboxField
|
||||
id="option-scm-update-on-launch"
|
||||
name="scm_update_on_launch"
|
||||
label={i18n._(t`Update Revision on Launch`)}
|
||||
tooltip={i18n._(
|
||||
t`Each time a job runs using this project, update the
|
||||
revision of the project prior to starting the job.`
|
||||
)}
|
||||
/>
|
||||
{!hideAllowOverride && (
|
||||
<CheckboxField
|
||||
id="option-allow-override"
|
||||
name="allow_override"
|
||||
label={i18n._(t`Allow Branch Override`)}
|
||||
tooltip={i18n._(
|
||||
t`Allow changing the SCM branch or revision in a job
|
||||
template that uses this project.`
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</FormRow>
|
||||
</FormGroup>
|
||||
{scmUpdateOnLaunch && (
|
||||
<>
|
||||
<SubFormTitle size="md">{i18n._(t`Option Details`)}</SubFormTitle>
|
||||
<FormField
|
||||
id="project-cache-timeout"
|
||||
name="scm_update_cache_timeout"
|
||||
type="number"
|
||||
min="0"
|
||||
label={i18n._(t`Cache Timeout`)}
|
||||
tooltip={i18n._(t`Time in seconds to consider a project
|
||||
to be current. During job runs and callbacks the task
|
||||
system will evaluate the timestamp of the latest project
|
||||
update. If it is older than Cache Timeout, it is not
|
||||
considered current, and a new project update will be
|
||||
performed.`)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
);
|
||||
@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import { withI18n } from '@lingui/react';
|
||||
import { t } from '@lingui/macro';
|
||||
import {
|
||||
UrlFormField,
|
||||
BranchFormField,
|
||||
ScmCredentialFormField,
|
||||
ScmTypeOptions,
|
||||
} from './SharedFields';
|
||||
|
||||
const SvnSubForm = ({
|
||||
i18n,
|
||||
scmCredential,
|
||||
setScmCredential,
|
||||
scmUpdateOnLaunch,
|
||||
}) => (
|
||||
<>
|
||||
<UrlFormField
|
||||
i18n={i18n}
|
||||
tooltip={
|
||||
<span>
|
||||
{i18n._(t`Example URLs for Subversion SCM include:`)}
|
||||
<ul css={{ margin: '10px 0 10px 20px' }}>
|
||||
<li>https://github.com/ansible/ansible</li>
|
||||
<li>svn://servername.example.com/path</li>
|
||||
<li>svn+ssh://servername.example.com/path</li>
|
||||
</ul>
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
<BranchFormField i18n={i18n} label={i18n._(t`Revision #`)} />
|
||||
<ScmCredentialFormField
|
||||
setScmCredential={setScmCredential}
|
||||
scmCredential={scmCredential}
|
||||
/>
|
||||
<ScmTypeOptions scmUpdateOnLaunch={scmUpdateOnLaunch} />
|
||||
</>
|
||||
);
|
||||
|
||||
export default withI18n()(SvnSubForm);
|
||||
@ -0,0 +1,5 @@
|
||||
export { default as GitSubForm } from './GitSubForm';
|
||||
export { default as HgSubForm } from './HgSubForm';
|
||||
export { default as SvnSubForm } from './SvnSubForm';
|
||||
export { default as InsightsSubForm } from './InsightsSubForm';
|
||||
export { SubFormTitle } from './SharedFields';
|
||||
Loading…
x
Reference in New Issue
Block a user