Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #31492 - Add basic hosts and inputs page #556

Merged
merged 3 commits into from Aug 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
58 changes: 40 additions & 18 deletions webpack/JobWizard/JobWizard.js
Expand Up @@ -3,43 +3,58 @@ import React, { useState, useEffect, useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Wizard } from '@patternfly/react-core';
import { get } from 'foremanReact/redux/API';
import { translate as __ } from 'foremanReact/common/I18n';
import history from 'foremanReact/history';
import CategoryAndTemplate from './steps/CategoryAndTemplate/';
import { AdvancedFields } from './steps/AdvancedFields/AdvancedFields';
import { JOB_TEMPLATE } from './JobWizardConstants';
import { JOB_TEMPLATE, WIZARD_TITLES } 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 https://github.com/theforeman/foreman_remote_execution/pull/605
const [selectedHosts, setSelectedHosts] = useState(['host1', 'host2']);
MariaAga marked this conversation as resolved.
Show resolved Hide resolved
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) {
setTemplateValues(() => {
inputs.forEach(input => {
defaultTemplateValues[input.name] = input?.default || '';
});
return defaultTemplateValues;
});
}
setAdvancedValues(currentAdvancedValues => ({
...currentAdvancedValues,
effectiveUserValue: effective_user?.value || '',
timeoutToKill: executionTimeoutInterval || '',
templateValues: advancedTemplateValues,
description: description_format || '',
}));
setAdvancedValues(currentAdvancedValues => {
if (advancedInputs) {
advancedInputs.forEach(input => {
advancedTemplateValues[input.name] = input?.default || '';
});
}
return {
...currentAdvancedValues,
effectiveUserValue: effective_user?.value || '',
timeoutToKill: executionTimeoutInterval || '',
templateValues: advancedTemplateValues,
description: description_format || '',
};
});
},
[]
);
Expand All @@ -59,7 +74,7 @@ export const JobWizard = () => {
const isTemplate = !templateError && !!jobTemplateID;
const steps = [
{
name: __('Category and Template'),
name: WIZARD_TITLES.categoryAndTemplate,
component: (
<CategoryAndTemplate
jobTemplate={jobTemplateID}
Expand All @@ -70,12 +85,19 @@ export const JobWizard = () => {
),
},
{
name: __('Target Hosts'),
component: <p>Target Hosts</p>,
name: WIZARD_TITLES.hostsAndInputs,
component: (
<HostsAndInputs
templateValues={templateValues}
setTemplateValues={setTemplateValues}
selectedHosts={selectedHosts}
setSelectedHosts={setSelectedHosts}
/>
),
canJumpTo: isTemplate,
},
{
name: __('Advanced Fields'),
name: WIZARD_TITLES.advanced,
component: (
<AdvancedFields
advancedValues={advancedValues}
Expand All @@ -91,12 +113,12 @@ export const JobWizard = () => {
canJumpTo: isTemplate,
},
{
name: __('Schedule'),
name: WIZARD_TITLES.schedule,
component: <Schedule />,
canJumpTo: isTemplate,
},
{
name: __('Review Details'),
name: WIZARD_TITLES.review,
component: <p>Review Details</p>,
nextButtonText: 'Run',
canJumpTo: isTemplate,
Expand Down
12 changes: 12 additions & 0 deletions 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
Expand Down Expand Up @@ -41,6 +46,9 @@
}
}

.hosts-chip-group {
margin-top: 8px;
}
.schedule-tab {
input[type='radio'],
input[type='checkbox'] {
Expand All @@ -50,4 +58,8 @@
text-align: start;
}
}
textarea {
min-height: 40px;
min-width: 100px;
}
}
8 changes: 8 additions & 0 deletions webpack/JobWizard/JobWizardConstants.js
Expand Up @@ -14,3 +14,11 @@ export const repeatTypes = {
daily: __('Daily'),
hourly: __('Hourly'),
};

export const WIZARD_TITLES = {
categoryAndTemplate: __('Category and Template'),
hostsAndInputs: __('Target hosts and inputs'),
advanced: __('Advanced Fields'),
schedule: __('Schedule'),
review: __('Review Details'),
};
8 changes: 8 additions & 0 deletions webpack/JobWizard/__tests__/fixtures.js
Expand Up @@ -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,
Expand Down
10 changes: 3 additions & 7 deletions webpack/JobWizard/__tests__/integration.test.js
Expand Up @@ -5,6 +5,7 @@ import { render, fireEvent, screen, act } from '@testing-library/react';
import * as api from 'foremanReact/redux/API';
import { JobWizard } from '../JobWizard';
import * as selectors from '../JobWizardSelectors';
import { WIZARD_TITLES } from '../JobWizardConstants';
import {
testSetup,
mockApi,
Expand Down Expand Up @@ -62,13 +63,8 @@ describe('Job wizard fill', () => {
<JobWizard />
</Provider>
);
const steps = [
'Target Hosts',
'Advanced Fields',
'Schedule',
'Review Details',
'Category and Template',
];
const titles = Object.values(WIZARD_TITLES);
const steps = [titles[1], titles[0], ...titles.slice(2)]; // the first title is selected at the beggining
// eslint-disable-next-line no-unused-vars
for await (const step of steps) {
const stepSelector = screen.getByText(step);
Expand Down
12 changes: 7 additions & 5 deletions webpack/JobWizard/steps/AdvancedFields/AdvancedFields.js
@@ -1,8 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import { Title, Form } from '@patternfly/react-core';
import { translate as __ } from 'foremanReact/common/I18n';
import { Form } from '@patternfly/react-core';
import {
selectEffectiveUser,
selectAdvancedTemplateInputs,
Expand All @@ -19,16 +18,19 @@ import {
TemplateInputsFields,
} from './Fields';
import { DescriptionField } from './DescriptionField';
import { WIZARD_TITLES } from '../../JobWizardConstants';
import { WizardTitle } from '../form/WizardTitle';

export const AdvancedFields = ({ advancedValues, setAdvancedValues }) => {
const effectiveUser = useSelector(selectEffectiveUser);
const advancedTemplateInputs = useSelector(selectAdvancedTemplateInputs);
const templateInputs = useSelector(selectTemplateInputs);
return (
<>
<Title headingLevel="h2" className="advanced-fields-title">
{__('Advanced Fields')}
</Title>
<WizardTitle
title={WIZARD_TITLES.advanced}
className="advanced-fields-title"
/>
<Form id="advanced-fields-job-template" autoComplete="off">
<TemplateInputsFields
inputs={advancedTemplateInputs}
Expand Down
Expand Up @@ -10,24 +10,16 @@ import {
testSetup,
mockApi,
} from '../../../__tests__/fixtures';
import { WIZARD_TITLES } from '../../../JobWizardConstants';

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(
Expand Down Expand Up @@ -72,7 +64,7 @@ describe('AdvancedFields', () => {
.simulate('click');

expect(wrapper.find('.pf-c-wizard__nav-link.pf-m-current').text()).toEqual(
'Target Hosts'
'Target hosts and inputs'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should it be WIZARD_TITLES.hostsAndInputs ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We usually dont use consts in tests. I think this could cause issues if there is an issue with the constant, for example if it's undefined or has a typo.

);
wrapper
.find('.pf-c-wizard__nav-link')
Expand All @@ -91,7 +83,7 @@ describe('AdvancedFields', () => {
</Provider>
);
await act(async () => {
fireEvent.click(screen.getByText('Advanced Fields'));
fireEvent.click(screen.getByText(WIZARD_TITLES.advanced));
});
const searchValue = 'search test';
const textValue = 'I am a text';
Expand All @@ -108,7 +100,7 @@ describe('AdvancedFields', () => {
fireEvent.click(selectField);
await act(async () => {
await fireEvent.click(screen.getByText('option 2'));
fireEvent.click(screen.getAllByText('Advanced Fields')[0]); // to remove focus
fireEvent.click(screen.getAllByText(WIZARD_TITLES.advanced)[0]); // to remove focus
await fireEvent.change(textField, {
target: { value: textValue },
});
Expand All @@ -128,9 +120,11 @@ describe('AdvancedFields', () => {
expect(searchField.value).toBe(searchValue);
expect(dateField.value).toBe(dateValue);
await act(async () => {
fireEvent.click(screen.getByText('Category and Template'));
fireEvent.click(screen.getByText(WIZARD_TITLES.categoryAndTemplate));
});
expect(screen.getAllByText('Category and Template')).toHaveLength(3);
expect(screen.getAllByText(WIZARD_TITLES.categoryAndTemplate)).toHaveLength(
3
);

await act(async () => {
fireEvent.click(screen.getByText('Advanced Fields'));
Expand Down
@@ -1,9 +1,11 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Title, Text, TextVariants, Form, Alert } from '@patternfly/react-core';
import { Text, TextVariants, Form, Alert } from '@patternfly/react-core';
import { translate as __ } from 'foremanReact/common/I18n';
import { SelectField } from '../form/SelectField';
import { GroupedSelectField } from '../form/GroupedSelectField';
import { WizardTitle } from '../form/WizardTitle';
import { WIZARD_TITLES } from '../../JobWizardConstants';

export const CategoryAndTemplate = ({
jobCategories,
Expand Down Expand Up @@ -40,7 +42,7 @@ export const CategoryAndTemplate = ({
const isError = !!(categoryError || allTemplatesError || templateError);
return (
<>
<Title headingLevel="h2">{__('Category and Template')}</Title>
<WizardTitle title={WIZARD_TITLES.categoryAndTemplate} />
<Text component={TextVariants.p}>{__('All fields are required.')}</Text>
<Form>
<SelectField
Expand Down
Expand Up @@ -5,6 +5,7 @@ import * as api from 'foremanReact/redux/API';
import { JobWizard } from '../../JobWizard';
import * as selectors from '../../JobWizardSelectors';
import { testSetup, mockApi } from '../../__tests__/fixtures';
import { WIZARD_TITLES } from '../../JobWizardConstants';

const store = testSetup(selectors, api);
mockApi(api);
Expand Down Expand Up @@ -32,7 +33,7 @@ describe('Category And Template', () => {
await act(async () => {
await fireEvent.click(screen.getByText('Puppet'));
});
fireEvent.click(screen.getAllByText('Category and Template')[0]); // to remove focus
fireEvent.click(screen.getAllByText(WIZARD_TITLES.categoryAndTemplate)[0]); // to remove focus
expect(
screen.queryAllByLabelText('Ansible Commands', { selector: 'button' })
).toHaveLength(0);
Expand All @@ -47,7 +48,7 @@ describe('Category And Template', () => {
await act(async () => {
await fireEvent.click(screen.getByText('template2'));
});
fireEvent.click(screen.getAllByText('Category and Template')[0]); // to remove focus
fireEvent.click(screen.getAllByText(WIZARD_TITLES.categoryAndTemplate)[0]); // to remove focus
expect(
screen.queryAllByDisplayValue('template1', { selector: 'button' })
).toHaveLength(0);
Expand Down
25 changes: 25 additions & 0 deletions webpack/JobWizard/steps/HostsAndInputs/SelectedChips.js
@@ -0,0 +1,25 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Chip, ChipGroup } from '@patternfly/react-core';

export const SelectedChips = ({ selected, setSelected }) => {
const deleteItem = itemToRemove => {
setSelected(oldSelected =>
oldSelected.filter(item => item !== itemToRemove)
);
};
return (
<ChipGroup className="hosts-chip-group">
{selected.map(chip => (
<Chip key={chip} id={chip} onClick={() => deleteItem(chip)}>
{chip}
</Chip>
))}
</ChipGroup>
);
};

SelectedChips.propTypes = {
selected: PropTypes.array.isRequired,
setSelected: PropTypes.func.isRequired,
};
23 changes: 23 additions & 0 deletions 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 (
<p className="gray-text">
{__('There are no available input fields for the selected template.')}
</p>
);
};
TemplateInputs.propTypes = {
inputs: PropTypes.array.isRequired,
value: PropTypes.object,
setValue: PropTypes.func.isRequired,
};

TemplateInputs.defaultProps = {
value: {},
};