refactor out OrganizationForm; share between add & edit

This commit is contained in:
Keith Grant
2019-03-27 15:29:46 -04:00
parent 40b2539626
commit 64e933acb4
4 changed files with 239 additions and 247 deletions

View File

@@ -0,0 +1,182 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { withRouter } from 'react-router-dom';
import { Formik, Field } from 'formik';
import { I18n } from '@lingui/react';
import { t } from '@lingui/macro';
import {
Form,
FormGroup,
} from '@patternfly/react-core';
import { ConfigContext } from '../../../context';
import FormField from '../../../components/FormField';
import FormActionGroup from '../../../components/FormActionGroup';
import AnsibleSelect from '../../../components/AnsibleSelect';
import InstanceGroupsLookup from './InstanceGroupsLookup';
import { required } from '../../../util/validators';
class OrganizationForm extends Component {
constructor (props) {
super(props);
this.getRelatedInstanceGroups = this.getRelatedInstanceGroups.bind(this);
this.handleInstanceGroupsChange = this.handleInstanceGroupsChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.state = {
instanceGroups: [],
formIsValid: true,
};
}
async componentDidMount () {
let instanceGroups = [];
if (!this.isEditingNewOrganization()) {
try {
instanceGroups = await this.getRelatedInstanceGroups();
} catch (err) {
this.setState({ error: err });
}
}
this.setState({
instanceGroups,
initialInstanceGroups: [...instanceGroups],
});
}
async getRelatedInstanceGroups () {
const {
api,
organization: { id }
} = this.props;
const { data } = await api.getOrganizationInstanceGroups(id);
return data.results;
}
isEditingNewOrganization () {
const { organization } = this.props;
return !organization.id;
}
handleInstanceGroupsChange (instanceGroups) {
this.setState({ instanceGroups });
}
async handleSubmit (values) {
const { handleSubmit } = this.props;
const { instanceGroups, initialInstanceGroups } = this.state;
const initialIds = initialInstanceGroups.map(ig => ig.id);
const updatedIds = instanceGroups.map(ig => ig.id);
const groupsToAssociate = [...updatedIds]
.filter(x => !initialIds.includes(x));
const groupsToDisassociate = [...initialIds]
.filter(x => !updatedIds.includes(x));
handleSubmit(values, groupsToAssociate, groupsToDisassociate);
}
render () {
const { api, organization, handleCancel } = this.props;
const { instanceGroups, formIsValid, error } = this.state;
const defaultVenv = '/venv/ansible/';
return (
<I18n>
{({ i18n }) => (
<Formik
initialValues={{
name: organization.name,
description: organization.description,
custom_virtualenv: organization.custom_virtualenv || '',
}}
onSubmit={this.handleSubmit}
render={formik => (
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
<div
style={{
display: 'grid',
gridGap: '20px',
gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))'
}}
>
<FormField
id="edit-org-form-name"
name="name"
type="text"
label={i18n._(t`Name`)}
validate={required()}
isRequired
/>
<FormField
id="edit-org-form-description"
name="description"
type="text"
label={i18n._(t`Description`)}
/>
<ConfigContext.Consumer>
{({ custom_virtualenvs }) => (
custom_virtualenvs && custom_virtualenvs.length > 1 && (
<Field
name="custom_virtualenv"
render={({ field }) => (
<FormGroup
fieldId="edit-org-custom-virtualenv"
label={i18n._(t`Ansible Environment`)}
>
<AnsibleSelect
data={custom_virtualenvs}
defaultSelected={defaultVenv}
label={i18n._(t`Ansible Environment`)}
{...field}
/>
</FormGroup>
)}
/>
)
)}
</ConfigContext.Consumer>
</div>
<InstanceGroupsLookup
api={api}
value={instanceGroups}
onChange={this.handleInstanceGroupsChange}
/>
<FormActionGroup
onCancel={handleCancel}
onSubmit={formik.handleSubmit}
submitDisabled={!formIsValid}
/>
{error ? <div>error</div> : null}
</Form>
)}
/>
)}
</I18n>
);
}
}
OrganizationForm.propTypes = {
api: PropTypes.shape().isRequired,
organization: PropTypes.shape(),
handleSubmit: PropTypes.func.isRequired,
handleCancel: PropTypes.func.isRequired,
};
OrganizationForm.defaultProps = {
organization: {
name: '',
description: '',
custom_virtualenv: '',
}
};
OrganizationForm.contextTypes = {
custom_virtualenvs: PropTypes.arrayOf(PropTypes.string)
};
export default withRouter(OrganizationForm);

