Add initial branching

Add DDF package and include initial branching based on text fields
This commit is contained in:
Karel Hala 2021-05-27 17:12:35 +02:00 committed by Sanne Raymaekers
parent 24ce4fc03e
commit 06d4fd718b
4 changed files with 519 additions and 363 deletions

View file

@ -4,6 +4,8 @@
"private": false,
"dependencies": {
"@babel/runtime": "7.14.6",
"@data-driven-forms/pf4-component-mapper": "^3.6.4",
"@data-driven-forms/react-form-renderer": "^3.6.4",
"@patternfly/patternfly": "4.115.2",
"@patternfly/react-core": "4.135.0",
"@patternfly/react-table": "4.29.0",
@ -47,8 +49,8 @@
"@babel/preset-flow": "7.8.3",
"@babel/preset-react": "7.8.3",
"@redhat-cloud-services/frontend-components-config": "4.0.28",
"@testing-library/react": "11.0.4",
"@testing-library/jest-dom": "5.10.1",
"@testing-library/react": "11.0.4",
"@testing-library/user-event": "12.2.2",
"babel-core": "7.0.0-bridge.0",
"babel-eslint": "10.0.3",

View file

@ -1,371 +1,125 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { withRouter } from 'react-router-dom';
import { connect } from 'react-redux';
import { actions } from '../../store/actions';
import React from 'react';
import ImageCreator from './ImageCreator';
import { useHistory } from 'react-router-dom';
import componentTypes from '@data-driven-forms/react-form-renderer/component-types';
import validatorTypes from '@data-driven-forms/react-form-renderer/validator-types';
import { Button, Wizard } from '@patternfly/react-core';
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
import { addNotification } from '@redhat-cloud-services/frontend-components-notifications/redux';
import WizardStepImageOutput from './WizardStepImageOutput';
import WizardStepUploadAWS from './WizardStepUploadAWS';
import WizardStepUploadAzure from './WizardStepUploadAzure';
import WizardStepPackages from './WizardStepPackages';
import WizardStepUploadGoogle from './WizardStepUploadGoogle';
import WizardStepRegistration from './WizardStepRegistration';
import WizardStepReview from './WizardStepReview';
import ImageWizardFooter from './ImageWizardFooter';
import './CreateImageWizard.scss';
class CreateImageWizard extends Component {
constructor(props) {
super(props);
this.onStep = this.onStep.bind(this);
this.onSave = this.onSave.bind(this);
this.onClose = this.onClose.bind(this);
this.validate = this.validate.bind(this);
this.validateUploadAmazon = this.validateUploadAmazon.bind(this);
this.state = {
/* errors take form of $fieldId: error */
uploadAWSErrors: {},
uploadAzureErrors: {},
uploadGoogleErrors: {},
isSaveInProgress: false,
isValidSubscription: true,
};
}
async componentDidMount() {
let user = await insights.chrome.auth.getUser();
this.setState({
subscription: {
organization: Number(user.identity.internal.org_id)
}
});
}
onStep(step) {
if (step.name === 'Review') {
this.validate();
}
}
validate() {
/* upload */
Object.keys(this.props.uploadDestinations).forEach(provider => {
switch (provider) {
case 'aws':
this.validateUploadAmazon();
break;
case 'azure':
this.validateUploadAzure();
break;
case 'google':
break;
default:
break;
}
});
/* subscription */
if (this.props.subscribeNow) {
this.setState({ isValidSubscription: this.props.subscription.activationKey ? true : false });
} else {
this.setState({ isValidSubscription: true });
}
}
validateUploadAmazon() {
let uploadAWSErrors = {};
let share = this.props.uploadAWS.shareWithAccounts;
if (share.length === 0 || share[0].length !== 12 || isNaN(share[0])) {
uploadAWSErrors['aws-account-id'] =
{ label: 'AWS account ID', value: 'A 12-digit number is required' };
}
this.setState({ uploadAWSErrors });
}
validateUploadAzure() {
let uploadAzureErrors = {};
let tenant_id = this.props.uploadAzure.tenantId;
if (tenant_id === null || tenant_id === '') {
uploadAzureErrors['azure-resource-group'] =
{ label: 'Azure tenant ID', value: 'A tenant ID is required' };
}
let subscriptionId = this.props.uploadAzure.subscriptionId;
if (subscriptionId === null || subscriptionId === '') {
uploadAzureErrors['azure-subscription-id'] =
{ label: 'Azure subscription ID', value: 'A subscription ID is required' };
}
let resource_group = this.props.uploadAzure.resourceGroup;
if (resource_group === null || resource_group === '') {
uploadAzureErrors['azure-resource-group'] =
{ label: 'Azure resource group', value: 'A resource group is required' };
}
// TODO check oauth2 thing too here?
}
onSave() {
this.setState({ isSaveInProgress: true });
let customizations = {
packages: this.props.selectedPackages.map(p => p.name),
};
if (this.props.subscribeNow) {
customizations.subscription = {
'activation-key': this.props.subscription.activationKey,
insights: this.props.subscription.insights,
organization: Number(this.props.subscription.organization),
'server-url': 'subscription.rhsm.redhat.com',
'base-url': 'https://cdn.redhat.com/',
};
}
let requests = [];
if (this.props.uploadDestinations.aws) {
let request = {
distribution: this.props.release.distro,
image_requests: [
{
architecture: this.props.release.arch,
image_type: 'ami',
upload_request: {
type: 'aws',
options: {
share_with_accounts: this.props.uploadAWS.shareWithAccounts,
const CreateImage = () => {
const history = useHistory();
return <ImageCreator
onClose={ () => history.push('/landing') }
onSubmit={ (values) => console.log(values) }
schema={ {
fields: [
{
component: componentTypes.WIZARD,
name: 'image-builder-wizard',
isDynamic: true,
inModal: true,
showTitles: true,
title: 'Create image',
crossroads: [ 'role-type' ],
description: <div>Create a RHEL image and push it to cloud providers.<a>link</a></div>,
fields: [
{
title: 'Image output',
name: 'step-1',
nextStep: {
when: 'role-type',
stepMapper: {
a: 'aws-target-env',
ab: 'aws-target-env',
undefined: 'registration'
},
},
fields: [
{
component: componentTypes.TEXT_FIELD,
name: 'role-type',
type: 'text',
label: 'Role name',
isRequired: true,
validate: [
{
type: validatorTypes.REQUIRED
}
],
}
]
},
}],
customizations,
};
requests.push(request);
}
if (this.props.uploadDestinations.google) {
let share = '';
switch (this.props.uploadGoogle.accountType) {
case 'googleAccount':
share = 'user:' + this.props.uploadGoogle.shareWithAccounts[0].user;
break;
case 'serviceAccount':
share = 'serviceAccount:' + this.props.uploadGoogle.shareWithAccounts[0].serviceAccount;
break;
case 'googleGroup':
share = 'group:' + this.props.uploadGoogle.shareWithAccounts[0].group;
break;
case 'domain':
share = 'domain:' + this.props.uploadGoogle.shareWithAccounts[0].domain;
break;
}
let request = {
distribution: this.props.release.distro,
image_requests: [
{
architecture: this.props.release.arch,
image_type: 'vhd',
upload_request: {
type: 'gcp',
options: {
share_with_accounts: [ share ],
{
title: 'Amazon Web Services',
name: 'aws-target-env',
substepOf: 'Target environment',
nextStep: {
when: 'role-type',
stepMapper: {
ab: 'ms-azure-target-env',
a: 'registration'
},
},
fields: [
{
component: componentTypes.TEXT_FIELD,
name: 'test-field',
type: 'text',
label: 'Role name',
isRequired: true,
validate: [
{
type: validatorTypes.REQUIRED,
},
],
}
]
},
}],
customizations,
};
requests.push(request);
}
if (this.props.uploadDestinations.azure) {
let request = {
distribution: this.props.release.distro,
image_requests: [
{
architecture: this.props.release.arch,
image_type: 'vhd',
upload_request: {
type: 'azure',
options: {
tenant_id: this.props.uploadAzure.tenantId,
subscription_id: this.props.uploadAzure.subscriptionId,
resource_group: this.props.uploadAzure.resourceGroup,
},
{
title: 'Microsoft Azure',
label: 'bla bla',
name: 'ms-azure-target-env',
substepOf: 'Target environment',
nextStep: 'registration',
fields: [
{
component: componentTypes.TEXT_FIELD,
name: 'test-field',
type: 'text',
label: 'Role name',
isRequired: true,
validate: [
{
type: validatorTypes.REQUIRED,
},
],
}
]
},
}],
customizations,
};
requests.push(request);
}
const composeRequests = requests.map(request => this.props.composeStart(request));
Promise.all(composeRequests)
.then(() => {
if (!this.props.composesError) {
this.props.addNotification({
variant: 'success',
title: 'Your image is being created',
});
this.props.history.push('/landing');
{
title: 'Registration',
name: 'registration',
fields: [
{
component: componentTypes.TEXT_FIELD,
name: 'another-field',
type: 'text',
label: 'Role name',
isRequired: true,
validate: [
{
type: validatorTypes.REQUIRED,
},
{
type: validatorTypes.MAX_LENGTH,
threshold: 150,
},
],
}
]
}
]
}
this.setState({ isSaveInProgress: false });
});
}
onClose () {
this.props.history.push('/landing');
}
render() {
const isValidUploadDestination = this.props.uploadDestinations.aws ||
this.props.uploadDestinations.azure ||
this.props.uploadDestinations.google;
const StepImageOutput = {
name: 'Image output',
component: <WizardStepImageOutput />
};
const StepUploadAWS = {
name: 'Amazon Web Services',
component: <WizardStepUploadAWS
errors={ this.state.uploadAWSErrors } />
};
const StepUploadGoogle = {
name: 'Google Cloud Platform',
component: <WizardStepUploadGoogle
errors={ this.state.uploadGoogleErrors } />
};
const StepUploadAzure = {
name: 'Microsoft Azure',
component: <WizardStepUploadAzure
errors={ this.state.uploadAzureErrors } />
};
const uploadDestinationSteps = [];
if (this.props.uploadDestinations.aws) {
uploadDestinationSteps.push(StepUploadAWS);
}
if (this.props.uploadDestinations.google) {
uploadDestinationSteps.push(StepUploadGoogle);
}
if (this.props.uploadDestinations.azure) {
uploadDestinationSteps.push(StepUploadAzure);
}
const StepTargetEnv = {
name: 'Target environment',
steps: uploadDestinationSteps
};
const StepImageRegistration = {
name: 'Registration',
component: <WizardStepRegistration
isValidSubscription={ this.state.isValidSubscription } />
};
const steps = [
StepImageOutput,
...(StepTargetEnv.steps.length > 0 ? [ StepTargetEnv ] : []),
...(this.props.release.distro === 'rhel-84' ? [ StepImageRegistration ] : []),
{
name: 'Packages',
component: <WizardStepPackages /> },
{
name: 'Review',
component: <WizardStepReview
uploadAWSErrors={ this.state.uploadAWSErrors }
isValidSubscription={ this.state.isValidSubscription } />,
nextButtonText: 'Create',
}
];
return (
<React.Fragment>
<Wizard
className="image-builder"
title={ 'Create image' }
description={ <>
Create a RHEL image and push it to cloud providers.
{' '}
<Button
component="a"
target="_blank"
variant="link"
icon={ <ExternalLinkAltIcon /> }
iconPosition="right"
isInline
href="
https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/uploading_a_customized_rhel_system_image_to_cloud_environments/index
">
Documentation
</Button>
</> }
onNext={ this.onStep }
onGoToStep={ this.onStep }
steps={ steps }
onClose={ this.onClose }
onSave={ this.onSave }
footer={ <ImageWizardFooter
isValidUploadDestination={ isValidUploadDestination }
isSaveInProgress={ this.state.isSaveInProgress }
isValidSubscription={ this.state.isValidSubscription }
error={ this.props.composesError } /> }
isOpen />
</React.Fragment>
);
}
}
function mapStateToProps(state) {
return {
composesError: state.composes.error,
release: state.pendingCompose.release,
uploadDestinations: state.pendingCompose.uploadDestinations,
uploadAWS: state.pendingCompose.uploadAWS,
uploadAzure: state.pendingCompose.uploadAzure,
uploadGoogle: state.pendingCompose.uploadGoogle,
selectedPackages: state.pendingCompose.selectedPackages,
subscription: state.pendingCompose.subscription,
subscribeNow: state.pendingCompose.subscribeNow,
};
}
function mapDispatchToProps(dispatch) {
return {
composeUpdated: (compose) => dispatch(actions.composeUpdated(compose)),
composeStart: (composeRequest) => dispatch(actions.composeStart(composeRequest)),
addNotification: (not) => dispatch(addNotification(not)),
};
}
CreateImageWizard.propTypes = {
composesError: PropTypes.string,
composeUpdated: PropTypes.func,
composeStart: PropTypes.func,
addNotification: PropTypes.func,
history: PropTypes.object,
release: PropTypes.object,
uploadDestinations: PropTypes.object,
uploadAWS: PropTypes.object,
uploadAzure: PropTypes.object,
uploadGoogle: PropTypes.object,
subscription: PropTypes.object,
subscribeNow: PropTypes.bool,
selectedPackages: PropTypes.array,
]
} } />;
};
export default connect(mapStateToProps, mapDispatchToProps)(withRouter(CreateImageWizard));
export default CreateImage;

View file

@ -0,0 +1,368 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { withRouter } from 'react-router-dom';
import { connect } from 'react-redux';
import { actions } from '../../store/actions';
import { Button, Wizard } from '@patternfly/react-core';
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
import { addNotification } from '@redhat-cloud-services/frontend-components-notifications/redux';
import WizardStepImageOutput from './WizardStepImageOutput';
import WizardStepUploadAWS from './WizardStepUploadAWS';
import WizardStepUploadAzure from './WizardStepUploadAzure';
import WizardStepPackages from './WizardStepPackages';
import WizardStepUploadGoogle from './WizardStepUploadGoogle';
import WizardStepRegistration from './WizardStepRegistration';
import WizardStepReview from './WizardStepReview';
import ImageWizardFooter from './ImageWizardFooter';
import './CreateImageWizard.scss';
class CreateImageWizard extends Component {
constructor(props) {
super(props);
this.onStep = this.onStep.bind(this);
this.onSave = this.onSave.bind(this);
this.onClose = this.onClose.bind(this);
this.validate = this.validate.bind(this);
this.validateUploadAmazon = this.validateUploadAmazon.bind(this);
this.state = {
/* errors take form of $fieldId: error */
uploadAWSErrors: {},
uploadAzureErrors: {},
uploadGoogleErrors: {},
isSaveInProgress: false,
isValidSubscription: true,
};
}
async componentDidMount() {
let user = await insights.chrome.auth.getUser();
this.setState({
subscription: {
organization: Number(user.identity.internal.org_id)
}
});
}
onStep(step) {
if (step.name === 'Review') {
this.validate();
}
}
validate() {
/* upload */
Object.keys(this.props.uploadDestinations).forEach(provider => {
switch (provider) {
case 'aws':
this.validateUploadAmazon();
break;
case 'azure':
this.validateUploadAzure();
break;
case 'google':
break;
default:
break;
}
});
/* subscription */
if (this.props.subscribeNow) {
this.setState({ isValidSubscription: this.props.subscription.activationKey ? true : false });
} else {
this.setState({ isValidSubscription: true });
}
}
validateUploadAmazon() {
let uploadAWSErrors = {};
let share = this.props.uploadAWS.shareWithAccounts;
if (share.length === 0 || share[0].length !== 12 || isNaN(share[0])) {
uploadAWSErrors['aws-account-id'] =
{ label: 'AWS account ID', value: 'A 12-digit number is required' };
}
this.setState({ uploadAWSErrors });
}
validateUploadAzure() {
let uploadAzureErrors = {};
let tenant_id = this.props.uploadAzure.tenantId;
if (tenant_id === null || tenant_id === '') {
uploadAzureErrors['azure-resource-group'] =
{ label: 'Azure tenant ID', value: 'A tenant ID is required' };
}
let subscriptionId = this.props.uploadAzure.subscriptionId;
if (subscriptionId === null || subscriptionId === '') {
uploadAzureErrors['azure-subscription-id'] =
{ label: 'Azure subscription ID', value: 'A subscription ID is required' };
}
let resource_group = this.props.uploadAzure.resourceGroup;
if (resource_group === null || resource_group === '') {
uploadAzureErrors['azure-resource-group'] =
{ label: 'Azure resource group', value: 'A resource group is required' };
}
// TODO check oauth2 thing too here?
}
onSave() {
this.setState({ isSaveInProgress: true });
let customizations = {
packages: this.props.selectedPackages.map(p => p.name),
};
if (this.props.subscribeNow) {
customizations.subscription = {
'activation-key': this.props.subscription.activationKey,
insights: this.props.subscription.insights,
organization: Number(this.props.subscription.organization),
'server-url': 'subscription.rhsm.redhat.com',
'base-url': 'https://cdn.redhat.com/',
};
}
let requests = [];
if (this.props.uploadDestinations.aws) {
let request = {
distribution: this.props.release.distro,
image_requests: [
{
architecture: this.props.release.arch,
image_type: 'ami',
upload_request: {
type: 'aws',
options: {
share_with_accounts: this.props.uploadAWS.shareWithAccounts,
},
},
}],
customizations,
};
requests.push(request);
}
if (this.props.uploadDestinations.google) {
let share = '';
switch (this.props.uploadGoogle.accountType) {
case 'googleAccount':
share = 'user:' + this.props.uploadGoogle.shareWithAccounts[0].user;
break;
case 'serviceAccount':
share = 'serviceAccount:' + this.props.uploadGoogle.shareWithAccounts[0].serviceAccount;
break;
case 'googleGroup':
share = 'group:' + this.props.uploadGoogle.shareWithAccounts[0].group;
break;
case 'domain':
share = 'domain:' + this.props.uploadGoogle.shareWithAccounts[0].domain;
break;
}
let request = {
distribution: this.props.release.distro,
image_requests: [
{
architecture: this.props.release.arch,
image_type: 'vhd',
upload_request: {
type: 'gcp',
options: {
share_with_accounts: [ share ],
},
},
}],
customizations,
};
requests.push(request);
}
if (this.props.uploadDestinations.azure) {
let request = {
distribution: this.props.release.distro,
image_requests: [
{
architecture: this.props.release.arch,
image_type: 'vhd',
upload_request: {
type: 'azure',
options: {
tenant_id: this.props.uploadAzure.tenantId,
subscription_id: this.props.uploadAzure.subscriptionId,
resource_group: this.props.uploadAzure.resourceGroup,
},
},
}],
customizations,
};
requests.push(request);
}
const composeRequests = requests.map(request => this.props.composeStart(request));
Promise.all(composeRequests)
.then(() => {
if (!this.props.composesError) {
this.props.addNotification({
variant: 'success',
title: 'Your image is being created',
});
this.props.history.push('/landing');
}
this.setState({ isSaveInProgress: false });
});
}
onClose () {
this.props.history.push('/landing');
}
render() {
const isValidUploadDestination = this.props.uploadDestinations.aws ||
this.props.uploadDestinations.azure ||
this.props.uploadDestinations.google;
const StepImageOutput = {
name: 'Image output',
component: <WizardStepImageOutput />
};
const StepUploadAWS = {
name: 'Amazon Web Services',
component: <WizardStepUploadAWS
errors={ this.state.uploadAWSErrors } />
};
const StepUploadAzure = {
name: 'Microsoft Azure',
component: <WizardStepUploadAzure
errors={ this.state.uploadAzureErrors } />
};
const StepUploadGoogle = {
name: 'Google Cloud Platform',
component: <WizardStepUploadGoogle
errors={ this.state.uploadGoogleErrors } />
};
const uploadDestinationSteps = [];
if (this.props.uploadDestinations.aws) {
uploadDestinationSteps.push(StepUploadAWS);
}
if (this.props.uploadDestinations.azure) {
uploadDestinationSteps.push(StepUploadAzure);
}
if (this.props.uploadDestinations.google) {
uploadDestinationSteps.push(StepUploadGoogle);
}
const StepTargetEnv = {
name: 'Target environment',
steps: uploadDestinationSteps
};
const steps = [
StepImageOutput,
...(StepTargetEnv.steps.length > 0 ? [ StepTargetEnv ] : []),
{
name: 'Registration',
component: <WizardStepRegistration
isValidSubscription={ this.state.isValidSubscription } /> },
{
name: 'Packages',
component: <WizardStepPackages /> },
{
name: 'Review',
component: <WizardStepReview
uploadAWSErrors={ this.state.uploadAWSErrors }
isValidSubscription={ this.state.isValidSubscription } />,
nextButtonText: 'Create',
}
];
return (
<React.Fragment>
<Wizard
className="image-builder"
title={ 'Create image' }
description={ <>
Create a RHEL image and push it to cloud providers.
{' '}
<Button
component="a"
target="_blank"
variant="link"
icon={ <ExternalLinkAltIcon /> }
iconPosition="right"
isInline
href="
https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/uploading_a_customized_rhel_system_image_to_cloud_environments/index
">
Documentation
</Button>
</> }
onNext={ this.onStep }
onGoToStep={ this.onStep }
steps={ steps }
onClose={ this.onClose }
onSave={ this.onSave }
footer={ <ImageWizardFooter
isValidUploadDestination={ isValidUploadDestination }
isSaveInProgress={ this.state.isSaveInProgress }
isValidSubscription={ this.state.isValidSubscription }
error={ this.props.composesError } /> }
isOpen />
</React.Fragment>
);
}
}
function mapStateToProps(state) {
return {
composesError: state.composes.error,
release: state.pendingCompose.release,
uploadDestinations: state.pendingCompose.uploadDestinations,
uploadAWS: state.pendingCompose.uploadAWS,
uploadAzure: state.pendingCompose.uploadAzure,
uploadGoogle: state.pendingCompose.uploadGoogle,
selectedPackages: state.pendingCompose.selectedPackages,
subscription: state.pendingCompose.subscription,
subscribeNow: state.pendingCompose.subscribeNow,
};
}
function mapDispatchToProps(dispatch) {
return {
composeUpdated: (compose) => dispatch(actions.composeUpdated(compose)),
composeStart: (composeRequest) => dispatch(actions.composeStart(composeRequest)),
addNotification: (not) => dispatch(addNotification(not)),
};
}
CreateImageWizard.propTypes = {
composesError: PropTypes.string,
composeUpdated: PropTypes.func,
composeStart: PropTypes.func,
addNotification: PropTypes.func,
history: PropTypes.object,
release: PropTypes.object,
uploadDestinations: PropTypes.object,
uploadAWS: PropTypes.object,
uploadAzure: PropTypes.object,
uploadGoogle: PropTypes.object,
subscription: PropTypes.object,
subscribeNow: PropTypes.bool,
selectedPackages: PropTypes.array,
};
export default connect(mapStateToProps, mapDispatchToProps)(withRouter(CreateImageWizard));

View file

@ -0,0 +1,32 @@
import React from 'react';
import FormRenderer from '@data-driven-forms/react-form-renderer/form-renderer';
import Pf4FormTemplate from '@data-driven-forms/pf4-component-mapper/form-template';
import componentTypes from '@data-driven-forms/react-form-renderer/component-types';
import Wizard from '@data-driven-forms/pf4-component-mapper/wizard';
import TextField from '@data-driven-forms/pf4-component-mapper/text-field';
import { Spinner } from '@patternfly/react-core';
import PropTypes from 'prop-types';
const CreateImageWizard = ({ schema, onSubmit, onClose }) => {
return schema ? <FormRenderer
schema={ schema }
subscription={ { values: true } }
FormTemplate={ (props) => <Pf4FormTemplate { ...props } showFormControls={ false } /> }
onSubmit={ (formValues) => onSubmit(formValues) }
componentMapper={ {
[componentTypes.WIZARD]: {
component: Wizard,
'data-ouia-component-id': 'image-creation-wizard'
},
[componentTypes.TEXT_FIELD]: TextField
} }
onCancel={ onClose } /> : <Spinner />;
};
CreateImageWizard.propTypes = {
schema: PropTypes.object,
onSubmit: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};
export default CreateImageWizard;