diff --git a/awx/ui_next/src/components/FormField/CheckboxField.jsx b/awx/ui_next/src/components/FormField/CheckboxField.jsx index 5d702a38f9..2f97e0dff5 100644 --- a/awx/ui_next/src/components/FormField/CheckboxField.jsx +++ b/awx/ui_next/src/components/FormField/CheckboxField.jsx @@ -16,6 +16,7 @@ function CheckboxField({ id, name, label, tooltip, validate, ...rest }) { validate={validate} render={({ field }) => ( {label} diff --git a/awx/ui_next/src/components/FormField/FormField.jsx b/awx/ui_next/src/components/FormField/FormField.jsx index 999ccd531a..3bd6370c3c 100644 --- a/awx/ui_next/src/components/FormField/FormField.jsx +++ b/awx/ui_next/src/components/FormField/FormField.jsx @@ -57,7 +57,7 @@ FormField.propTypes = { type: PropTypes.string, validate: PropTypes.func, isRequired: PropTypes.bool, - tooltip: PropTypes.string, + tooltip: PropTypes.node, }; FormField.defaultProps = { diff --git a/awx/ui_next/src/components/Lookup/InstanceGroupsLookup.jsx b/awx/ui_next/src/components/Lookup/InstanceGroupsLookup.jsx index a85e184f76..0cce199bc2 100644 --- a/awx/ui_next/src/components/Lookup/InstanceGroupsLookup.jsx +++ b/awx/ui_next/src/components/Lookup/InstanceGroupsLookup.jsx @@ -12,10 +12,11 @@ const getInstanceGroups = async params => InstanceGroupsAPI.read(params); class InstanceGroupsLookup extends React.Component { render() { - const { value, tooltip, onChange, i18n } = this.props; + const { value, tooltip, onChange, className, i18n } = this.props; return ( {i18n._(t`Instance Groups`)}{' '} diff --git a/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.test.jsx b/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.test.jsx index 682d29b01a..1020dc6901 100644 --- a/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.test.jsx +++ b/awx/ui_next/src/screens/Template/JobTemplateEdit/JobTemplateEdit.test.jsx @@ -92,6 +92,32 @@ const mockRelatedProjectPlaybooks = [ 'vault.yml', ]; +const mockInstanceGroups = [ + { + id: 1, + type: 'instance_group', + url: '/api/v2/instance_groups/1/', + related: { + jobs: '/api/v2/instance_groups/1/jobs/', + instances: '/api/v2/instance_groups/1/instances/', + }, + name: 'tower', + capacity: 59, + committed_capacity: 0, + consumed_capacity: 0, + percent_capacity_remaining: 100.0, + jobs_running: 0, + jobs_total: 3, + instances: 1, + controller: null, + is_controller: false, + is_isolated: false, + policy_instance_percentage: 100, + policy_instance_minimum: 0, + policy_instance_list: [], + }, +]; + JobTemplatesAPI.readCredentials.mockResolvedValue({ data: mockRelatedCredentials, }); @@ -101,12 +127,25 @@ ProjectsAPI.readPlaybooks.mockResolvedValue({ LabelsAPI.read.mockResolvedValue({ data: { results: [] } }); describe('', () => { - test('initially renders successfully', async done => { + beforeEach(() => { + LabelsAPI.read.mockResolvedValue({ data: { results: [] } }); + JobTemplatesAPI.readCredentials.mockResolvedValue({ + data: mockRelatedCredentials, + }); + JobTemplatesAPI.readInstanceGroups.mockReturnValue({ + data: { results: mockInstanceGroups }, + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('initially renders successfully', async () => { const wrapper = mountWithContexts( ); await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0); - done(); }); test('handleSubmit should call api update', async done => { diff --git a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx index 2254e95b83..4b078e4308 100644 --- a/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx +++ b/awx/ui_next/src/screens/Template/shared/JobTemplateForm.jsx @@ -157,23 +157,6 @@ class JobTemplateForm extends Component { newLabel => newLabel.name !== label ); this.setState({ newLabels: filteredLabels }); - } else if (typeof label === 'string') { - setFieldValue('newLabels', [ - ...newLabels, - { - name: label, - organization: template.summary_fields.inventory.organization_id, - }, - ]); - this.setState({ - newLabels: [ - ...newLabels, - { - name: label, - organization: template.summary_fields.inventory.organization_id, - }, - ], - }); } else { setFieldValue('newLabels', [ ...newLabels, @@ -182,7 +165,12 @@ class JobTemplateForm extends Component { this.setState({ newLabels: [ ...newLabels, - { name: label.name, associate: true, id: label.id }, + { + name: label.name, + associate: true, + id: label.id, + organization: template.summary_fields.inventory.organization_id, + }, ], }); } @@ -311,6 +299,12 @@ class JobTemplateForm extends Component { { value: '3', key: '3', label: i18n._(t`3 (Debug)`) }, { value: '4', key: '4', label: i18n._(t`4 (Connection Debug)`) }, ]; + let callbackUrl; + if (template && template.related) { + const { origin } = document.location; + const path = template.related.callback || `${template.url}callback`; + callbackUrl = `${origin}${path}`; + } if (hasContentLoading) { return ( @@ -477,6 +471,7 @@ class JobTemplateForm extends Component { id="template-forks" name="forks" type="number" + min="0" label={i18n._(t`Forks`)} tooltip={ @@ -568,6 +563,7 @@ class JobTemplateForm extends Component { /> ( ( )} /> - + @@ -677,19 +680,22 @@ class JobTemplateForm extends Component {
- - - + {callbackUrl && ( + + + + )} ', () => { labels: { results: [{ name: 'Sushi', id: 1 }, { name: 'Major', id: 2 }] }, }, }; + const mockInstanceGroups = [ + { + id: 1, + type: 'instance_group', + url: '/api/v2/instance_groups/1/', + related: { + jobs: '/api/v2/instance_groups/1/jobs/', + instances: '/api/v2/instance_groups/1/instances/', + }, + name: 'tower', + capacity: 59, + committed_capacity: 0, + consumed_capacity: 0, + percent_capacity_remaining: 100.0, + jobs_running: 0, + jobs_total: 3, + instances: 1, + controller: null, + is_controller: false, + is_isolated: false, + policy_instance_percentage: 100, + policy_instance_minimum: 0, + policy_instance_list: [], + }, + ]; beforeEach(() => { LabelsAPI.read.mockReturnValue({ data: mockData.summary_fields.labels, }); + JobTemplatesAPI.readInstanceGroups.mockReturnValue({ + data: { results: mockInstanceGroups }, + }); }); afterEach(() => { jest.clearAllMocks(); }); - test('initially renders successfully', async done => { + test('should render labels MultiSelect', async () => { const wrapper = mountWithContexts( ', () => { handleCancel={jest.fn()} /> ); - - await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0); + await waitForElement(wrapper, 'Form', el => el.length === 0); expect(LabelsAPI.read).toHaveBeenCalled(); + expect(JobTemplatesAPI.readInstanceGroups).toHaveBeenCalled(); + wrapper.update(); expect( wrapper - .find('FormGroup[fieldId="template-labels"] MultiSelect Chip') - .first() - .text() - ).toEqual('Sushi'); - done(); + .find('FormGroup[fieldId="template-labels"] MultiSelect') + .prop('associatedItems') + ).toEqual(mockData.summary_fields.labels.results); }); - test('should update form values on input changes', async done => { + test('should update form values on input changes', async () => { const wrapper = mountWithContexts( ', () => { target: { value: 'new baz type', name: 'playbook' }, }); expect(form.state('values').playbook).toEqual('new baz type'); - done(); }); - test('should call handleSubmit when Submit button is clicked', async done => { + test('should call handleSubmit when Submit button is clicked', async () => { const handleSubmit = jest.fn(); const wrapper = mountWithContexts( ', () => { wrapper.find('button[aria-label="Save"]').simulate('click'); await sleep(1); expect(handleSubmit).toBeCalled(); - done(); }); - test('should call handleCancel when Cancel button is clicked', async done => { + test('should call handleCancel when Cancel button is clicked', async () => { const handleCancel = jest.fn(); const wrapper = mountWithContexts( ', () => { expect(handleCancel).not.toHaveBeenCalled(); wrapper.find('button[aria-label="Cancel"]').prop('onClick')(); expect(handleCancel).toBeCalled(); - done(); }); - test('should call loadRelatedProjectPlaybooks when project value changes', async done => { + test('should call loadRelatedProjectPlaybooks when project value changes', async () => { const loadRelatedProjectPlaybooks = jest.spyOn( _JobTemplateForm.prototype, 'loadRelatedProjectPlaybooks' @@ -150,15 +174,10 @@ describe('', () => { name: 'project', }); expect(loadRelatedProjectPlaybooks).toHaveBeenCalledWith(10); - done(); }); - test('handleNewLabel should arrange new labels properly', async done => { - const handleNewLabel = jest.spyOn( - _JobTemplateForm.prototype, - 'handleNewLabel' - ); - const event = { key: 'Enter', preventDefault: () => {} }; + test('handleNewLabel should arrange new labels properly', async () => { + const event = { key: 'Enter' }; const wrapper = mountWithContexts( ', () => { /> ); await waitForElement(wrapper, 'EmptyStateBody', el => el.length === 0); - const multiSelect = wrapper.find('MultiSelect'); + const multiSelect = wrapper.find( + 'FormGroup[fieldId="template-labels"] MultiSelect' + ); const component = wrapper.find('JobTemplateForm'); wrapper.setState({ newLabels: [], loadedLabels: [], removedLabels: [] }); multiSelect.setState({ input: 'Foo' }); - component.find('input[aria-label="labels"]').prop('onKeyDown')(event); - expect(handleNewLabel).toHaveBeenCalledWith('Foo'); + component + .find('FormGroup[fieldId="template-labels"] input[aria-label="labels"]') + .prop('onKeyDown')(event); component.instance().handleNewLabel({ name: 'Bar', id: 2 }); - expect(component.state().newLabels).toEqual([ - { name: 'Foo', organization: 1 }, - { associate: true, id: 2, name: 'Bar' }, - ]); - done(); + const newLabels = component.state('newLabels'); + expect(newLabels).toHaveLength(2); + expect(newLabels[0].name).toEqual('Foo'); + expect(newLabels[0].organization).toEqual(1); }); - test('disassociateLabel should arrange new labels properly', async done => { + + test('disassociateLabel should arrange new labels properly', async () => { const wrapper = mountWithContexts( ', () => { component.instance().removeLabel({ name: 'Sushi', id: 1 }); expect(component.state().newLabels.length).toBe(0); expect(component.state().removedLabels.length).toBe(1); - done(); }); });