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 ContentError from '@components/ContentError';
import ContentLoading from '@components/ContentLoading'; import ContentLoading from '@components/ContentLoading';
import AnsibleSelect from '@components/AnsibleSelect'; import AnsibleSelect from '@components/AnsibleSelect';
import MultiSelect, { TagMultiSelect } from '@components/MultiSelect'; import { TagMultiSelect } from '@components/MultiSelect';
import FormActionGroup from '@components/FormActionGroup'; import FormActionGroup from '@components/FormActionGroup';
import FormField, { CheckboxField, FieldTooltip } from '@components/FormField'; import FormField, { CheckboxField, FieldTooltip } from '@components/FormField';
import FormRow from '@components/FormRow'; import FormRow from '@components/FormRow';
@@ -28,7 +28,8 @@ import {
InstanceGroupsLookup, InstanceGroupsLookup,
ProjectLookup, ProjectLookup,
} from '@components/Lookup'; } from '@components/Lookup';
import { JobTemplatesAPI, LabelsAPI, ProjectsAPI } from '@api'; import { JobTemplatesAPI } from '@api';
import LabelSelect from './LabelSelect';
import PlaybookSelect from './PlaybookSelect'; import PlaybookSelect from './PlaybookSelect';
const GridFormGroup = styled(FormGroup)` const GridFormGroup = styled(FormGroup)`
@@ -71,17 +72,11 @@ class JobTemplateForm extends Component {
this.state = { this.state = {
hasContentLoading: true, hasContentLoading: true,
contentError: false, contentError: false,
loadedLabels: [],
newLabels: [],
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,
relatedInstanceGroups: [], relatedInstanceGroups: [],
allowCallbacks: !!props.template.host_config_key, 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.handleProjectValidation = this.handleProjectValidation.bind(this);
this.loadRelatedInstanceGroups = this.loadRelatedInstanceGroups.bind(this); this.loadRelatedInstanceGroups = this.loadRelatedInstanceGroups.bind(this);
this.handleInstanceGroupsChange = this.handleInstanceGroupsChange.bind( this.handleInstanceGroupsChange = this.handleInstanceGroupsChange.bind(
@@ -92,7 +87,8 @@ class JobTemplateForm extends Component {
componentDidMount() { componentDidMount() {
const { validateField } = this.props; const { validateField } = this.props;
this.setState({ contentError: null, hasContentLoading: true }); 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 }); this.setState({ hasContentLoading: false });
validateField('project'); 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() { async loadRelatedInstanceGroups() {
const { template } = this.props; const { template } = this.props;
if (!template.id) { 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() { handleProjectValidation() {
const { i18n, touched } = this.props; const { i18n, touched } = this.props;
const { project } = this.state; const { project } = this.state;
@@ -244,7 +152,7 @@ class JobTemplateForm extends Component {
render() { render() {
const { const {
loadedLabels, // loadedLabels,
contentError, contentError,
hasContentLoading, hasContentLoading,
inventory, inventory,
@@ -256,6 +164,7 @@ class JobTemplateForm extends Component {
handleCancel, handleCancel,
handleSubmit, handleSubmit,
handleBlur, handleBlur,
setFieldValue,
i18n, i18n,
template, template,
} = this.props; } = this.props;
@@ -406,8 +315,13 @@ class JobTemplateForm extends Component {
t`Select the playbook to be executed by this job.` t`Select the playbook to be executed by this job.`
)} )}
/> />
<PlaybookSelect projectId={form.values.project} <PlaybookSelect
isValid={isValid} form={form} field={field} /> projectId={form.values.project}
isValid={isValid}
form={form}
field={field}
onError={err => this.setState({ contentError: err })}
/>
</FormGroup> </FormGroup>
); );
}} }}
@@ -416,15 +330,19 @@ class JobTemplateForm extends Component {
<FormRow> <FormRow>
<FormGroup label={i18n._(t`Labels`)} fieldId="template-labels"> <FormGroup label={i18n._(t`Labels`)} fieldId="template-labels">
<FieldTooltip <FieldTooltip
content={i18n._( content={i18n._(t`Optional labels that describe this job template,
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.` such as 'dev' or 'test'. Labels can be used to group and filter
)} job templates and completed jobs.`)}
/> />
<MultiSelect <LabelSelect
onAddNewItem={this.handleNewLabel} initialValues={template.summary_fields.labels.results}
onRemoveItem={this.removeLabel} onNewLabelsChange={newLabels => {
associatedItems={template.summary_fields.labels.results} setFieldValue('newLabels', newLabels);
options={loadedLabels} }}
onRemovedLabelsChange={removedLabels => {
setFieldValue('removedLabels', removedLabels);
}}
onError={err => this.setState({ contentError: err })}
/> />
</FormGroup> </FormGroup>
</FormRow> </FormRow>
@@ -485,8 +403,8 @@ class JobTemplateForm extends Component {
min="1" min="1"
label={i18n._(t`Job Slicing`)} label={i18n._(t`Job Slicing`)}
tooltip={i18n._(t`Divide the work done by this job template tooltip={i18n._(t`Divide the work done by this job template
into the specified number of job slices, each running the into the specified number of job slices, each running the
same tasks against a portion of the inventory.`)} same tasks against a portion of the inventory.`)}
/> />
<FormField <FormField
id="template-timeout" id="template-timeout"
@@ -495,8 +413,8 @@ class JobTemplateForm extends Component {
min="0" min="0"
label={i18n._(t`Timeout`)} label={i18n._(t`Timeout`)}
tooltip={i18n._(t`The amount of time (in seconds) to run tooltip={i18n._(t`The amount of time (in seconds) to run
before the task is canceled. Defaults to 0 for no job before the task is canceled. Defaults to 0 for no job
timeout.`)} timeout.`)}
/> />
<Field <Field
name="diff_mode" name="diff_mode"
@@ -528,9 +446,8 @@ class JobTemplateForm extends Component {
css="margin-top: 20px" css="margin-top: 20px"
value={relatedInstanceGroups} value={relatedInstanceGroups}
onChange={this.handleInstanceGroupsChange} onChange={this.handleInstanceGroupsChange}
tooltip={i18n._( tooltip={i18n._(t`Select the Instance Groups for this Organization
t`Select the Instance Groups for this Organization to run on.` to run on.`)}
)}
/> />
<Field <Field
name="job_tags" name="job_tags"
@@ -586,9 +503,8 @@ class JobTemplateForm extends Component {
id="option-privilege-escalation" id="option-privilege-escalation"
name="become_enabled" name="become_enabled"
label={i18n._(t`Privilege Escalation`)} label={i18n._(t`Privilege Escalation`)}
tooltip={i18n._( tooltip={i18n._(t`If enabled, run this playbook as an
t`If enabled, run this playbook as an administrator.` administrator.`)}
)}
/> />
<Checkbox <Checkbox
aria-label={i18n._(t`Provisioning Callbacks`)} aria-label={i18n._(t`Provisioning Callbacks`)}
@@ -597,11 +513,10 @@ class JobTemplateForm extends Component {
{i18n._(t`Provisioning Callbacks`)} {i18n._(t`Provisioning Callbacks`)}
&nbsp; &nbsp;
<FieldTooltip <FieldTooltip
content={i18n._( content={i18n._(t`Enables creation of a provisioning
t`Enables creation of a provisioning callback URL. Using callback URL. Using the URL a host can contact BRAND_NAME
the URL a host can contact BRAND_NAME and request a and request a configuration update using this job
configuration update using this job template.` template.`)}
)}
/> />
</span> </span>
} }
@@ -615,19 +530,15 @@ class JobTemplateForm extends Component {
id="option-concurrent" id="option-concurrent"
name="allow_simultaneous" name="allow_simultaneous"
label={i18n._(t`Concurrent Jobs`)} label={i18n._(t`Concurrent Jobs`)}
tooltip={i18n._( tooltip={i18n._(t`If enabled, simultaneous runs of this job
t`If enabled, simultaneous runs of this job template will template will be allowed.`)}
be allowed.`
)}
/> />
<CheckboxField <CheckboxField
id="option-fact-cache" id="option-fact-cache"
name="use_fact_cache" name="use_fact_cache"
label={i18n._(t`Fact Cache`)} label={i18n._(t`Fact Cache`)}
tooltip={i18n._( tooltip={i18n._(t`If enabled, use cached facts if available
t`If enabled, use cached facts if available and store and store discovered facts in the cache.`)}
discovered facts in the cache.`
)}
/> />
</GridFormGroup> </GridFormGroup>
<div <div
@@ -690,6 +601,10 @@ const FormikApp = withFormik({
allow_simultaneous: template.allow_simultaneous || false, allow_simultaneous: template.allow_simultaneous || false,
use_fact_cache: template.use_fact_cache || false, use_fact_cache: template.use_fact_cache || false,
host_config_key: template.host_config_key || '', host_config_key: template.host_config_key || '',
addedInstanceGroups: [],
removedInstanceGroups: [],
newLabels: [],
removedLabels: [],
}; };
}, },
handleSubmit: (values, bag) => bag.props.handleSubmit(values), 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 }) { function PlaybookSelect({ projectId, isValid, form, field, onError, i18n }) {
const [options, setOptions] = useState([]); const [options, setOptions] = useState([]);
useEffect(() => { useEffect(() => {
if (!projectId) {
return;
}
(async () => { (async () => {
try { try {
const { data } = await ProjectsAPI.readPlaybooks(projectId); const { data } = await ProjectsAPI.readPlaybooks(projectId);