diff --git a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx index a0de18a39f..9c1eaab9a2 100644 --- a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx +++ b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx @@ -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.` )} /> - + this.setState({ contentError: err })} + /> ); }} @@ -416,15 +330,19 @@ class JobTemplateForm extends Component { - { + setFieldValue('newLabels', newLabels); + }} + onRemovedLabelsChange={removedLabels => { + setFieldValue('removedLabels', removedLabels); + }} + onError={err => this.setState({ contentError: err })} /> @@ -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.`)} /> } @@ -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.`)} />
bag.props.handleSubmit(values), diff --git a/awx/ui_next/src/screens/Template/shared/LabelSelect.jsx b/awx/ui_next/src/screens/Template/shared/LabelSelect.jsx new file mode 100644 index 0000000000..4959a138e8 --- /dev/null +++ b/awx/ui_next/src/screens/Template/shared/LabelSelect.jsx @@ -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 ( + + ); +} +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; diff --git a/awx/ui_next/src/screens/Template/shared/PlaybookSelect.jsx b/awx/ui_next/src/screens/Template/shared/PlaybookSelect.jsx index 7b9c89f110..3e7d03c37e 100644 --- a/awx/ui_next/src/screens/Template/shared/PlaybookSelect.jsx +++ b/awx/ui_next/src/screens/Template/shared/PlaybookSelect.jsx @@ -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);