From 222ae33be27c0f5a855c3d247305b7c25b5fccf1 Mon Sep 17 00:00:00 2001 From: MariaAga Date: Mon, 12 Jul 2021 16:35:40 +0200 Subject: [PATCH 1/3] Fixes #31492 - Add basic hosts and inputs page --- webpack/JobWizard/JobWizard.js | 23 ++++++- webpack/JobWizard/JobWizard.scss | 8 +++ webpack/JobWizard/__tests__/fixtures.js | 8 +++ .../JobWizard/__tests__/integration.test.js | 2 +- .../__tests__/AdvancedFields.test.js | 11 +--- .../CategoryAndTemplate.js | 4 +- .../steps/HostsAndInputs/SelectedChips.js | 25 +++++++ .../steps/HostsAndInputs/TemplateInputs.js | 23 +++++++ .../__tests__/SelectedChips.test.js | 37 +++++++++++ .../__tests__/TemplateInputs.test.js | 47 +++++++++++++ .../JobWizard/steps/HostsAndInputs/index.js | 66 +++++++++++++++++++ webpack/JobWizard/steps/form/Formatter.js | 19 +++--- 12 files changed, 250 insertions(+), 23 deletions(-) create mode 100644 webpack/JobWizard/steps/HostsAndInputs/SelectedChips.js create mode 100644 webpack/JobWizard/steps/HostsAndInputs/TemplateInputs.js create mode 100644 webpack/JobWizard/steps/HostsAndInputs/__tests__/SelectedChips.test.js create mode 100644 webpack/JobWizard/steps/HostsAndInputs/__tests__/TemplateInputs.test.js create mode 100644 webpack/JobWizard/steps/HostsAndInputs/index.js diff --git a/webpack/JobWizard/JobWizard.js b/webpack/JobWizard/JobWizard.js index 7ee3c9de7..3f557afa9 100644 --- a/webpack/JobWizard/JobWizard.js +++ b/webpack/JobWizard/JobWizard.js @@ -10,29 +10,41 @@ import { AdvancedFields } from './steps/AdvancedFields/AdvancedFields'; import { JOB_TEMPLATE } from './JobWizardConstants'; import { selectTemplateError } from './JobWizardSelectors'; import Schedule from './steps/Schedule/'; +import HostsAndInputs from './steps/HostsAndInputs/'; import './JobWizard.scss'; export const JobWizard = () => { const [jobTemplateID, setJobTemplateID] = useState(null); const [category, setCategory] = useState(''); const [advancedValues, setAdvancedValues] = useState({}); + const [templateValues, setTemplateValues] = useState({}); // TODO use templateValues in advanced fields - description + const [selectedHosts, setSelectedHosts] = useState(['host1', 'host2']); const dispatch = useDispatch(); const setDefaults = useCallback( ({ data: { + template_inputs, advanced_template_inputs, effective_user, job_template: { executionTimeoutInterval, description_format }, }, }) => { const advancedTemplateValues = {}; + const defaultTemplateValues = {}; + const inputs = template_inputs; const advancedInputs = advanced_template_inputs; if (advancedInputs) { advancedInputs.forEach(input => { advancedTemplateValues[input.name] = input?.default || ''; }); } + if (inputs) { + inputs.forEach(input => { + defaultTemplateValues[input.name] = input?.default || ''; + }); + } + setTemplateValues(defaultTemplateValues); setAdvancedValues(currentAdvancedValues => ({ ...currentAdvancedValues, effectiveUserValue: effective_user?.value || '', @@ -70,8 +82,15 @@ export const JobWizard = () => { ), }, { - name: __('Target Hosts'), - component:

Target Hosts

