extract new LabelSelect component from JobTemplateForm

This commit is contained in:
Keith Grant 2019-09-25 15:18:30 -07:00
parent 61f6e3c4d2
commit 439727f1bd
3 changed files with 159 additions and 131 deletions

View File

@ -15,7 +15,7 @@ import {
import ContentError from '@components/ContentError';
import ContentLoading from '@components/ContentLoading';
import AnsibleSelect from '@components/AnsibleSelect';
import MultiSelect, { TagMultiSelect } from '@components/MultiSelect';
import { TagMultiSelect } from '@components/MultiSelect';
import FormActionGroup from '@components/FormActionGroup';
import FormField, { CheckboxField, FieldTooltip } from '@components/FormField';
import FormRow from '@components/FormRow';
@ -28,7 +28,8 @@ import {
InstanceGroupsLookup,
ProjectLookup,
} from '@components/Lookup';
import { JobTemplatesAPI, LabelsAPI, ProjectsAPI } from '@api';
import { JobTemplatesAPI } from '@api';
import LabelSelect from './LabelSelect';
import PlaybookSelect from './PlaybookSelect';
const GridFormGroup = styled(FormGroup)`
@ -71,17 +72,11 @@ class JobTemplateForm extends Component {
this.state = {
hasContentLoading: true,
contentError: false,
loadedLabels: [],
newLabels: [],
removedLabels: [],
project: props.template.summary_fields.project,
inventory: props.template.summary_fields.inventory,
relatedInstanceGroups: [],
allowCallbacks: !!props.template.host_config_key,
};
this.handleNewLabel = this.handleNewLabel.bind(this);
this.loadLabels = this.loadLabels.bind(this);
this.removeLabel = this.removeLabel.bind(this);
this.handleProjectValidation = this.handleProjectValidation.bind(this);
this.loadRelatedInstanceGroups = this.loadRelatedInstanceGroups.bind(this);
this.handleInstanceGroupsChange = this.handleInstanceGroupsChange.bind(
@ -92,7 +87,8 @@ class JobTemplateForm extends Component {
componentDidMount() {
const { validateField } = this.props;
this.setState({ contentError: null, hasContentLoading: true });
Promise.all([this.loadLabels(), this.loadRelatedInstanceGroups()]).then(
// TODO: determine whene LabelSelect has finished loading labels?
Promise.all([this.loadRelatedInstanceGroups()]).then(
() => {
this.setState({ hasContentLoading: false });
validateField('project');
@ -100,35 +96,6 @@ class JobTemplateForm extends Component {
);
}
async loadLabels() {
// This function assumes that the user has no more than 400
// labels. For the vast majority of users this will be more thans
// enough. This can be updated to allow more than 400 labels if we
// decide it is necessary.
let loadedLabels;
try {
const { data } = await LabelsAPI.read({
page: 1,
page_size: 200,
order_by: 'name',
});
loadedLabels = [...data.results];
if (data.next && data.next.includes('page=2')) {
const {
data: { results },
} = await LabelsAPI.read({
page: 2,
page_size: 200,
order_by: 'name',
});
loadedLabels = loadedLabels.concat(results);
}
this.setState({ loadedLabels });
} catch (err) {
this.setState({ contentError: err });
}
}
async loadRelatedInstanceGroups() {
const { template } = this.props;
if (!template.id) {
@ -145,65 +112,6 @@ class JobTemplateForm extends Component {
}
}
handleNewLabel(label) {
const { newLabels } = this.state;
const { template, setFieldValue } = this.props;
const isIncluded = newLabels.some(newLabel => newLabel.name === label.name);
if (isIncluded) {
const filteredLabels = newLabels.filter(
newLabel => newLabel.name !== label
);
this.setState({ newLabels: filteredLabels });
} else {
setFieldValue('newLabels', [
...newLabels,
{ name: label.name, associate: true, id: label.id },
]);
this.setState({
newLabels: [
...newLabels,
{
name: label.name,
associate: true,
id: label.id,
organization: template.summary_fields.inventory.organization_id,
},
],
});
}
}
removeLabel(label) {
const { removedLabels, newLabels } = this.state;
const { template, setFieldValue } = this.props;
const isAssociatedLabel = template.summary_fields.labels.results.some(
tempLabel => tempLabel.id === label.id
);
if (isAssociatedLabel) {
setFieldValue(
'removedLabels',
removedLabels.concat({
disassociate: true,
id: label.id,
})
);
this.setState({
removedLabels: removedLabels.concat({
disassociate: true,
id: label.id,
}),
});
} else {
const filteredLabels = newLabels.filter(
newLabel => newLabel.name !== label.name
);
setFieldValue('newLabels', filteredLabels);
this.setState({ newLabels: filteredLabels });
}
}
handleProjectValidation() {
const { i18n, touched } = this.props;
const { project } = this.state;
@ -244,7 +152,7 @@ class JobTemplateForm extends Component {
render() {
const {
loadedLabels,
// loadedLabels,
contentError,
hasContentLoading,
inventory,
@ -256,6 +164,7 @@ class JobTemplateForm extends Component {
handleCancel,
handleSubmit,
handleBlur,
setFieldValue,
i18n,
template,
} = this.props;
@ -406,8 +315,13 @@ class JobTemplateForm extends Component {
t`Select the playbook to be executed by this job.`
)}
/>
<PlaybookSelect projectId={form.values.project}
isValid={isValid} form={form} field={field} />
<PlaybookSelect
projectId={form.values.project}
isValid={isValid}
form={form}
field={field}
onError={err => this.setState({ contentError: err })}
/>
</FormGroup>
);
}}
@ -416,15 +330,19 @@ class JobTemplateForm extends Component {
<FormRow>
<FormGroup label={i18n._(t`Labels`)} fieldId="template-labels">
<FieldTooltip
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.`
)}
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.`)}
/>
<MultiSelect
onAddNewItem={this.handleNewLabel}
onRemoveItem={this.removeLabel}
associatedItems={template.summary_fields.labels.results}
options={loadedLabels}
<LabelSelect
initialValues={template.summary_fields.labels.results}
onNewLabelsChange={newLabels => {
setFieldValue('newLabels', newLabels);
}}
onRemovedLabelsChange={removedLabels => {
setFieldValue('removedLabels', removedLabels);
}}
onError={err => this.setState({ contentError: err })}
/>
</FormGroup>
</FormRow>
@ -485,8 +403,8 @@ class JobTemplateForm extends Component {
min="1"
label={i18n._(t`Job Slicing`)}
tooltip={i18n._(t`Divide the work done by this job template
into the specified number of job slices, each running the
same tasks against a portion of the inventory.`)}
into the specified number of job slices, each running the
same tasks against a portion of the inventory.`)}
/>
<FormField
id="template-timeout"
@ -495,8 +413,8 @@ class JobTemplateForm extends Component {
min="0"
label={i18n._(t`Timeout`)}
tooltip={i18n._(t`The amount of time (in seconds) to run
before the task is canceled. Defaults to 0 for no job
timeout.`)}
before the task is canceled. Defaults to 0 for no job
timeout.`)}
/>
<Field
name="diff_mode"
@ -528,9 +446,8 @@ class JobTemplateForm extends Component {
css="margin-top: 20px"
value={relatedInstanceGroups}
onChange={this.handleInstanceGroupsChange}
tooltip={i18n._(
t`Select the Instance Groups for this Organization to run on.`
)}
tooltip={i18n._(t`Select the Instance Groups for this Organization
to run on.`)}
/>
<Field
name="job_tags"
@ -586,9 +503,8 @@ class JobTemplateForm extends Component {
id="option-privilege-escalation"
name="become_enabled"
label={i18n._(t`Privilege Escalation`)}
tooltip={i18n._(
t`If enabled, run this playbook as an administrator.`
)}
tooltip={i18n._(t`If enabled, run this playbook as an
administrator.`)}
/>
<Checkbox
aria-label={i18n._(t`Provisioning Callbacks`)}
@ -597,11 +513,10 @@ class JobTemplateForm extends Component {
{i18n._(t`Provisioning Callbacks`)}
&nbsp;
<FieldTooltip
content={i18n._(
t`Enables creation of a provisioning callback URL. Using
the URL a host can contact BRAND_NAME and request a
configuration update using this job template.`
)}
content={i18n._(t`Enables creation of a provisioning
callback URL. Using the URL a host can contact BRAND_NAME
and request a configuration update using this job
template.`)}
/>
</span>
}
@ -615,19 +530,15 @@ class JobTemplateForm extends Component {
id="option-concurrent"
name="allow_simultaneous"
label={i18n._(t`Concurrent Jobs`)}
tooltip={i18n._(
t`If enabled, simultaneous runs of this job template will
be allowed.`
)}
tooltip={i18n._(t`If enabled, simultaneous runs of this job
template will be allowed.`)}
/>
<CheckboxField
id="option-fact-cache"
name="use_fact_cache"
label={i18n._(t`Fact Cache`)}
tooltip={i18n._(
t`If enabled, use cached facts if available and store
discovered facts in the cache.`
)}
tooltip={i18n._(t`If enabled, use cached facts if available
and store discovered facts in the cache.`)}
/>
</GridFormGroup>
<div
@ -690,6 +601,10 @@ const FormikApp = withFormik({
allow_simultaneous: template.allow_simultaneous || false,
use_fact_cache: template.use_fact_cache || false,
host_config_key: template.host_config_key || '',
addedInstanceGroups: [],
removedInstanceGroups: [],
newLabels: [],
removedLabels: [],
};
},
handleSubmit: (values, bag) => bag.props.handleSubmit(values),

View File

@ -0,0 +1,110 @@
import React, { useState, useEffect } from 'react';
import { func, arrayOf, number, shape, string } from 'prop-types';
import MultiSelect from '@components/MultiSelect';
import { LabelsAPI } from '@api';
async function loadLabelOptions(setLabels, onError) {
let labels;
try {
const { data } = await LabelsAPI.read({
page: 1,
page_size: 200,
order_by: 'name',
});
labels = data.results;
setLabels(labels);
if (data.next && data.next.includes('page=2')) {
const {
data: { results },
} = await LabelsAPI.read({
page: 2,
page_size: 200,
order_by: 'name',
});
labels = labels.concat(results);
}
setLabels(labels);
} catch (err) {
onError(err);
}
}
function LabelSelect({
initialValues,
organizationId,
onNewLabelsChange,
onRemovedLabelsChange,
onError,
}) {
const [options, setOptions] = useState([]);
// TODO: move newLabels into a prop?
const [newLabels, setNewLabels] = useState([]);
const [removedLabels, setRemovedLabels] = useState([]);
useEffect(() => {
loadLabelOptions(setOptions, onError);
}, []);
const handleNewLabel = label => {
const isIncluded = newLabels.some(l => l.name === label.name);
if (isIncluded) {
const filteredLabels = newLabels.filter(
newLabel => newLabel.name !== label
);
setNewLabels(filteredLabels);
} else {
const updatedNewLabels = newLabels.concat({
name: label.name,
associate: true,
id: label.id,
// TODO: can this be null? what happens if inventory > org id changes?
// organization: organizationId,
});
setNewLabels(updatedNewLabels);
onNewLabelsChange(updatedNewLabels);
}
};
const handleRemoveLabel = label => {
const isAssociatedLabel = initialValues.some(
l => l.id === label.id
);
if (isAssociatedLabel) {
const updatedRemovedLabels = removedLabels.concat({
id: label.id,
disassociate: true,
});
setRemovedLabels(updatedRemovedLabels);
onRemovedLabelsChange(updatedRemovedLabels);
} else {
const filteredLabels = newLabels.filter(l => l.name !== label.name);
setNewLabels(filteredLabels);
onNewLabelsChange(filteredLabels);
}
};
return (
<MultiSelect
onAddNewItem={handleNewLabel}
onRemoveItem={handleRemoveLabel}
associatedItems={initialValues}
options={options}
/>
);
}
LabelSelect.propTypes = {
initialValues: arrayOf(
shape({
id: number.isRequired,
name: string.isRequired,
})
).isRequired,
organizationId: number,
onNewLabelsChange: func.isRequired,
onRemovedLabelsChange: func.isRequired,
onError: func.isRequired,
};
LabelSelect.defaultProps = {
organizationId: null,
};
export default LabelSelect;

View File

@ -8,6 +8,9 @@ import { ProjectsAPI } from '@api';
function PlaybookSelect({ projectId, isValid, form, field, onError, i18n }) {
const [options, setOptions] = useState([]);
useEffect(() => {
if (!projectId) {
return;
}
(async () => {
try {
const { data } = await ProjectsAPI.readPlaybooks(projectId);