mirror of
https://github.com/ansible/awx.git
synced 2026-03-05 18:51:06 -03:30
Add playbook select and project field validation
This commit is contained in:
@@ -4,6 +4,12 @@ class Projects extends Base {
|
|||||||
constructor(http) {
|
constructor(http) {
|
||||||
super(http);
|
super(http);
|
||||||
this.baseUrl = '/api/v2/projects/';
|
this.baseUrl = '/api/v2/projects/';
|
||||||
|
|
||||||
|
this.readPlaybooks = this.readPlaybooks.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
readPlaybooks(id) {
|
||||||
|
return this.http.get(`${this.baseUrl}${id}/playbooks/`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,13 +17,16 @@ class AnsibleSelect extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { value, data, i18n } = this.props;
|
const { id, data, i18n, isValid, onBlur, value } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormSelect
|
<FormSelect
|
||||||
|
id={id}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={this.onSelectChange}
|
onChange={this.onSelectChange}
|
||||||
|
onBlur={onBlur}
|
||||||
aria-label={i18n._(t`Select Input`)}
|
aria-label={i18n._(t`Select Input`)}
|
||||||
|
isValid={isValid}
|
||||||
>
|
>
|
||||||
{data.map(datum => (
|
{data.map(datum => (
|
||||||
<FormSelectOption
|
<FormSelectOption
|
||||||
@@ -40,10 +43,15 @@ class AnsibleSelect extends React.Component {
|
|||||||
|
|
||||||
AnsibleSelect.defaultProps = {
|
AnsibleSelect.defaultProps = {
|
||||||
data: [],
|
data: [],
|
||||||
|
isValid: true,
|
||||||
|
onBlur: () => {},
|
||||||
};
|
};
|
||||||
|
|
||||||
AnsibleSelect.propTypes = {
|
AnsibleSelect.propTypes = {
|
||||||
data: PropTypes.arrayOf(PropTypes.object),
|
data: PropTypes.arrayOf(PropTypes.object),
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
|
isValid: PropTypes.bool,
|
||||||
|
onBlur: PropTypes.func,
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
value: PropTypes.string.isRequired,
|
value: PropTypes.string.isRequired,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ describe('<AnsibleSelect />', () => {
|
|||||||
test('initially renders succesfully', async () => {
|
test('initially renders succesfully', async () => {
|
||||||
mountWithContexts(
|
mountWithContexts(
|
||||||
<AnsibleSelect
|
<AnsibleSelect
|
||||||
|
id="bar"
|
||||||
value="foo"
|
value="foo"
|
||||||
name="bar"
|
name="bar"
|
||||||
onChange={() => {}}
|
onChange={() => {}}
|
||||||
@@ -31,6 +32,7 @@ describe('<AnsibleSelect />', () => {
|
|||||||
const spy = jest.spyOn(_AnsibleSelect.prototype, 'onSelectChange');
|
const spy = jest.spyOn(_AnsibleSelect.prototype, 'onSelectChange');
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<AnsibleSelect
|
<AnsibleSelect
|
||||||
|
id="bar"
|
||||||
value="foo"
|
value="foo"
|
||||||
name="bar"
|
name="bar"
|
||||||
onChange={() => {}}
|
onChange={() => {}}
|
||||||
@@ -45,6 +47,7 @@ describe('<AnsibleSelect />', () => {
|
|||||||
test('Returns correct select options', () => {
|
test('Returns correct select options', () => {
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<AnsibleSelect
|
<AnsibleSelect
|
||||||
|
id="bar"
|
||||||
value="foo"
|
value="foo"
|
||||||
name="bar"
|
name="bar"
|
||||||
onChange={() => {}}
|
onChange={() => {}}
|
||||||
|
|||||||
@@ -186,6 +186,7 @@ class Lookup extends React.Component {
|
|||||||
columns,
|
columns,
|
||||||
multiple,
|
multiple,
|
||||||
name,
|
name,
|
||||||
|
onBlur,
|
||||||
required,
|
required,
|
||||||
i18n,
|
i18n,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
@@ -209,7 +210,7 @@ class Lookup extends React.Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<InputGroup>
|
<InputGroup onBlur={onBlur}>
|
||||||
<Button
|
<Button
|
||||||
aria-label="Search"
|
aria-label="Search"
|
||||||
id={id}
|
id={id}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { withRouter, Redirect } from 'react-router-dom';
|
|||||||
import { CardBody } from '@patternfly/react-core';
|
import { CardBody } from '@patternfly/react-core';
|
||||||
import ContentError from '@components/ContentError';
|
import ContentError from '@components/ContentError';
|
||||||
import ContentLoading from '@components/ContentLoading';
|
import ContentLoading from '@components/ContentLoading';
|
||||||
import { JobTemplatesAPI } from '@api';
|
import { JobTemplatesAPI, ProjectsAPI } from '@api';
|
||||||
import { JobTemplate } from '@types';
|
import { JobTemplate } from '@types';
|
||||||
import JobTemplateForm from '../shared/JobTemplateForm';
|
import JobTemplateForm from '../shared/JobTemplateForm';
|
||||||
|
|
||||||
@@ -21,6 +21,7 @@ class JobTemplateEdit extends Component {
|
|||||||
contentError: null,
|
contentError: null,
|
||||||
formSubmitError: null,
|
formSubmitError: null,
|
||||||
relatedCredentials: [],
|
relatedCredentials: [],
|
||||||
|
relatedProjectPlaybooks: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -31,6 +32,9 @@ class JobTemplateEdit extends Component {
|
|||||||
this.handleCancel = this.handleCancel.bind(this);
|
this.handleCancel = this.handleCancel.bind(this);
|
||||||
this.handleSubmit = this.handleSubmit.bind(this);
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
this.loadRelatedCredentials = this.loadRelatedCredentials.bind(this);
|
this.loadRelatedCredentials = this.loadRelatedCredentials.bind(this);
|
||||||
|
this.loadRelatedProjectPlaybooks = this.loadRelatedProjectPlaybooks.bind(
|
||||||
|
this
|
||||||
|
);
|
||||||
this.submitLabels = this.submitLabels.bind(this);
|
this.submitLabels = this.submitLabels.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,15 +45,13 @@ class JobTemplateEdit extends Component {
|
|||||||
async loadRelated() {
|
async loadRelated() {
|
||||||
this.setState({ contentError: null, hasContentLoading: true });
|
this.setState({ contentError: null, hasContentLoading: true });
|
||||||
try {
|
try {
|
||||||
const [
|
const [relatedCredentials, relatedProjectPlaybooks] = await Promise.all([
|
||||||
relatedCredentials,
|
|
||||||
// relatedProjectPlaybooks,
|
|
||||||
] = await Promise.all([
|
|
||||||
this.loadRelatedCredentials(),
|
this.loadRelatedCredentials(),
|
||||||
// this.loadRelatedProjectPlaybooks(),
|
this.loadRelatedProjectPlaybooks(),
|
||||||
]);
|
]);
|
||||||
this.setState({
|
this.setState({
|
||||||
relatedCredentials,
|
relatedCredentials,
|
||||||
|
relatedProjectPlaybooks,
|
||||||
});
|
});
|
||||||
} catch (contentError) {
|
} catch (contentError) {
|
||||||
this.setState({ contentError });
|
this.setState({ contentError });
|
||||||
@@ -86,7 +88,20 @@ class JobTemplateEdit extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleSubmit(values, newLabels = [], removedLabels = []) {
|
async loadRelatedProjectPlaybooks() {
|
||||||
|
const {
|
||||||
|
template: { project },
|
||||||
|
} = this.props;
|
||||||
|
try {
|
||||||
|
const { data: playbooks = [] } = await ProjectsAPI.readPlaybooks(project);
|
||||||
|
this.setState({ relatedProjectPlaybooks: playbooks });
|
||||||
|
return playbooks;
|
||||||
|
} catch (err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleSubmit(values) {
|
||||||
const {
|
const {
|
||||||
template: { id },
|
template: { id },
|
||||||
history,
|
history,
|
||||||
@@ -95,14 +110,16 @@ class JobTemplateEdit extends Component {
|
|||||||
this.setState({ formSubmitError: null });
|
this.setState({ formSubmitError: null });
|
||||||
try {
|
try {
|
||||||
await JobTemplatesAPI.update(id, { ...values });
|
await JobTemplatesAPI.update(id, { ...values });
|
||||||
await Promise.all([this.submitLabels(newLabels, removedLabels)]);
|
await Promise.all([
|
||||||
|
this.submitLabels(values.newLabels, values.removedLabels),
|
||||||
|
]);
|
||||||
history.push(this.detailsUrl);
|
history.push(this.detailsUrl);
|
||||||
} catch (formSubmitError) {
|
} catch (formSubmitError) {
|
||||||
this.setState({ formSubmitError });
|
this.setState({ formSubmitError });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async submitLabels(newLabels, removedLabels) {
|
async submitLabels(newLabels = [], removedLabels = []) {
|
||||||
const {
|
const {
|
||||||
template: { id },
|
template: { id },
|
||||||
} = this.props;
|
} = this.props;
|
||||||
@@ -131,7 +148,12 @@ class JobTemplateEdit extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { template } = this.props;
|
const { template } = this.props;
|
||||||
const { contentError, formSubmitError, hasContentLoading } = this.state;
|
const {
|
||||||
|
contentError,
|
||||||
|
formSubmitError,
|
||||||
|
hasContentLoading,
|
||||||
|
relatedProjectPlaybooks,
|
||||||
|
} = this.state;
|
||||||
const canEdit = template.summary_fields.user_capabilities.edit;
|
const canEdit = template.summary_fields.user_capabilities.edit;
|
||||||
|
|
||||||
if (hasContentLoading) {
|
if (hasContentLoading) {
|
||||||
@@ -152,6 +174,7 @@ class JobTemplateEdit extends Component {
|
|||||||
template={template}
|
template={template}
|
||||||
handleCancel={this.handleCancel}
|
handleCancel={this.handleCancel}
|
||||||
handleSubmit={this.handleSubmit}
|
handleSubmit={this.handleSubmit}
|
||||||
|
relatedProjectPlaybooks={relatedProjectPlaybooks}
|
||||||
/>
|
/>
|
||||||
{formSubmitError ? <div> error </div> : null}
|
{formSubmitError ? <div> error </div> : null}
|
||||||
</CardBody>
|
</CardBody>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
|||||||
import { withRouter } from 'react-router-dom';
|
import { withRouter } from 'react-router-dom';
|
||||||
import { withI18n } from '@lingui/react';
|
import { withI18n } from '@lingui/react';
|
||||||
import { t } from '@lingui/macro';
|
import { t } from '@lingui/macro';
|
||||||
import { Formik, Field } from 'formik';
|
import { withFormik, Field } from 'formik';
|
||||||
import { Form, FormGroup, Tooltip, Card } from '@patternfly/react-core';
|
import { Form, FormGroup, Tooltip, Card } from '@patternfly/react-core';
|
||||||
import { QuestionCircleIcon as PFQuestionCircleIcon } from '@patternfly/react-icons';
|
import { QuestionCircleIcon as PFQuestionCircleIcon } from '@patternfly/react-icons';
|
||||||
import ContentError from '@components/ContentError';
|
import ContentError from '@components/ContentError';
|
||||||
@@ -18,7 +18,7 @@ import styled from 'styled-components';
|
|||||||
import { JobTemplate } from '@types';
|
import { JobTemplate } from '@types';
|
||||||
import InventoriesLookup from './InventoriesLookup';
|
import InventoriesLookup from './InventoriesLookup';
|
||||||
import ProjectLookup from './ProjectLookup';
|
import ProjectLookup from './ProjectLookup';
|
||||||
import { LabelsAPI } from '@api';
|
import { LabelsAPI, ProjectsAPI } from '@api';
|
||||||
|
|
||||||
const QuestionCircleIcon = styled(PFQuestionCircleIcon)`
|
const QuestionCircleIcon = styled(PFQuestionCircleIcon)`
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
@@ -47,6 +47,7 @@ class JobTemplateForm extends Component {
|
|||||||
summary_fields: {
|
summary_fields: {
|
||||||
inventory: null,
|
inventory: null,
|
||||||
labels: { results: [] },
|
labels: { results: [] },
|
||||||
|
project: null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -61,14 +62,21 @@ class JobTemplateForm extends Component {
|
|||||||
removedLabels: [],
|
removedLabels: [],
|
||||||
project: props.template.summary_fields.project,
|
project: props.template.summary_fields.project,
|
||||||
inventory: props.template.summary_fields.inventory,
|
inventory: props.template.summary_fields.inventory,
|
||||||
|
relatedProjectPlaybooks: props.relatedProjectPlaybooks,
|
||||||
};
|
};
|
||||||
this.handleNewLabel = this.handleNewLabel.bind(this);
|
this.handleNewLabel = this.handleNewLabel.bind(this);
|
||||||
this.loadLabels = this.loadLabels.bind(this);
|
this.loadLabels = this.loadLabels.bind(this);
|
||||||
this.removeLabel = this.removeLabel.bind(this);
|
this.removeLabel = this.removeLabel.bind(this);
|
||||||
|
this.handleProjectValidation = this.handleProjectValidation.bind(this);
|
||||||
|
this.loadRelatedProjectPlaybooks = this.loadRelatedProjectPlaybooks.bind(
|
||||||
|
this
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
async componentDidMount() {
|
||||||
this.loadLabels(QSConfig);
|
const { validateField } = this.props;
|
||||||
|
await this.loadLabels(QSConfig);
|
||||||
|
validateField('project');
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadLabels(QueryConfig) {
|
async loadLabels(QueryConfig) {
|
||||||
@@ -101,7 +109,7 @@ class JobTemplateForm extends Component {
|
|||||||
|
|
||||||
handleNewLabel(label) {
|
handleNewLabel(label) {
|
||||||
const { newLabels } = this.state;
|
const { newLabels } = this.state;
|
||||||
const { template } = this.props;
|
const { template, setFieldValue } = this.props;
|
||||||
const isIncluded = newLabels.some(newLabel => newLabel.name === label.name);
|
const isIncluded = newLabels.some(newLabel => newLabel.name === label.name);
|
||||||
if (isIncluded) {
|
if (isIncluded) {
|
||||||
const filteredLabels = newLabels.filter(
|
const filteredLabels = newLabels.filter(
|
||||||
@@ -109,6 +117,13 @@ class JobTemplateForm extends Component {
|
|||||||
);
|
);
|
||||||
this.setState({ newLabels: filteredLabels });
|
this.setState({ newLabels: filteredLabels });
|
||||||
} else if (typeof label === 'string') {
|
} else if (typeof label === 'string') {
|
||||||
|
setFieldValue('newLabels', [
|
||||||
|
...newLabels,
|
||||||
|
{
|
||||||
|
name: label,
|
||||||
|
organization: template.summary_fields.inventory.organization_id,
|
||||||
|
},
|
||||||
|
]);
|
||||||
this.setState({
|
this.setState({
|
||||||
newLabels: [
|
newLabels: [
|
||||||
...newLabels,
|
...newLabels,
|
||||||
@@ -119,6 +134,10 @@ class JobTemplateForm extends Component {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
setFieldValue('newLabels', [
|
||||||
|
...newLabels,
|
||||||
|
{ name: label.name, associate: true, id: label.id },
|
||||||
|
]);
|
||||||
this.setState({
|
this.setState({
|
||||||
newLabels: [
|
newLabels: [
|
||||||
...newLabels,
|
...newLabels,
|
||||||
@@ -130,13 +149,20 @@ class JobTemplateForm extends Component {
|
|||||||
|
|
||||||
removeLabel(label) {
|
removeLabel(label) {
|
||||||
const { removedLabels, newLabels } = this.state;
|
const { removedLabels, newLabels } = this.state;
|
||||||
const { template } = this.props;
|
const { template, setFieldValue } = this.props;
|
||||||
|
|
||||||
const isAssociatedLabel = template.summary_fields.labels.results.some(
|
const isAssociatedLabel = template.summary_fields.labels.results.some(
|
||||||
tempLabel => tempLabel.id === label.id
|
tempLabel => tempLabel.id === label.id
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isAssociatedLabel) {
|
if (isAssociatedLabel) {
|
||||||
|
setFieldValue(
|
||||||
|
'removedLabels',
|
||||||
|
removedLabels.concat({
|
||||||
|
disassociate: true,
|
||||||
|
id: label.id,
|
||||||
|
})
|
||||||
|
);
|
||||||
this.setState({
|
this.setState({
|
||||||
removedLabels: removedLabels.concat({
|
removedLabels: removedLabels.concat({
|
||||||
disassociate: true,
|
disassociate: true,
|
||||||
@@ -147,10 +173,34 @@ class JobTemplateForm extends Component {
|
|||||||
const filteredLabels = newLabels.filter(
|
const filteredLabels = newLabels.filter(
|
||||||
newLabel => newLabel.name !== label.name
|
newLabel => newLabel.name !== label.name
|
||||||
);
|
);
|
||||||
|
setFieldValue('newLabels', filteredLabels);
|
||||||
this.setState({ newLabels: filteredLabels });
|
this.setState({ newLabels: filteredLabels });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async loadRelatedProjectPlaybooks(project) {
|
||||||
|
try {
|
||||||
|
const { data: playbooks = [] } = await ProjectsAPI.readPlaybooks(project);
|
||||||
|
this.setState({ relatedProjectPlaybooks: playbooks });
|
||||||
|
} catch (contentError) {
|
||||||
|
this.setState({ contentError });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleProjectValidation() {
|
||||||
|
const { i18n, touched } = this.props;
|
||||||
|
const { project } = this.state;
|
||||||
|
return () => {
|
||||||
|
if (!project && touched.project) {
|
||||||
|
return i18n._(t`Select a value for this field`);
|
||||||
|
}
|
||||||
|
if (project && project.status === 'never updated') {
|
||||||
|
return i18n._(t`This project needs to be updated`);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
loadedLabels,
|
loadedLabels,
|
||||||
@@ -158,10 +208,15 @@ class JobTemplateForm extends Component {
|
|||||||
hasContentLoading,
|
hasContentLoading,
|
||||||
inventory,
|
inventory,
|
||||||
project,
|
project,
|
||||||
newLabels,
|
relatedProjectPlaybooks = [],
|
||||||
removedLabels,
|
|
||||||
} = this.state;
|
} = this.state;
|
||||||
const { handleCancel, handleSubmit, i18n, template } = this.props;
|
const {
|
||||||
|
handleCancel,
|
||||||
|
handleSubmit,
|
||||||
|
handleBlur,
|
||||||
|
i18n,
|
||||||
|
template,
|
||||||
|
} = this.props;
|
||||||
const jobTypeOptions = [
|
const jobTypeOptions = [
|
||||||
{
|
{
|
||||||
value: '',
|
value: '',
|
||||||
@@ -177,6 +232,28 @@ class JobTemplateForm extends Component {
|
|||||||
isDisabled: false,
|
isDisabled: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
const playbookOptions = relatedProjectPlaybooks
|
||||||
|
.map(playbook => {
|
||||||
|
return {
|
||||||
|
value: playbook,
|
||||||
|
key: playbook,
|
||||||
|
label: playbook,
|
||||||
|
isDisabled: false,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.reduce(
|
||||||
|
(arr, playbook) => {
|
||||||
|
return arr.concat(playbook);
|
||||||
|
},
|
||||||
|
[
|
||||||
|
{
|
||||||
|
value: '',
|
||||||
|
key: '',
|
||||||
|
label: i18n._(t`Choose a playbook`),
|
||||||
|
isDisabled: false,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
if (hasContentLoading) {
|
if (hasContentLoading) {
|
||||||
return (
|
return (
|
||||||
@@ -195,129 +272,180 @@ class JobTemplateForm extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Formik
|
<Form autoComplete="off" onSubmit={handleSubmit}>
|
||||||
initialValues={{
|
<FormRow>
|
||||||
name: template.name,
|
<FormField
|
||||||
description: template.description,
|
id="template-name"
|
||||||
job_type: template.job_type,
|
name="name"
|
||||||
inventory: template.inventory || '',
|
type="text"
|
||||||
project: template.project || '',
|
label={i18n._(t`Name`)}
|
||||||
playbook: template.playbook,
|
validate={required(null, i18n)}
|
||||||
labels: template.summary_fields.labels.results,
|
isRequired
|
||||||
}}
|
/>
|
||||||
onSubmit={values => {
|
<FormField
|
||||||
handleSubmit(values, newLabels, removedLabels);
|
id="template-description"
|
||||||
}}
|
name="description"
|
||||||
render={formik => (
|
type="text"
|
||||||
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
|
label={i18n._(t`Description`)}
|
||||||
<FormRow>
|
/>
|
||||||
<FormField
|
<Field
|
||||||
id="template-name"
|
name="job_type"
|
||||||
name="name"
|
validate={required(null, i18n)}
|
||||||
type="text"
|
onBlur={handleBlur}
|
||||||
label={i18n._(t`Name`)}
|
render={({ form, field }) => {
|
||||||
validate={required(null, i18n)}
|
const isValid =
|
||||||
isRequired
|
form && (!form.touched[field.name] || !form.errors[field.name]);
|
||||||
/>
|
return (
|
||||||
<FormField
|
<FormGroup
|
||||||
id="template-description"
|
fieldId="template-job-type"
|
||||||
name="description"
|
helperTextInvalid={form.errors.job_type}
|
||||||
type="text"
|
isRequired
|
||||||
label={i18n._(t`Description`)}
|
isValid={isValid}
|
||||||
/>
|
label={i18n._(t`Job Type`)}
|
||||||
<Field
|
|
||||||
name="job_type"
|
|
||||||
validate={required(null, i18n)}
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormGroup
|
|
||||||
fieldId="template-job-type"
|
|
||||||
isRequired
|
|
||||||
label={i18n._(t`Job Type`)}
|
|
||||||
>
|
|
||||||
<Tooltip
|
|
||||||
position="right"
|
|
||||||
content={i18n._(t`For job templates, select run to execute
|
|
||||||
the playbook. Select check to only check playbook syntax,
|
|
||||||
test environment setup, and report problems without
|
|
||||||
executing the playbook.`)}
|
|
||||||
>
|
|
||||||
<QuestionCircleIcon />
|
|
||||||
</Tooltip>
|
|
||||||
<AnsibleSelect data={jobTypeOptions} {...field} />
|
|
||||||
</FormGroup>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Field
|
|
||||||
name="inventory"
|
|
||||||
validate={required(null, i18n)}
|
|
||||||
render={({ form }) => (
|
|
||||||
<InventoriesLookup
|
|
||||||
value={inventory}
|
|
||||||
tooltip={i18n._(t`Select the inventory containing the hosts
|
|
||||||
you want this job to manage.`)}
|
|
||||||
onChange={value => {
|
|
||||||
form.setFieldValue('inventory', value.id);
|
|
||||||
this.setState({ inventory: value });
|
|
||||||
}}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Field
|
|
||||||
name="project"
|
|
||||||
validate={required(null, i18n)}
|
|
||||||
render={({ form }) => (
|
|
||||||
<ProjectLookup
|
|
||||||
value={project}
|
|
||||||
tooltip={i18n._(t`Select the project containing the playbook
|
|
||||||
you want this job to execute.`)}
|
|
||||||
onChange={value => {
|
|
||||||
form.setFieldValue('project', value.id);
|
|
||||||
this.setState({ project: value });
|
|
||||||
}}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
id="template-playbook"
|
|
||||||
name="playbook"
|
|
||||||
type="text"
|
|
||||||
label={i18n._(t`Playbook`)}
|
|
||||||
tooltip={i18n._(
|
|
||||||
t`Select the playbook to be executed by this job.`
|
|
||||||
)}
|
|
||||||
isRequired
|
|
||||||
validate={required(null, i18n)}
|
|
||||||
/>
|
|
||||||
</FormRow>
|
|
||||||
<FormRow>
|
|
||||||
<FormGroup label={i18n._(t`Labels`)} fieldId="template-labels">
|
|
||||||
<Tooltip
|
|
||||||
position="right"
|
|
||||||
content={i18n._(
|
|
||||||
t`Optional labels that describe this job template, such as 'dev' or 'test'. Labels can be used to group and filter job templates and completed jobs.`
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<QuestionCircleIcon />
|
<Tooltip
|
||||||
</Tooltip>
|
position="right"
|
||||||
<MultiSelect
|
content={i18n._(t`For job templates, select run to execute
|
||||||
onAddNewItem={this.handleNewLabel}
|
the playbook. Select check to only check playbook syntax,
|
||||||
onRemoveItem={this.removeLabel}
|
test environment setup, and report problems without
|
||||||
associatedItems={template.summary_fields.labels.results}
|
executing the playbook.`)}
|
||||||
options={loadedLabels}
|
>
|
||||||
|
<QuestionCircleIcon />
|
||||||
|
</Tooltip>
|
||||||
|
<AnsibleSelect
|
||||||
|
isValid={isValid}
|
||||||
|
id="job_type"
|
||||||
|
data={jobTypeOptions}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Field
|
||||||
|
name="inventory"
|
||||||
|
validate={required(null, i18n)}
|
||||||
|
render={({ form }) => (
|
||||||
|
<InventoriesLookup
|
||||||
|
value={inventory}
|
||||||
|
tooltip={i18n._(t`Select the inventory containing the hosts
|
||||||
|
you want this job to manage.`)}
|
||||||
|
onChange={value => {
|
||||||
|
form.setFieldValue('inventory', value.id);
|
||||||
|
this.setState({ inventory: value });
|
||||||
|
}}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Field
|
||||||
|
name="project"
|
||||||
|
validate={this.handleProjectValidation()}
|
||||||
|
render={({ form }) => {
|
||||||
|
const isValid = form && !form.errors.project;
|
||||||
|
return (
|
||||||
|
<ProjectLookup
|
||||||
|
helperTextInvalid={form.errors.project}
|
||||||
|
isValid={isValid}
|
||||||
|
value={project}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
tooltip={i18n._(t`Select the project containing the playbook
|
||||||
|
you want this job to execute.`)}
|
||||||
|
onChange={value => {
|
||||||
|
this.loadRelatedProjectPlaybooks(value.id);
|
||||||
|
form.setFieldValue('project', value.id);
|
||||||
|
form.setFieldTouched('project');
|
||||||
|
this.setState({ project: value });
|
||||||
|
}}
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
);
|
||||||
</FormRow>
|
}}
|
||||||
<FormActionGroup
|
/>
|
||||||
onCancel={handleCancel}
|
<Field
|
||||||
onSubmit={formik.handleSubmit}
|
name="playbook"
|
||||||
|
validate={required(i18n._(t`Select a value for this field`), i18n)}
|
||||||
|
onBlur={handleBlur}
|
||||||
|
render={({ field, form }) => {
|
||||||
|
const isValid =
|
||||||
|
form && (!form.touched[field.name] || !form.errors[field.name]);
|
||||||
|
return (
|
||||||
|
<FormGroup
|
||||||
|
fieldId="template-playbook"
|
||||||
|
helperTextInvalid={form.errors.playbook}
|
||||||
|
isRequired
|
||||||
|
isValid={isValid}
|
||||||
|
label={i18n._(t`Playbook`)}
|
||||||
|
>
|
||||||
|
<Tooltip
|
||||||
|
position="right"
|
||||||
|
content={i18n._(
|
||||||
|
t`Select the playbook to be executed by this job.`
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<QuestionCircleIcon />
|
||||||
|
</Tooltip>
|
||||||
|
<AnsibleSelect
|
||||||
|
id="playbook"
|
||||||
|
data={playbookOptions}
|
||||||
|
isValid={isValid}
|
||||||
|
form={form}
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormRow>
|
||||||
|
<FormRow>
|
||||||
|
<FormGroup label={i18n._(t`Labels`)} fieldId="template-labels">
|
||||||
|
<Tooltip
|
||||||
|
position="right"
|
||||||
|
content={i18n._(
|
||||||
|
t`Optional labels that describe this job template, such as 'dev' or 'test'. Labels can be used to group and filter job templates and completed jobs.`
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<QuestionCircleIcon />
|
||||||
|
</Tooltip>
|
||||||
|
<MultiSelect
|
||||||
|
onAddNewItem={this.handleNewLabel}
|
||||||
|
onRemoveItem={this.removeLabel}
|
||||||
|
associatedItems={template.summary_fields.labels.results}
|
||||||
|
options={loadedLabels}
|
||||||
/>
|
/>
|
||||||
</Form>
|
</FormGroup>
|
||||||
)}
|
</FormRow>
|
||||||
/>
|
<FormActionGroup onCancel={handleCancel} onSubmit={handleSubmit} />
|
||||||
|
</Form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FormikApp = withFormik({
|
||||||
|
mapPropsToValues(props) {
|
||||||
|
const { template = {} } = props;
|
||||||
|
const {
|
||||||
|
name = '',
|
||||||
|
description = '',
|
||||||
|
job_type = '',
|
||||||
|
inventory = '',
|
||||||
|
playbook = '',
|
||||||
|
project = '',
|
||||||
|
summary_fields = { labels: { results: [] } },
|
||||||
|
} = { ...template };
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: name || '',
|
||||||
|
description: description || '',
|
||||||
|
job_type: job_type || '',
|
||||||
|
inventory: inventory || '',
|
||||||
|
project: project || '',
|
||||||
|
playbook: playbook || '',
|
||||||
|
labels: summary_fields.labels.results,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
handleSubmit: (values, bag) => bag.props.handleSubmit(values),
|
||||||
|
})(JobTemplateForm);
|
||||||
|
|
||||||
export { JobTemplateForm as _JobTemplateForm };
|
export { JobTemplateForm as _JobTemplateForm };
|
||||||
export default withI18n()(withRouter(JobTemplateForm));
|
export default withI18n()(withRouter(FormikApp));
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
|
import { mountWithContexts, waitForElement } from '@testUtils/enzymeHelpers';
|
||||||
import { sleep } from '@testUtils/testUtils';
|
import { sleep } from '@testUtils/testUtils';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
import JobTemplateForm, { _JobTemplateForm } from './JobTemplateForm';
|
import JobTemplateForm, { _JobTemplateForm } from './JobTemplateForm';
|
||||||
import { LabelsAPI } from '@api';
|
import { LabelsAPI } from '@api';
|
||||||
|
|
||||||
@@ -41,12 +42,15 @@ describe('<JobTemplateForm />', () => {
|
|||||||
|
|
||||||
test('initially renders successfully', async done => {
|
test('initially renders successfully', async done => {
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<JobTemplateForm
|
shallow(
|
||||||
template={mockData}
|
<JobTemplateForm
|
||||||
handleSubmit={jest.fn()}
|
template={mockData}
|
||||||
handleCancel={jest.fn()}
|
handleSubmit={jest.fn()}
|
||||||
/>
|
handleCancel={jest.fn()}
|
||||||
|
/>
|
||||||
|
).get(0)
|
||||||
);
|
);
|
||||||
|
|
||||||
await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0);
|
await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0);
|
||||||
expect(LabelsAPI.read).toHaveBeenCalled();
|
expect(LabelsAPI.read).toHaveBeenCalled();
|
||||||
expect(
|
expect(
|
||||||
@@ -60,11 +64,13 @@ describe('<JobTemplateForm />', () => {
|
|||||||
|
|
||||||
test('should update form values on input changes', async done => {
|
test('should update form values on input changes', async done => {
|
||||||
const wrapper = mountWithContexts(
|
const wrapper = mountWithContexts(
|
||||||
<JobTemplateForm
|
shallow(
|
||||||
template={mockData}
|
<JobTemplateForm
|
||||||
handleSubmit={jest.fn()}
|
template={mockData}
|
||||||
handleCancel={jest.fn()}
|
handleSubmit={jest.fn()}
|
||||||
/>
|
handleCancel={jest.fn()}
|
||||||
|
/>
|
||||||
|
).get(0)
|
||||||
);
|
);
|
||||||
await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0);
|
await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0);
|
||||||
const form = wrapper.find('Formik');
|
const form = wrapper.find('Formik');
|
||||||
@@ -90,7 +96,7 @@ describe('<JobTemplateForm />', () => {
|
|||||||
name: 'project',
|
name: 'project',
|
||||||
});
|
});
|
||||||
expect(form.state('values').project).toEqual(4);
|
expect(form.state('values').project).toEqual(4);
|
||||||
wrapper.find('input#template-playbook').simulate('change', {
|
wrapper.find('AnsibleSelect[name="playbook"]').simulate('change', {
|
||||||
target: { value: 'new baz type', name: 'playbook' },
|
target: { value: 'new baz type', name: 'playbook' },
|
||||||
});
|
});
|
||||||
expect(form.state('values').playbook).toEqual('new baz type');
|
expect(form.state('values').playbook).toEqual('new baz type');
|
||||||
|
|||||||
@@ -17,12 +17,23 @@ const loadProjects = async params => ProjectsAPI.read(params);
|
|||||||
|
|
||||||
class ProjectLookup extends React.Component {
|
class ProjectLookup extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
const { value, tooltip, onChange, required, i18n } = this.props;
|
const {
|
||||||
|
helperTextInvalid,
|
||||||
|
i18n,
|
||||||
|
isValid,
|
||||||
|
onChange,
|
||||||
|
required,
|
||||||
|
tooltip,
|
||||||
|
value,
|
||||||
|
onBlur,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormGroup
|
<FormGroup
|
||||||
fieldId="project-lookup"
|
fieldId="project"
|
||||||
|
helperTextInvalid={helperTextInvalid}
|
||||||
isRequired={required}
|
isRequired={required}
|
||||||
|
isValid={isValid}
|
||||||
label={i18n._(t`Project`)}
|
label={i18n._(t`Project`)}
|
||||||
>
|
>
|
||||||
{tooltip && (
|
{tooltip && (
|
||||||
@@ -31,10 +42,11 @@ class ProjectLookup extends React.Component {
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
<Lookup
|
<Lookup
|
||||||
id="project-lookup"
|
id="project"
|
||||||
lookupHeader={i18n._(t`Projects`)}
|
lookupHeader={i18n._(t`Projects`)}
|
||||||
name="project"
|
name="project"
|
||||||
value={value}
|
value={value}
|
||||||
|
onBlur={onBlur}
|
||||||
onLookupSave={onChange}
|
onLookupSave={onChange}
|
||||||
getItems={loadProjects}
|
getItems={loadProjects}
|
||||||
required={required}
|
required={required}
|
||||||
@@ -47,15 +59,21 @@ class ProjectLookup extends React.Component {
|
|||||||
|
|
||||||
ProjectLookup.propTypes = {
|
ProjectLookup.propTypes = {
|
||||||
value: Project,
|
value: Project,
|
||||||
tooltip: string,
|
helperTextInvalid: string,
|
||||||
|
isValid: bool,
|
||||||
|
onBlur: func,
|
||||||
onChange: func.isRequired,
|
onChange: func.isRequired,
|
||||||
required: bool,
|
required: bool,
|
||||||
|
tooltip: string,
|
||||||
};
|
};
|
||||||
|
|
||||||
ProjectLookup.defaultProps = {
|
ProjectLookup.defaultProps = {
|
||||||
value: null,
|
helperTextInvalid: '',
|
||||||
tooltip: '',
|
isValid: true,
|
||||||
required: false,
|
required: false,
|
||||||
|
tooltip: '',
|
||||||
|
value: null,
|
||||||
|
onBlur: () => {},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withI18n()(ProjectLookup);
|
export default withI18n()(ProjectLookup);
|
||||||
|
|||||||
Reference in New Issue
Block a user