diff --git a/src/PresentationalComponents/CreateImageWizard/WizardStepImageOutput.js b/src/PresentationalComponents/CreateImageWizard/WizardStepImageOutput.js new file mode 100644 index 00000000..7d43755b --- /dev/null +++ b/src/PresentationalComponents/CreateImageWizard/WizardStepImageOutput.js @@ -0,0 +1,40 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { Form, FormGroup, FormSelect, FormSelectOption, Title } from '@patternfly/react-core'; + +const WizardStepImageOutput = (props) => { + const releaseOptions = [ + { value: 'rhel-8', label: 'Red Hat Enterprise Linux (RHEL) 8.2' }, + ]; + const uploadOptions = [ + { value: 'aws', label: 'Amazon Web Services' }, + ]; + return ( +
+ Image output + + props.setRelease(value) } isRequired + aria-label="Select release input" id="release-select" data-testid="release-select"> + { releaseOptions.map(option => ) } + + + + props.setUpload({ type: value, options: props.upload.options }) } aria-label="Select upload destination"> + { uploadOptions.map(option => ) } + + +
+ ); +}; + +WizardStepImageOutput.propTypes = { + setRelease: PropTypes.func, + value: PropTypes.string, + upload: PropTypes.object, + setUpload: PropTypes.func, +}; + +export default WizardStepImageOutput; diff --git a/src/PresentationalComponents/CreateImageWizard/WizardStepRegistration.js b/src/PresentationalComponents/CreateImageWizard/WizardStepRegistration.js new file mode 100644 index 00000000..90791264 --- /dev/null +++ b/src/PresentationalComponents/CreateImageWizard/WizardStepRegistration.js @@ -0,0 +1,48 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { Form, FormGroup, TextInput, Radio, Title } from '@patternfly/react-core'; + +const WizardStepRegistration = (props) => { + return ( +
+ Registration + + props.setSubscribeNow(true) } + data-testid="register-now-radio-button" /> + props.setSubscribeNow(false) } + data-testid="register-later-radio-button" /> + + { props.subscribeNow && + <> + + + + + props.setSubscription(Object.assign(props.subscription, { 'activation-key': value })) } /> + + } +
+ ); +}; + +WizardStepRegistration.propTypes = { + setSubscription: PropTypes.func, + setSubscribeNow: PropTypes.func, + subscription: PropTypes.object, + subscribeNow: PropTypes.bool, + errors: PropTypes.object, +}; + +export default WizardStepRegistration; diff --git a/src/PresentationalComponents/CreateImageWizard/WizardStepReview.js b/src/PresentationalComponents/CreateImageWizard/WizardStepReview.js new file mode 100644 index 00000000..1f2e623f --- /dev/null +++ b/src/PresentationalComponents/CreateImageWizard/WizardStepReview.js @@ -0,0 +1,95 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { Alert, TextContent, Title } from '@patternfly/react-core'; +import { ExclamationCircleIcon } from '@patternfly/react-icons'; + +import './WizardStepReview.scss'; + +const WizardStepReview = (props) => { + const releaseOptions = { + 'rhel-8': 'Red Hat Enterprise Linux (RHEL) 8.2' + }; + const uploadOptions = { + aws: 'Amazon Web Services' + }; + return ( + <> + { (Object.keys(props.uploadErrors).length > 0 || + Object.keys(props.subscriptionErrors).length > 0) && + } + Create image + + + Review the information and click Create image + to create the image using the following criteria. + +

Image output

+
+
+ Release +
+
+ { releaseOptions[props.release]} +
+
+ Target environment +
+
+ { props.upload && <>{ uploadOptions[props.upload.type] } } +
+
+ { Object.entries(props.uploadErrors).length > 0 && ( +

Upload to AWS

+ )} +
+ { Object.entries(props.uploadErrors).map(([ key, error ]) => { + return ( +
+ { error.label } +
+
+ { error.value } +
+
); + })} +
+

Registration

+
+
+ Subscription +
+ { !props.subscribeNow && +
+ Register the system later +
} + { props.subscribeNow && +
+ Register the system on first boot +
} + { Object.entries(props.subscriptionErrors).map(([ key, error ]) => { + return ( +
+ { error.label } +
+
+ { error.value } +
+
); + })} +
+
+ + ); +}; + +WizardStepReview.propTypes = { + release: PropTypes.string, + upload: PropTypes.object, + subscription: PropTypes.object, + subscribeNow: PropTypes.bool, + uploadErrors: PropTypes.object, + subscriptionErrors: PropTypes.object, +}; + +export default WizardStepReview; diff --git a/src/PresentationalComponents/CreateImageWizard/WizardStepReview.scss b/src/PresentationalComponents/CreateImageWizard/WizardStepReview.scss new file mode 100644 index 00000000..0aa26bb0 --- /dev/null +++ b/src/PresentationalComponents/CreateImageWizard/WizardStepReview.scss @@ -0,0 +1,17 @@ +.error { + color: var(--pf-global--danger-color--100); + } +// Increasing margins for h3 for better spacing and readability +.pf-c-content h3 { + margin-top: var(--pf-global--spacer--xl); + margin-bottom: var(--pf-global--spacer--md); +} +// Decreasing gap between dl items for better spacing and readability +// Also setting a first column width to 25% instead of auto, +// to guarantee the same width for each section. +@media screen and (min-width: 576px) { + .pf-c-content dl { + grid-template: 1fr / 25% 1fr; + grid-gap: var(--pf-global--spacer--sm); + } +} \ No newline at end of file diff --git a/src/PresentationalComponents/CreateImageWizard/WizardStepUploadAWS.js b/src/PresentationalComponents/CreateImageWizard/WizardStepUploadAWS.js new file mode 100644 index 00000000..f74efbaf --- /dev/null +++ b/src/PresentationalComponents/CreateImageWizard/WizardStepUploadAWS.js @@ -0,0 +1,63 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { Form, FormGroup, FormSelect, TextInput, FormSelectOption, Title } from '@patternfly/react-core'; + +const WizardStepUploadAWS = (props) => { + const serviceOptions = [ + { value: 'ec2', label: 'Amazon Elastic Compute Cloud (ec2)' }, + { value: 's3', label: 'Amazon Simple Storage Service (s3)' }, + ]; + + return ( +
+ Upload to AWS + + props.setUploadOptions(Object.assign(props.upload.options, { access_key_id: value })) } /> + + + props.setUploadOptions(Object.assign(props.upload.options, { secret_access_key: value })) } /> + + + props.setUploadOptions(Object.assign(props.upload.options, { service: value })) }> + { serviceOptions.map(option => ) } + + + + props.setUploadOptions(Object.assign(props.upload.options, { region: value })) } /> + + { props.upload.options.service === 's3' && + + props.setUploadOptions(Object.assign(props.upload.options, { bucket: value })) } /> + } +
+ ); +}; + +WizardStepUploadAWS.propTypes = { + setUploadOptions: PropTypes.func, + upload: PropTypes.object, + errors: PropTypes.object, +}; + +export default WizardStepUploadAWS; diff --git a/src/SmartComponents/CreateImageWizard/CreateImageWizard.js b/src/SmartComponents/CreateImageWizard/CreateImageWizard.js index 581d6667..b524ab3e 100644 --- a/src/SmartComponents/CreateImageWizard/CreateImageWizard.js +++ b/src/SmartComponents/CreateImageWizard/CreateImageWizard.js @@ -5,249 +5,15 @@ import { connect } from 'react-redux'; import { actions } from '../redux'; import { PageHeader, PageHeaderTitle } from '@redhat-cloud-services/frontend-components'; -import { - Alert, - Flex, - FlexItem, - Form, - FormGroup, - FormSelect, - FormSelectOption, - Radio, - TextContent, - TextInput, - Wizard, -} from '@patternfly/react-core'; +import { Wizard } from '@patternfly/react-core'; -import { ExclamationCircleIcon } from '@patternfly/react-icons'; +import WizardStepImageOutput from '../../PresentationalComponents/CreateImageWizard/WizardStepImageOutput'; +import WizardStepUploadAWS from '../../PresentationalComponents/CreateImageWizard/WizardStepUploadAWS'; +import WizardStepRegistration from '../../PresentationalComponents/CreateImageWizard/WizardStepRegistration'; +import WizardStepReview from '../../PresentationalComponents/CreateImageWizard/WizardStepReview'; import api from './../../api.js'; -const ReleaseComponent = (props) => { - const options = [ - { value: 'rhel-8', label: 'Red Hat Enterprise Linux (RHEL) 8.2' }, - ]; - return ( -
- - props.setRelease(value) } - aria-label="Select release input" id="release-select" data-testid="release-select"> - { options.map(option => ) } - - -
- ); -}; - -ReleaseComponent.propTypes = { - setRelease: PropTypes.func, - value: PropTypes.string, -}; - -const AmazonUploadComponent = (props) => { - const serviceOptions = [ - { value: 'ec2', label: 'Amazon Elastic Compute Cloud (ec2)' }, - { value: 's3', label: 'Amazon Simple Storage Service (s3)' }, - ]; - - return ( - <> - - props.setUploadOptions(Object.assign(props.upload.options, { access_key_id: value })) } /> - - - props.setUploadOptions(Object.assign(props.upload.options, { secret_access_key: value })) } /> - - - props.setUploadOptions(Object.assign(props.upload.options, { service: value })) }> - { serviceOptions.map(option => ) } - - - - props.setUploadOptions(Object.assign(props.upload.options, { region: value })) } /> - - { props.upload.options.service === 's3' && - - props.setUploadOptions(Object.assign(props.upload.options, { bucket: value })) } /> - } - - ); -}; - -AmazonUploadComponent.propTypes = { - setUploadOptions: PropTypes.func, - upload: PropTypes.object, - errors: PropTypes.object, -}; - -const UploadComponent = (props) => { - const uploadTypes = [ - { value: 'aws', label: 'Amazon Machine Image (.raw)' }, - ]; - - return ( - <> -
- - props.setUpload({ type: value, options: props.upload.options }) } aria-label="Select upload destination"> - { uploadTypes.map(type => ) } - - - { props.upload.type === 'aws' && - } - - - ); -}; - -UploadComponent.propTypes = { - setUpload: PropTypes.func, - setUploadOptions: PropTypes.func, - upload: PropTypes.object, - errors: PropTypes.object, -}; - -const SubscriptionComponent = (props) => { - return ( -
- - props.setSubscribeNow(true) } - data-testid="register-now-radio-button" /> - props.setSubscribeNow(false) } - data-testid="register-later-radio-button" /> - - { props.subscribeNow && - <> - - - - - props.setSubscription(Object.assign(props.subscription, { 'activation-key': value })) } /> - - } -
- ); -}; - -SubscriptionComponent.propTypes = { - setSubscription: PropTypes.func, - setSubscribeNow: PropTypes.func, - subscription: PropTypes.object, - subscribeNow: PropTypes.bool, - errors: PropTypes.object, -}; - -const ReviewComponent = (props) => { - return ( - <> - { (Object.keys(props.uploadErrors).length > 0 || - Object.keys(props.subscriptionErrors).length > 0) && - } - -

Create image

- - Review the information and click Create image - to create the image using the following criteria. - -

Release

- - - Release - - - { props.release } - - -

Image output

- - - Destination - - - { props.upload && <>{ props.upload.type } } - - - { Object.entries(props.uploadErrors).map(([ key, error ]) => { - return ( - - { error.label } - - - { error.value } - - ); - })} -

Registration

- - - Subscription - - { !props.subscribeNow && - - Register the system later - } - { props.subscribeNow && - - Register the system on first boot - } - - { Object.entries(props.subscriptionErrors).map(([ key, error ]) => { - return ( - - { error.label } - - - { error.value } - - ); - })} -
- - ); -}; - -ReviewComponent.propTypes = { - release: PropTypes.string, - upload: PropTypes.object, - subscription: PropTypes.object, - subscribeNow: PropTypes.bool, - uploadErrors: PropTypes.object, - subscriptionErrors: PropTypes.object, -}; - class CreateImageWizard extends Component { constructor(props) { super(props); @@ -283,7 +49,7 @@ class CreateImageWizard extends Component { 'base-url': 'https://cdn.redhat.com/', insights: true }, - subscribeNow: false, + subscribeNow: true, /* errors take form of $fieldId: error */ uploadErrors: {}, subscriptionErrors: {}, @@ -431,22 +197,28 @@ class CreateImageWizard extends Component { } render() { + const StepImageOutput = { + name: 'Image output', + component: + }; + const StepUploadAWS = { + name: 'Upload to AWS', + component: + }; + const steps = [ - { - name: 'Release', - component: }, - { - name: 'Target environment', - component: }, + StepImageOutput, + ...(this.state.upload.type === 'aws' ? [ StepUploadAWS ] : []), { name: 'Registration', - component: }, { name: 'Review', - component: { // left sidebar navigation const sidebar = screen.getByRole('navigation'); - getByText(sidebar, 'Release'); - getByText(sidebar, 'Target environment'); + getByText(sidebar, 'Image output'); getByText(sidebar, 'Registration'); getByText(sidebar, 'Review'); }); }); -describe('Step Release', () => { +describe('Step Image output', () => { beforeEach(() => { const { _component, history } = renderWithReduxRouter(); historySpy = jest.spyOn(history, 'push'); // left sidebar navigation const sidebar = screen.getByRole('navigation'); - const anchor = getByText(sidebar, 'Release'); + const anchor = getByText(sidebar, 'Image output'); + screen.getByTestId('upload-destination'); // load from sidebar anchor.click(); }); - test('clicking Next loads Target environment', () => { + test('clicking Next loads Upload to AWS', () => { const [ next, , ] = verifyButtons(); next.click(); - screen.getByText('Destination'); screen.getByText('Secret access key'); }); @@ -115,16 +114,22 @@ describe('Step Release', () => { userEvent.selectOptions(release, [ 'rhel-8' ]); }); + + test('target environment is required', () => { + const destination = screen.getByTestId('upload-destination'); + expect(destination).toBeEnabled(); + expect(destination).toBeRequired(); + }); }); -describe('Step Target environment', () => { +describe('Step Upload to AWS', () => { beforeEach(() => { const { _component, history } = renderWithReduxRouter(); historySpy = jest.spyOn(history, 'push'); // left sidebar navigation const sidebar = screen.getByRole('navigation'); - const anchor = getByText(sidebar, 'Target environment'); + const anchor = getByText(sidebar, 'Upload to AWS'); // load from sidebar anchor.click(); @@ -153,10 +158,6 @@ describe('Step Target environment', () => { // change the select to enable the bucket field userEvent.selectOptions(screen.getByTestId('aws-service-select'), [ 's3' ]); - const destination = screen.getByTestId('upload-destination'); - expect(destination).toBeEnabled(); - expect(destination).toBeRequired(); - const accessKeyId = screen.getByTestId('aws-access-key'); expect(accessKeyId).toHaveValue(''); expect(accessKeyId).toBeEnabled(); @@ -182,10 +183,6 @@ describe('Step Target environment', () => { // change the select to enable the bucket field userEvent.selectOptions(screen.getByTestId('aws-service-select'), [ 's3' ]); - const destination = screen.getByTestId('upload-destination'); - expect(destination).toBeEnabled(); - expect(destination).toBeRequired(); - const accessKeyId = screen.getByTestId('aws-access-key'); expect(accessKeyId).toHaveValue(''); expect(accessKeyId).toBeEnabled(); @@ -229,11 +226,11 @@ describe('Step Registration', () => { screen.getByText('Review the information and click Create image to create the image using the following criteria.'); }); - test('clicking Back loads Target environment', () => { + test('clicking Back loads Upload to AWS', () => { const [ , back, ] = verifyButtons(); back.click(); - screen.getByText('Destination'); + screen.getByText('Access key ID'); screen.getByText('Secret access key'); }); @@ -320,13 +317,13 @@ describe('Click through all steps', () => { test('with valid values', async () => { const next = screen.getByRole('button', { name: /Next/ }); - // select release + // select image output userEvent.selectOptions(screen.getByTestId('release-select'), [ 'rhel-8' ]); + userEvent.selectOptions(screen.getByTestId('upload-destination'), [ 'aws' ]); next.click(); // select upload target - await screen.findByTestId('upload-destination'); - userEvent.selectOptions(screen.getByTestId('upload-destination'), [ 'aws' ]); + await screen.findByTestId('aws-access-key'); userEvent.type(screen.getByTestId('aws-access-key'), 'key'); userEvent.type(screen.getByTestId('aws-secret-access-key'), 'secret'); userEvent.selectOptions(screen.getByTestId('aws-service-select'), [ 's3' ]); @@ -342,10 +339,10 @@ describe('Click through all steps', () => { userEvent.type(screen.getByTestId('subscription-activation'), '1234567890'); next.click(); + // review await screen. findByText('Review the information and click Create image to create the image using the following criteria.'); - await screen.findByText('rhel-8'); - await screen.findByText('aws'); + await screen.findByText('Amazon Web Services'); await screen.findByText('Register the system on first boot'); // mock the backend API @@ -369,11 +366,9 @@ describe('Click through all steps', () => { // select release userEvent.selectOptions(screen.getByTestId('release-select'), [ 'rhel-8' ]); + userEvent.selectOptions(screen.getByTestId('upload-destination'), [ 'aws' ]); next.click(); - // select upload target - await screen.findByTestId('upload-destination'); - userEvent.selectOptions(screen.getByTestId('upload-destination'), [ 'aws' ]); // leave AWS access keys empty userEvent.selectOptions(screen.getByTestId('aws-service-select'), [ 's3' ]); userEvent.clear(screen.getByTestId('aws-region')); @@ -389,8 +384,7 @@ describe('Click through all steps', () => { await screen. findByText('Review the information and click Create image to create the image using the following criteria.'); - await screen.findByText('rhel-8'); - await screen.findByText('aws'); + await screen.findByText('Amazon Web Services'); await screen.findByText('Register the system on first boot'); const errorMessages = await screen.findAllByText('A value is required');