mirror of
https://github.com/ansible/awx.git
synced 2026-03-23 11:55:04 -02:30
extract new LabelSelect component from JobTemplateForm
This commit is contained in:
@@ -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`)}
|
||||||
|
|
||||||
<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),
|
||||||
|
|||||||
110
awx/ui_next/src/screens/Template/shared/LabelSelect.jsx
Normal file
110
awx/ui_next/src/screens/Template/shared/LabelSelect.jsx
Normal 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;
|
||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user