diff --git a/src/Routes.js b/src/Routes.js
index 8ee0272d..0ae6919a 100644
--- a/src/Routes.js
+++ b/src/Routes.js
@@ -4,6 +4,7 @@ import React from 'react';
import asyncComponent from './Utilities/asyncComponent';
const LandingPage = asyncComponent(() => import('./SmartComponents/LandingPage/LandingPage'));
+const CreateImageWizard = asyncComponent(() => import('./SmartComponents/CreateImageWizard/CreateImageWizard'));
const InsightsRoute = ({ component: Component, rootClass, ...rest }) => {
const root = document.getElementById('root');
@@ -22,6 +23,7 @@ export const Routes = () => {
return (
+
);
diff --git a/src/SmartComponents/CreateImageWizard/CreateImageWizard.js b/src/SmartComponents/CreateImageWizard/CreateImageWizard.js
new file mode 100644
index 00000000..48d4ec5f
--- /dev/null
+++ b/src/SmartComponents/CreateImageWizard/CreateImageWizard.js
@@ -0,0 +1,489 @@
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import { withRouter } from 'react-router-dom';
+import { connect } from 'react-redux';
+import { actions } from '../redux';
+
+import {
+ Alert,
+ Flex,
+ FlexItem,
+ FlexModifiers,
+ Form,
+ FormGroup,
+ FormSelect,
+ FormSelectOption,
+ Radio,
+ TextContent,
+ TextInput,
+ Title,
+ Wizard,
+} from '@patternfly/react-core';
+
+import { ExclamationCircleIcon } from '@patternfly/react-icons';
+
+import api from './../../api.js';
+
+const ReleaseComponent = (props) => {
+ const options = [
+ { value: 'rhel-8', label: 'Red Hat Enterprise Linux (RHEL) 8.2' },
+ ];
+ return (
+
+ );
+};
+
+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 (.vhdx)' },
+ ];
+
+ return (
+ <>
+
+ >
+ );
+};
+
+UploadComponent.propTypes = {
+ setUpload: PropTypes.func,
+ setUploadOptions: PropTypes.func,
+ upload: PropTypes.object,
+ errors: PropTypes.object,
+};
+
+const SubscriptionComponent = (props) => {
+ return (
+
+ );
+};
+
+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);
+
+ this.setRelease = this.setRelease.bind(this);
+ this.setUpload = this.setUpload.bind(this);
+ this.setUploadOptions = this.setUploadOptions.bind(this);
+ this.setSubscription = this.setSubscription.bind(this);
+ this.setSubscribeNow = this.setSubscribeNow.bind(this);
+ 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.validateSubscription = this.validateSubscription.bind(this);
+
+ this.state = {
+ release: 'rhel-8',
+ upload: {
+ type: 'aws',
+ options: {
+ service: 'ec2',
+ region: 'eu-west-2',
+ access_key_id: null,
+ secret_access_key: null,
+ bucket: null,
+ }
+ },
+ subscription: {
+ organization: null,
+ 'activation-key': null,
+ 'server-url': 'subscription.rhsm.redhat.com',
+ 'base-url': 'http:cdn.redhat.com/',
+ insights: true
+ },
+ subscribeNow: false,
+ /* errors take form of $fieldId: error */
+ uploadErrors: {},
+ subscriptionErrors: {},
+ };
+ }
+
+ 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 */
+ if (this.state.upload.type === 'aws') {
+ this.validateUploadAmazon();
+ } else {
+ this.setState({ uploadErrors: {}});
+ }
+
+ /* subscription */
+ if (this.state.subscribeNow) {
+ this.validateSubscription();
+ } else {
+ this.setState({ subscriptionErrors: {}});
+ }
+ }
+
+ validateUploadAmazon() {
+ let uploadErrors = {};
+ if (!this.state.upload.options.access_key_id) {
+ uploadErrors['amazon-access-id'] =
+ { label: 'Access key ID', value: 'A value is required' };
+ }
+
+ if (!this.state.upload.options.secret_access_key) {
+ uploadErrors['amazon-access-secret'] =
+ { label: 'Secret access key', value: 'A value is required' };
+ }
+
+ if (!this.state.upload.options.region) {
+ uploadErrors['amazon-region'] =
+ { label: 'Region', value: 'A value is required' };
+ }
+
+ if (this.state.upload.options.service === 's3' &&
+ !this.state.upload.options.bucket) {
+ uploadErrors['amazon-bucket'] =
+ { label: 'Bucket', value: 'A value is required' };
+ }
+
+ this.setState({ uploadErrors });
+ }
+
+ validateSubscription() {
+ let subscriptionErrors = {};
+ if (!this.state.subscription['activation-key']) {
+ subscriptionErrors['subscription-activation'] =
+ { label: 'Activation key', value: 'A value is required' };
+ }
+
+ this.setState({ subscriptionErrors });
+ }
+
+ setRelease(release) {
+ this.setState({ release });
+ }
+
+ setUpload(upload) {
+ this.setState({ upload });
+ }
+
+ setUploadOptions(uploadOptions) {
+ this.setState(oldState => {
+ return {
+ upload: {
+ type: oldState.upload.type,
+ options: uploadOptions
+ }
+ };
+ });
+ }
+
+ setSubscribeNow(subscribeNow) {
+ this.setState({ subscribeNow });
+ }
+
+ setSubscription(subscription) {
+ this.setState({ subscription }, this.validate);
+ }
+
+ onSave () {
+ let request = {
+ distribution: this.state.release,
+ image_requests: [
+ {
+ architecture: 'x86_64',
+ image_type: 'qcow2',
+ upload_requests: [{
+ type: 'aws',
+ options: {
+ region: this.state.upload.options.region,
+ s3: {
+ access_key_id: this.state.upload.options.access_key_id,
+ secret_access_key: this.state.upload.options.secret_access_key,
+ bucket: this.state.upload.options.bucket,
+ },
+ ec2: {
+ access_key_id: this.state.upload.options.access_key_id,
+ secret_access_key: this.state.upload.options.secret_access_key,
+ },
+ },
+ }],
+ }],
+ customizations: {
+ subscription: this.state.subscription,
+ },
+ };
+
+ let { updateCompose } = this.props;
+ api.composeImage(request).then(response => {
+ let compose = {};
+ compose[response.id] = {
+ status: 'request sent',
+ distribution: request.distribution,
+ architecture: request.image_requests[0].architecture,
+ image_type: request.image_requests[0].image_type,
+ };
+ updateCompose(compose);
+ this.props.history.push('/landing');
+ });
+ }
+
+ onClose () {
+ this.props.history.push('/landing');
+ }
+
+ render() {
+ const steps = [
+ {
+ name: 'Release',
+ component: },
+ {
+ name: 'Target environment',
+ component: },
+ {
+ name: 'Registration',
+ component: },
+ {
+ name: 'Review',
+ component: ,
+ nextButtonText: 'Create',
+ }
+ ];
+
+ return (
+ <>
+
+
+ Create a new image
+
+
+
+ >
+ );
+ }
+}
+
+function mapDispatchToProps(dispatch) {
+ return {
+ updateCompose: (compose) => dispatch(actions.updateCompose(compose)),
+ };
+}
+
+CreateImageWizard.propTypes = {
+ updateCompose: PropTypes.func,
+ history: PropTypes.object,
+};
+
+export default connect(null, mapDispatchToProps)(withRouter(CreateImageWizard));
diff --git a/src/SmartComponents/LandingPage/CreateImageCard.js b/src/SmartComponents/LandingPage/CreateImageCard.js
index 6065b2ab..d5dcab0b 100644
--- a/src/SmartComponents/LandingPage/CreateImageCard.js
+++ b/src/SmartComponents/LandingPage/CreateImageCard.js
@@ -1,7 +1,5 @@
-import PropTypes from 'prop-types';
import React, { Component } from 'react';
-import { connect } from 'react-redux';
-import { actions } from '../redux';
+import { Link } from 'react-router-dom';
import {
Button,
@@ -14,42 +12,10 @@ import {
FormGroup,
} from '@patternfly/react-core';
-import { DefaultApi } from '@redhat-cloud-services/osbuild-installer';
-
class CreateImageCard extends Component {
constructor(props) {
super(props);
- this.buildImage = this.buildImage.bind(this);
- }
-
- buildImage() {
- let request = {
- image_builds: [
- {
- distribution: 'fedora-31',
- architecture: 'x86_64',
- image_type: 'qcow2',
- repositories: [{ baseurl: 'http://download.fedoraproject.org/pub/fedora/linux/releases/30/Everything/x86_64/os/' }],
- }]
- };
- let api = new DefaultApi();
- let { updateCompose } = this.props;
- api.composeImage(request).then(response => {
- /* request failed? */
- if (response.data.compose_id === undefined) {
- return;
- }
-
- let compose = {};
- compose[response.data.compose_id] = {
- status: 'request sent',
- distribution: request.image_builds[0].distribution,
- architecture: request.image_builds[0].architecture,
- image_type: request.image_builds[0].image_type,
- };
- updateCompose(compose);
- });
}
render() {
@@ -58,10 +24,12 @@ class CreateImageCard extends Component {
Create a new image
@@ -69,14 +37,4 @@ class CreateImageCard extends Component {
}
}
-function mapDispatchToProps(dispatch) {
- return {
- updateCompose: (compose) => dispatch(actions.updateCompose(compose)),
- };
-}
-
-CreateImageCard.propTypes = {
- updateCompose: PropTypes.func,
-};
-
-export default connect(() => {}, mapDispatchToProps)(CreateImageCard);
+export default CreateImageCard;
diff --git a/src/SmartComponents/LandingPage/ImagesCard.js b/src/SmartComponents/LandingPage/ImagesCard.js
index 7c538a99..b536e31f 100644
--- a/src/SmartComponents/LandingPage/ImagesCard.js
+++ b/src/SmartComponents/LandingPage/ImagesCard.js
@@ -9,7 +9,7 @@ import {
CardBody,
} from '@patternfly/react-core';
-import { DefaultApi } from '@redhat-cloud-services/osbuild-installer';
+import api from '../../api.js';
class ImagesCard extends Component {
constructor(props) {
@@ -21,13 +21,16 @@ class ImagesCard extends Component {
this.interval = setInterval(() => this.pollComposeStatuses(), 8000);
}
+ componentWillUnmount() {
+ clearInterval(this.interval);
+ }
+
pollComposeStatuses() {
- let api = new DefaultApi();
let { updateCompose, composes } = this.props;
Object.entries(composes).map(([ id, compose ]) => {
api.getComposeStatus(id).then(response => {
let newCompose = {};
- newCompose[id] = Object.assign({}, compose, { status: response.data.status });
+ newCompose[id] = Object.assign({}, compose, { status: response.status });
updateCompose(newCompose);
});
});
diff --git a/src/SmartComponents/LandingPage/LandingPage.js b/src/SmartComponents/LandingPage/LandingPage.js
index 39eefdf4..3f83fb8b 100644
--- a/src/SmartComponents/LandingPage/LandingPage.js
+++ b/src/SmartComponents/LandingPage/LandingPage.js
@@ -22,6 +22,7 @@ class LandingPage extends Component {
constructor(props) {
super(props);
}
+
render() {
return (
@@ -33,7 +34,7 @@ class LandingPage extends Component {
-
+
diff --git a/src/SmartComponents/redux/reducers.js b/src/SmartComponents/redux/reducers.js
index 758f1f8e..53f3fb9e 100644
--- a/src/SmartComponents/redux/reducers.js
+++ b/src/SmartComponents/redux/reducers.js
@@ -15,8 +15,6 @@ export function composeReducer(state = { }, action) {
case types.UPDATE_COMPOSE:
return Object.assign({}, state, action.compose);
default:
- return {
- ...state
- };
+ return state;
}
}
diff --git a/src/api.js b/src/api.js
new file mode 100644
index 00000000..64351a16
--- /dev/null
+++ b/src/api.js
@@ -0,0 +1,23 @@
+import axios from 'axios';
+import {
+ OSBUILD_INSTALLER_API,
+} from './constants';
+
+const postHeaders = { headers: { 'Content-Type': 'application/json' }};
+
+async function composeImage(body) {
+ let path = '/compose';
+ const request = await axios.post(OSBUILD_INSTALLER_API.concat(path), body, postHeaders);
+ return request.data;
+}
+
+async function getComposeStatus(id) {
+ let path = '/compose/' + id;
+ const request = await axios.get(OSBUILD_INSTALLER_API.concat(path));
+ return request.data;
+}
+
+export default {
+ composeImage,
+ getComposeStatus,
+};
diff --git a/src/constants.js b/src/constants.js
new file mode 100644
index 00000000..5b1af6b8
--- /dev/null
+++ b/src/constants.js
@@ -0,0 +1 @@
+export const OSBUILD_INSTALLER_API = '/api/osbuild-installer/v1';