, + name: __('Target hosts and inputs'), + component: ( + + ), canJumpTo: isTemplate, }, { diff --git a/webpack/JobWizard/JobWizard.scss b/webpack/JobWizard/JobWizard.scss index 23fefa7c5..e8295cac0 100644 --- a/webpack/JobWizard/JobWizard.scss +++ b/webpack/JobWizard/JobWizard.scss @@ -1,5 +1,10 @@ .job-wizard { + .wizard-title { + margin-bottom: 25px; + } + .pf-c-wizard__main { + overflow: visible; z-index: calc( var(--pf-c-wizard__footer--ZIndex) + 1 ); // So the select box can be shown above the wizard footer @@ -41,6 +46,9 @@ } } + .hosts-chip-group { + margin-top: 8px; + } .schedule-tab { input[type='radio'], input[type='checkbox'] { diff --git a/webpack/JobWizard/__tests__/fixtures.js b/webpack/JobWizard/__tests__/fixtures.js index af3759330..2048eb421 100644 --- a/webpack/JobWizard/__tests__/fixtures.js +++ b/webpack/JobWizard/__tests__/fixtures.js @@ -93,6 +93,14 @@ export const testSetup = (selectors, api) => { jest.spyOn(selectors, 'selectJobCategories'); jest.spyOn(selectors, 'selectJobCategoriesStatus'); + jest.spyOn(selectors, 'selectTemplateInputs'); + jest.spyOn(selectors, 'selectAdvancedTemplateInputs'); + selectors.selectTemplateInputs.mockImplementation( + () => jobTemplateResponse.template_inputs + ); + selectors.selectAdvancedTemplateInputs.mockImplementation( + () => jobTemplateResponse.advanced_template_inputs + ); selectors.selectJobCategories.mockImplementation(() => jobCategories); selectors.selectJobTemplates.mockImplementation(() => [ jobTemplate, diff --git a/webpack/JobWizard/__tests__/integration.test.js b/webpack/JobWizard/__tests__/integration.test.js index 2eae60489..c1598f88c 100644 --- a/webpack/JobWizard/__tests__/integration.test.js +++ b/webpack/JobWizard/__tests__/integration.test.js @@ -63,7 +63,7 @@ describe('Job wizard fill', () => { ); const steps = [ - 'Target Hosts', + 'Target hosts and inputs', 'Advanced Fields', 'Schedule', 'Review Details', diff --git a/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js b/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js index 172f1e496..77dd9a997 100644 --- a/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js +++ b/webpack/JobWizard/steps/AdvancedFields/__tests__/AdvancedFields.test.js @@ -15,19 +15,10 @@ const store = testSetup(selectors, api); mockApi(api); jest.spyOn(selectors, 'selectEffectiveUser'); -jest.spyOn(selectors, 'selectTemplateInputs'); -jest.spyOn(selectors, 'selectAdvancedTemplateInputs'); selectors.selectEffectiveUser.mockImplementation( () => jobTemplate.effective_user ); -selectors.selectTemplateInputs.mockImplementation( - () => jobTemplate.template_inputs -); - -selectors.selectAdvancedTemplateInputs.mockImplementation( - () => jobTemplate.advanced_template_inputs -); describe('AdvancedFields', () => { it('should save data between steps for advanced fields', async () => { const wrapper = mount( @@ -72,7 +63,7 @@ describe('AdvancedFields', () => { .simulate('click'); expect(wrapper.find('.pf-c-wizard__nav-link.pf-m-current').text()).toEqual( - 'Target Hosts' + 'Target hosts and inputs' ); wrapper .find('.pf-c-wizard__nav-link') diff --git a/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js b/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js index a8e7474bf..fe7642ecb 100644 --- a/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js +++ b/webpack/JobWizard/steps/CategoryAndTemplate/CategoryAndTemplate.js @@ -40,7 +40,9 @@ export const CategoryAndTemplate = ({ const isError = !!(categoryError || allTemplatesError || templateError); return ( <> - {__('Category and Template')} + + {__('Category and Template')} + {__('All fields are required.')}
{ + const deleteItem = itemToRemove => { + setSelected(oldSelected => + oldSelected.filter(item => item !== itemToRemove) + ); + }; + return ( + + {selected.map(chip => ( + deleteItem(chip)}> + {chip} + + ))} + + ); +}; + +SelectedChips.propTypes = { + selected: PropTypes.array.isRequired, + setSelected: PropTypes.func.isRequired, +}; diff --git a/webpack/JobWizard/steps/HostsAndInputs/TemplateInputs.js b/webpack/JobWizard/steps/HostsAndInputs/TemplateInputs.js new file mode 100644 index 000000000..5d4518aef --- /dev/null +++ b/webpack/JobWizard/steps/HostsAndInputs/TemplateInputs.js @@ -0,0 +1,23 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { translate as __ } from 'foremanReact/common/I18n'; +import { formatter } from '../form/Formatter'; + +export const TemplateInputs = ({ inputs, value, setValue }) => { + if (inputs.length) + return <>{inputs?.map(input => formatter(input, value, setValue))}; + return ( +

+ {__('There are no available input fields for the selected template.')} +

+ ); +}; +TemplateInputs.propTypes = { + inputs: PropTypes.array.isRequired, + value: PropTypes.object, + setValue: PropTypes.func.isRequired, +}; + +TemplateInputs.defaultProps = { + value: {}, +}; diff --git a/webpack/JobWizard/steps/HostsAndInputs/__tests__/SelectedChips.test.js b/webpack/JobWizard/steps/HostsAndInputs/__tests__/SelectedChips.test.js new file mode 100644 index 000000000..af6a750cc --- /dev/null +++ b/webpack/JobWizard/steps/HostsAndInputs/__tests__/SelectedChips.test.js @@ -0,0 +1,37 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import { fireEvent, screen, render, act } from '@testing-library/react'; +import * as api from 'foremanReact/redux/API'; +import { JobWizard } from '../../../JobWizard'; +import * as selectors from '../../../JobWizardSelectors'; +import { testSetup, mockApi } from '../../../__tests__/fixtures'; + +const store = testSetup(selectors, api); +mockApi(api); + +describe('TemplateInputs', () => { + it('should save data between steps for template input fields', async () => { + render( + + + + ); + await act(async () => { + await fireEvent.click( + screen.getByText('Target hosts and inputs', { selector: 'button' }) + ); + }); + + expect( + screen.getAllByLabelText('host2', { selector: 'button' }) + ).toHaveLength(1); + const chip1 = screen.getByLabelText('host1', { selector: 'button' }); + fireEvent.click(chip1); + expect( + screen.queryAllByLabelText('host1', { selector: 'button' }) + ).toHaveLength(0); + expect( + screen.queryAllByLabelText('host2', { selector: 'button' }) + ).toHaveLength(1); + }); +}); diff --git a/webpack/JobWizard/steps/HostsAndInputs/__tests__/TemplateInputs.test.js b/webpack/JobWizard/steps/HostsAndInputs/__tests__/TemplateInputs.test.js new file mode 100644 index 000000000..071baece2 --- /dev/null +++ b/webpack/JobWizard/steps/HostsAndInputs/__tests__/TemplateInputs.test.js @@ -0,0 +1,47 @@ +import React from 'react'; +import { Provider } from 'react-redux'; +import { fireEvent, screen, render, act } from '@testing-library/react'; +import * as api from 'foremanReact/redux/API'; +import { JobWizard } from '../../../JobWizard'; +import * as selectors from '../../../JobWizardSelectors'; +import { testSetup, mockApi } from '../../../__tests__/fixtures'; + +const store = testSetup(selectors, api); +mockApi(api); + +describe('TemplateInputs', () => { + it('should save data between steps for template input fields', async () => { + render( + + + + ); + await act(async () => { + fireEvent.click(screen.getByText('Target hosts and inputs')); + }); + const textValue = 'I am a plain text'; + const textField = screen.getByLabelText('plain hidden', { + selector: 'textarea', + }); + + await act(async () => { + await fireEvent.change(textField, { + target: { value: textValue }, + }); + }); + expect( + screen.getByLabelText('plain hidden', { + selector: 'textarea', + }).value + ).toBe(textValue); + await act(async () => { + fireEvent.click(screen.getByText('Category and Template')); + }); + expect(screen.getAllByText('Category and Template')).toHaveLength(3); + + await act(async () => { + fireEvent.click(screen.getByText('Target hosts and inputs')); + }); + expect(textField.value).toBe(textValue); + }); +}); diff --git a/webpack/JobWizard/steps/HostsAndInputs/index.js b/webpack/JobWizard/steps/HostsAndInputs/index.js new file mode 100644 index 000000000..ad49b5902 --- /dev/null +++ b/webpack/JobWizard/steps/HostsAndInputs/index.js @@ -0,0 +1,66 @@ +import React, { useState } from 'react'; +import { Title, Button, Form, FormGroup } from '@patternfly/react-core'; +import PropTypes from 'prop-types'; +import { useSelector } from 'react-redux'; +import { translate as __ } from 'foremanReact/common/I18n'; +import { selectTemplateInputs } from '../../JobWizardSelectors'; +import { SelectField } from '../form/SelectField'; +import { SelectedChips } from './SelectedChips'; +import { TemplateInputs } from './TemplateInputs'; + +const HostsAndInputs = ({ + templateValues, + setTemplateValues, + selectedHosts, + setSelectedHosts, +}) => { + const templateInputs = useSelector(selectTemplateInputs); + const hostMethods = [ + __('Hosts'), + __('Host collection'), + __('Host group'), + __('Search query'), + ]; + const [hostMethod, setHostMethod] = useState(hostMethods[0]); + return ( + <> + + {__('Target hosts and inputs')} + + + + + + + + {__('Apply to')}{' '} + + + + + + ); +}; + +HostsAndInputs.propTypes = { + templateValues: PropTypes.object.isRequired, + setTemplateValues: PropTypes.func.isRequired, + selectedHosts: PropTypes.array.isRequired, + setSelectedHosts: PropTypes.func.isRequired, +}; + +export default HostsAndInputs; diff --git a/webpack/JobWizard/steps/form/Formatter.js b/webpack/JobWizard/steps/form/Formatter.js index 7173ad22f..b12551da2 100644 --- a/webpack/JobWizard/steps/form/Formatter.js +++ b/webpack/JobWizard/steps/form/Formatter.js @@ -22,11 +22,12 @@ const TemplateSearchField = ({ setValue({ ...values, [name]: searchQuery }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [searchQuery]); + const id = name.replace(/ /g, '-'); return ( @@ -54,16 +55,16 @@ export const formatter = (input, values, setValue) => { const { name, required, hidden_value: hidden } = input; const labelText = input.description; const value = values[name]; - + const id = name.replace(/ /g, '-'); if (isSelectType) { const options = input.options.split(/\r?\n/).map(option => option.trim()); return ( { key={name} label={name} labelIcon={helpLabel(labelText, name)} - fieldId={name} + fieldId={id} isRequired={required} >