View File

@@ -1,82 +1,30 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { withRouter } from 'react-router-dom'; import { withRouter } from 'react-router-dom';
import { Formik, Field } from 'formik'; import { CardBody } from '@patternfly/react-core';
import { I18n, i18nMark } from '@lingui/react';
import { t } from '@lingui/macro';
import {
CardBody,
Form,
FormGroup,
} from '@patternfly/react-core';
import { ConfigContext } from '../../../../context'; import OrganizationForm from '../../components/OrganizationForm';
import FormField from '../../../../components/FormField';
import FormActionGroup from '../../../../components/FormActionGroup';
import AnsibleSelect from '../../../../components/AnsibleSelect';
import InstanceGroupsLookup from '../../components/InstanceGroupsLookup';
function required (message) {
return value => {
if (!value.trim()) {
return message || i18nMark('This field must not be blank');
}
return undefined;
};
}
class OrganizationEdit extends Component { class OrganizationEdit extends Component {
constructor (props) { constructor (props) {
super(props); super(props);
this.getRelatedInstanceGroups = this.getRelatedInstanceGroups.bind(this); // this.getRelatedInstanceGroups = this.getRelatedInstanceGroups.bind(this);
this.handleInstanceGroupsChange = this.handleInstanceGroupsChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this); this.handleSubmit = this.handleSubmit.bind(this);
this.postInstanceGroups = this.postInstanceGroups.bind(this); this.postInstanceGroups = this.postInstanceGroups.bind(this);
this.handleCancel = this.handleCancel.bind(this); this.handleCancel = this.handleCancel.bind(this);
this.handleSuccess = this.handleSuccess.bind(this); this.handleSuccess = this.handleSuccess.bind(this);
this.state = { this.state = {
initialInstanceGroups: [],
instanceGroups: [],
error: '', error: '',
formIsValid: true,
}; };
} }
async componentDidMount () { async handleSubmit (values, groupsToAssociate, groupsToDisassociate) {
let instanceGroups;
try {
instanceGroups = await this.getRelatedInstanceGroups();
} catch (err) {
this.setState({ error: err });
}
this.setState({
instanceGroups,
initialInstanceGroups: instanceGroups,
});
}
async getRelatedInstanceGroups () {
const {
api,
organization: { id }
} = this.props;
const { data } = await api.getOrganizationInstanceGroups(id);
return data.results;
}
handleInstanceGroupsChange (instanceGroups) {
this.setState({ instanceGroups });
}
async handleSubmit (values) {
const { api, organization } = this.props; const { api, organization } = this.props;
const { instanceGroups } = this.state;
try { try {
await api.updateOrganizationDetails(organization.id, values); await api.updateOrganizationDetails(organization.id, values);
await this.postInstanceGroups(instanceGroups); await this.postInstanceGroups(groupsToAssociate, groupsToDisassociate);
} catch (err) { } catch (err) {
this.setState({ error: err }); this.setState({ error: err });
} finally { } finally {
@@ -94,19 +42,10 @@ class OrganizationEdit extends Component {
history.push(`/organizations/${id}`); history.push(`/organizations/${id}`);
} }
async postInstanceGroups (instanceGroups) { async postInstanceGroups (groupsToAssociate, groupsToDisassociate) {
const { api, organization } = this.props; const { api, organization } = this.props;
const { initialInstanceGroups } = this.state;
const url = organization.related.instance_groups; const url = organization.related.instance_groups;
const initialIds = initialInstanceGroups.map(ig => ig.id);
const updatedIds = instanceGroups.map(ig => ig.id);
const groupsToAssociate = [...updatedIds]
.filter(x => !initialIds.includes(x));
const groupsToDisassociate = [...initialIds]
.filter(x => !updatedIds.includes(x));
try { try {
await Promise.all(groupsToAssociate.map(async id => { await Promise.all(groupsToAssociate.map(async id => {
await api.associateInstanceGroup(url, id); await api.associateInstanceGroup(url, id);
@@ -121,91 +60,27 @@ class OrganizationEdit extends Component {
render () { render () {
const { api, organization } = this.props; const { api, organization } = this.props;
const { const { error } = this.state;
instanceGroups,
formIsValid,
error,
} = this.state;
const defaultVenv = '/venv/ansible/';
return ( return (
<CardBody> <CardBody>
<I18n> <OrganizationForm
{({ i18n }) => ( api={api}
<Formik organization={organization}
initialValues={{ handleSubmit={this.handleSubmit}
name: organization.name, handleCancel={this.handleCancel}
description: organization.description, />
custom_virtualenv: organization.custom_virtualenv || '', {error ? <div>error</div> : null}
}}
onSubmit={this.handleSubmit}
render={formik => (
<Form autoComplete="off" onSubmit={formik.handleSubmit}>
<div
style={{
display: 'grid',
gridGap: '20px',
gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))'
}}
>
<FormField
id="edit-org-form-name"
name="name"
type="text"
label={i18n._(t`Name`)}
validate={required()}
isRequired
/>
<FormField
id="edit-org-form-description"
name="description"
type="text"
label={i18n._(t`Description`)}
/>
<ConfigContext.Consumer>
{({ custom_virtualenvs }) => (
custom_virtualenvs && custom_virtualenvs.length > 1 && (
<Field
name="custom_virtualenv"
render={({ field }) => (
<FormGroup
fieldId="edit-org-custom-virtualenv"
label={i18n._(t`Ansible Environment`)}
>
<AnsibleSelect
data={custom_virtualenvs}
defaultSelected={defaultVenv}
label={i18n._(t`Ansible Environment`)}
{...field}
/>
</FormGroup>
)}
/>
)
)}
</ConfigContext.Consumer>
</div>
<InstanceGroupsLookup
api={api}
value={instanceGroups}
onChange={this.handleInstanceGroupsChange}
/>
<FormActionGroup
onCancel={this.handleCancel}
onSubmit={formik.handleSubmit}
submitDisabled={!formIsValid}
/>
{ error ? <div>error</div> : '' }
</Form>
)}
/>
)}
</I18n>
</CardBody> </CardBody>
); );
} }
} }
OrganizationEdit.propTypes = {
api: PropTypes.shape().isRequired,
organization: PropTypes.shape().isRequired,
};
OrganizationEdit.contextTypes = { OrganizationEdit.contextTypes = {
custom_virtualenvs: PropTypes.arrayOf(PropTypes.string) custom_virtualenvs: PropTypes.arrayOf(PropTypes.string)
}; };

View File

@@ -1,26 +1,19 @@
import React, { Fragment } from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { withRouter } from 'react-router-dom'; import { withRouter } from 'react-router-dom';
import { I18n } from '@lingui/react'; import { I18n } from '@lingui/react';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { import {
PageSection, PageSection,
Form,
FormGroup,
TextInput,
Gallery,
Card, Card,
CardHeader, CardHeader,
CardBody, CardBody,
Button, Button,
Tooltip, Tooltip,
} from '@patternfly/react-core'; } from '@patternfly/react-core';
import { QuestionCircleIcon, TimesIcon } from '@patternfly/react-icons'; import { TimesIcon } from '@patternfly/react-icons';
import { ConfigContext } from '../../../context'; import OrganizationForm from '../components/OrganizationForm';
import AnsibleSelect from '../../../components/AnsibleSelect';
import FormActionGroup from '../../../components/FormActionGroup';
import InstanceGroupsLookup from '../components/InstanceGroupsLookup';
class OrganizationAdd extends React.Component { class OrganizationAdd extends React.Component {
constructor (props) { constructor (props) {
@@ -33,12 +26,7 @@ class OrganizationAdd extends React.Component {
this.handleSuccess = this.handleSuccess.bind(this); this.handleSuccess = this.handleSuccess.bind(this);
this.state = { this.state = {
name: '',
description: '',
custom_virtualenv: '',
instanceGroups: [],
error: '', error: '',
defaultEnv: '/venv/ansible/',
}; };
} }
@@ -50,23 +38,15 @@ class OrganizationAdd extends React.Component {
this.setState({ [targetName]: val }); this.setState({ [targetName]: val });
} }
async handleSubmit () { async handleSubmit (values, groupsToAssociate) {
const { api } = this.props; const { api } = this.props;
const { name, description, custom_virtualenv, instanceGroups } = this.state;
const data = {
name,
description,
custom_virtualenv
};
try { try {
const { data: response } = await api.createOrganization(data); const { data: response } = await api.createOrganization(values);
const instanceGroupsUrl = response.related.instance_groups; const instanceGroupsUrl = response.related.instance_groups;
try { try {
if (instanceGroups.length > 0) { await Promise.all(groupsToAssociate.map(async id => {
instanceGroups.forEach(async (select) => { await api.associateInstanceGroup(instanceGroupsUrl, id);
await api.associateInstanceGroup(instanceGroupsUrl, select.id); }));
});
}
} catch (err) { } catch (err) {
this.setState({ error: err }); this.setState({ error: err });
} finally { } finally {
@@ -89,15 +69,7 @@ class OrganizationAdd extends React.Component {
render () { render () {
const { api } = this.props; const { api } = this.props;
const { const { error } = this.state;
name,
description,
custom_virtualenv,
defaultEnv,
instanceGroups,
error
} = this.state;
const enabled = name.length > 0; // TODO: add better form validation
return ( return (
<PageSection> <PageSection>
@@ -119,72 +91,12 @@ class OrganizationAdd extends React.Component {
</Tooltip> </Tooltip>
</CardHeader> </CardHeader>
<CardBody> <CardBody>
<Form autoComplete="off"> <OrganizationForm
<Gallery gutter="md"> api={api}
<FormGroup handleSubmit={this.handleSubmit}
label={i18n._(t`Name`)} handleCancel={this.handleCancel}
isRequired />
fieldId="add-org-form-name" {error ? <div>error</div> : ''}
>
<TextInput
isRequired
id="add-org-form-name"
name="name"
value={name}
onChange={this.handleFieldChange}
/>
</FormGroup>
<FormGroup label={i18n._(t`Description`)} fieldId="add-org-form-description">
<TextInput
id="add-org-form-description"
name="description"
value={description}
onChange={this.handleFieldChange}
/>
</FormGroup>
<InstanceGroupsLookup
api={api}
value={instanceGroups}
onChange={this.handleInstanceGroupsChange}
/>
<ConfigContext.Consumer>
{({ custom_virtualenvs }) => (
custom_virtualenvs && custom_virtualenvs.length > 1 && (
<FormGroup
label={(
<Fragment>
{i18n._(t`Ansible Environment`)}
{' '}
<Tooltip
position="right"
content={i18n._(t`Select the custom Python virtual environment for this organization to run on.`)}
>
<QuestionCircleIcon />
</Tooltip>
</Fragment>
)}
fieldId="add-org-custom-virtualenv"
>
<AnsibleSelect
label={i18n._(t`Ansible Environment`)}
name="custom_virtualenv"
value={custom_virtualenv}
onChange={this.handleFieldChange}
data={custom_virtualenvs}
defaultSelected={defaultEnv}
/>
</FormGroup>
)
)}
</ConfigContext.Consumer>
</Gallery>
<FormActionGroup
onSubmit={this.handleSubmit}
submitDisabled={!enabled}
onCancel={this.handleCancel}
/>
{error ? <div>error</div> : ''}
</Form>
</CardBody> </CardBody>
</Card> </Card>
)} )}
@@ -194,6 +106,10 @@ class OrganizationAdd extends React.Component {
} }
} }
OrganizationAdd.propTypes = {
api: PropTypes.shape().isRequired,
};
OrganizationAdd.contextTypes = { OrganizationAdd.contextTypes = {
custom_virtualenvs: PropTypes.arrayOf(PropTypes.string) custom_virtualenvs: PropTypes.arrayOf(PropTypes.string)
}; };

19
src/util/validators.js Normal file
View File

@@ -0,0 +1,19 @@
import { i18nMark } from '@lingui/react';
export function required (message) {
return value => {
if (!value.trim()) {
return message || i18nMark('This field must not be blank');
}
return undefined;
};
}
export function maxLength (max) {
return value => {
if (value.trim() > max) {
return i18nMark(`This field must not exceed ${max} characters`);
}
return undefined;
};
}