import React from "react";
import { Button, Wizard, WizardContextConsumer, WizardFooter } from "@patternfly/react-core";
import { defineMessages, FormattedMessage, injectIntl, intlShape } from "react-intl";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import NotificationsApi from "../../data/NotificationsApi";
import BlueprintApi from "../../data/BlueprintApi";
import { setBlueprint } from "../../core/actions/blueprints";
import { fetchingQueue, clearQueue, startCompose, fetchingComposeTypes } from "../../core/actions/composes";
import AWSAuthStep from "./AWSAuthStep";
import AWSDestinationStep from "./AWSDestinationStep";
import AzureAuthStep from "./AzureAuthStep";
import AzureDestinationStep from "./AzureDestinationStep";
import ReviewStep from "./ReviewStep";
import ImageStep from "./ImageStep";
const messages = defineMessages({
title: {
defaultMessage: "Create image",
},
});
class CreateImageUpload extends React.Component {
constructor(props) {
super(props);
this.state = {
isOpen: false,
};
this.open = this.open.bind(this);
this.close = this.close.bind(this);
}
open() {
this.setState({ isOpen: true });
}
close() {
this.setState({ isOpen: false });
}
render() {
return (
<>
<Button id="create-image-button" variant="secondary" onClick={this.open}>
<FormattedMessage defaultMessage="Create image" />
</Button>
{this.state.isOpen && <CreateImageUploadModal {...this.props} close={this.close} isOpen={this.state.isOpen} />}
</>
);
}
}
class CreateImageUploadModal extends React.Component {
constructor(props) {
super(props);
this.state = {
imageType: "",
imageName: "",
imageSize: undefined,
minImageSize: 0,
maxImageSize: 2000,
ostreeSettings: {
parent: undefined,
ref: undefined,
},
showUploadAwsStep: false,
showUploadAzureStep: false,
showReviewStep: false,
uploadService: "",
uploadSettings: {},
};
this.disableCreateButton = this.disableCreateButton.bind(this);
this.getDefaultImageSize = this.getDefaultImageSize.bind(this);
this.isPendingChange = this.isPendingChange.bind(this);
this.isValidImageSize = this.isValidImageSize.bind(this);
this.requiresImageSize = this.requiresImageSize.bind(this);
this.missingRequiredFields = this.missingRequiredFields.bind(this);
this.setNotifications = this.setNotifications.bind(this);
this.setImageSize = this.setImageSize.bind(this);
this.setImageName = this.setImageName.bind(this);
this.setImageType = this.setImageType.bind(this);
this.setOstreeParent = this.setOstreeParent.bind(this);
this.setOstreeRef = this.setOstreeRef.bind(this);
this.setUploadSettings = this.setUploadSettings.bind(this);
this.handleUploadService = this.handleUploadService.bind(this);
this.handleCreateImage = this.handleCreateImage.bind(this);
this.handleCommit = this.handleCommit.bind(this);
this.handleStartCompose = this.handleStartCompose.bind(this);
this.handleNextStep = this.handleNextStep.bind(this);
}
componentWillMount() {
if (this.props.imageTypes.length === 0) {
this.props.fetchingComposeTypes();
}
if (this.props.composeQueueFetched === false) {
this.props.fetchingQueue();
}
}
componentWillUnmount() {
this.props.clearQueue();
}
getDefaultImageSize(imageType) {
if (imageType === "ami") {
return 6;
}
if (imageType === undefined) {
return null;
}
return 2;
}
setNotifications() {
this.props.layout.setNotifications();
}
setUploadSettings(_, event) {
const key = event.target.name;
const { value } = event.target;
this.setState((prevState) => ({ uploadSettings: { ...prevState.uploadSettings, [key]: value } }));
}
setImageSize(value) {
this.setState({
imageSize: value ? Number(value) : undefined,
});
}
setImageName(value) {
this.setState({
imageName: value,
});
}
setOstreeParent(value) {
this.setState((prevState) => ({ ostreeSettings: { ...prevState.ostreeSettings, parent: value } }));
}
setOstreeRef(value) {
this.setState((prevState) => ({ ostreeSettings: { ...prevState.ostreeSettings, ref: value } }));
}
setImageType(imageType) {
const defaultImageSize = this.getDefaultImageSize(imageType);
this.setState({
imageType,
imageName: "",
imageSize: defaultImageSize,
minImageSize: defaultImageSize,
ostreeSettings: {
parent: undefined,
ref: undefined,
},
uploadService: "",
uploadSettings: {},
showUploadAwsStep: false,
showUploadAzureStep: false,
showReviewStep: false,
});
}
isPendingChange() {
return (
this.props.blueprint.workspacePendingChanges.length > 0 || this.props.blueprint.localPendingChanges.length > 0
);
}
isValidImageSize() {
if (this.state.imageSize !== undefined && this.state.imageSize < this.state.minImageSize) {
return false;
}
return true;
}
requiresImageSize(imageType) {
if (imageType === "fedora-iot-commit" || imageType === "rhel-edge-commit") {
return false;
}
return true;
}
disableCreateButton(activeStep) {
if (this.state.imageType === "") {
return true;
}
if (
this.requiresImageSize(this.state.imageType) &&
(this.state.imageSize === undefined || (!this.isValidImageSize() && this.state.uploadService === ""))
) {
return true;
}
if (this.missingRequiredFields() && activeStep.name === "Review") {
return true;
}
return false;
}
missingRequiredFields() {
if (this.state.uploadService.length === 0) return true;
if (this.state.imageName.length === 0) return true;
if (Object.values(this.state.uploadSettings).some((setting) => setting === "")) return true;
for (const setting in this.state.uploadSettings) {
if (this.state.uploadSettings[setting].length === 0) return true;
}
if (this.state.imageSize === undefined || this.state.imageSize < this.state.minImageSize) return true;
return false;
}
handleUploadService(_, event) {
const uploadService = event.target.value;
const { checked } = event.target;
if (!checked) {
this.setState({
uploadService: "",
showUploadAwsStep: false,
showUploadAzureStep: false,
showReviewStep: false,
});
} else {
switch (uploadService) {
case "aws":
this.setState({
uploadService,
uploadSettings: {
accessKeyID: "",
secretAccessKey: "",
bucket: "",
region: "",
},
showUploadAwsStep: true,
showReviewStep: true,
});
break;
case "azure":
this.setState({
uploadService,
uploadSettings: {
storageAccount: "",
storageAccessKey: "",
container: "",
},
showUploadAzureStep: true,
showReviewStep: true,
});
break;
default:
break;
}
}
}
handleCommit() {
// clear existing notifications
NotificationsApi.closeNotification(undefined, "committed");
NotificationsApi.closeNotification(undefined, "committing");
// display the committing notification
NotificationsApi.displayNotification(this.props.blueprint.name, "committing");
this.setNotifications();
// post blueprint (includes 'committed' notification)
Promise.all([BlueprintApi.handleCommitBlueprint(this.props.blueprint)])
.then(() => {
// then after blueprint is posted, reload blueprint details
// to get details that were updated during commit (i.e. version)
// and call create image
Promise.all([BlueprintApi.reloadBlueprintDetails(this.props.blueprint)])
.then((data) => {
const blueprintToSet = { ...this.props.blueprint, version: data[0].version };
this.props.setBlueprint(blueprintToSet);
this.handleCreateImage();
})
.catch((e) => console.log(`Error in reload blueprint details: ${e}`));
})
.catch((e) => console.log(`Error in blueprint commit: ${e}`));
}
handleCreateImage() {
NotificationsApi.displayNotification(this.props.blueprint.name, "imageWaiting");
if (this.setNotifications) this.setNotifications();
if (this.handleStartCompose)
this.handleStartCompose(
this.props.blueprint.name,
this.state.imageType,
this.state.imageName,
this.state.imageSize,
this.state.ostreeSettings,
this.state.uploadService,
this.state.uploadSettings
);
this.props.close();
}
handleStartCompose(blueprintName, composeType, imageName, imageSize, ostreeSettings, uploadService, uploadSettings) {
const upload = {
image_name: imageName,
provider: uploadService,
settings: uploadSettings,
};
let ostree;
if (ostreeSettings.parent !== undefined || ostreeSettings.ref !== undefined) {
ostree = ostreeSettings;
}
if (uploadService === "") {
this.props.startCompose(blueprintName, composeType, imageSize, ostree);
} else {
this.props.startCompose(blueprintName, composeType, imageSize, ostree, upload);
}
}
handleNextStep(activeStep, toNextStep) {
if (activeStep.name === "Review" || (activeStep.name === "Image type" && this.state.uploadService.length === 0)) {
if (this.isPendingChange()) this.handleCommit();
else this.handleCreateImage();
} else toNextStep();
}
render() {
const { formatMessage } = this.props.intl;
const { showUploadAwsStep, showUploadAzureStep, showReviewStep, uploadService } = this.state;
const imageStep = {
name: "Image type",
component: (
<ImageStep
blueprint={this.props.blueprint}
handleUploadService={this.handleUploadService}
imageName={this.state.imageName}
imageSize={this.state.imageSize}
imageType={this.state.imageType}
imageTypes={this.props.imageTypes}
isPendingChange={this.isPendingChange}
isValidImageSize={this.isValidImageSize}
minImageSize={this.state.minImageSize}
maxImageSize={this.state.maxImageSize}
ostreeSettings={this.state.ostreeSettings}
requiresImageSize={this.requiresImageSize}
setImageSize={this.setImageSize}
setImageType={this.setImageType}
setOstreeParent={this.setOstreeParent}
setOstreeRef={this.setOstreeRef}
uploadService={this.state.uploadService}
/>
),
};
const awsUploadAuth = {
name: "Authentication",
component: <AWSAuthStep uploadSettings={this.state.uploadSettings} setUploadSettings={this.setUploadSettings} />,
};
const awsUploadDest = {
name: "Destination",
component: (
<AWSDestinationStep
imageName={this.state.imageName}
uploadSettings={this.state.uploadSettings}
setImageName={this.setImageName}
setUploadSettings={this.setUploadSettings}
/>
),
};
const awsUploadStep = {
name: "Upload to AWS",
steps: [awsUploadAuth, awsUploadDest],
};
const azureUploadAuth = {
name: "Authentication",
component: (
<AzureAuthStep uploadSettings={this.state.uploadSettings} setUploadSettings={this.setUploadSettings} />
),
};
const azureUploadDest = {
name: "Destination",
component: (
<AzureDestinationStep
imageName={this.state.imageName}
uploadSettings={this.state.uploadSettings}
setImageName={this.setImageName}
setUploadSettings={this.setUploadSettings}
/>
),
};
const azureUploadStep = {
name: "Upload to Azure",
steps: [azureUploadAuth, azureUploadDest],
};
const reviewStep = {
name: "Review",
component: (
<ReviewStep
imageName={this.state.imageName}
imageSize={this.state.imageSize}
imageType={this.state.imageType}
imageTypes={this.props.imageTypes}
minImageSize={this.state.minImageSize}
maxImageSize={this.state.maxImageSize}
uploadService={this.state.uploadService}
uploadSettings={this.state.uploadSettings}
missingRequiredFields={this.missingRequiredFields}
/>
),
};
const steps = [
imageStep,
...(showUploadAwsStep ? [awsUploadStep] : []),
...(showUploadAzureStep ? [azureUploadStep] : []),
...(showReviewStep ? [reviewStep] : []),
];
const createImageUploadFooter = (
<WizardFooter>
<WizardContextConsumer>
{({ activeStep, onNext, onBack, onClose }) => {
return (
<>
<Button
id="continue-button"
variant="primary"
isDisabled={this.disableCreateButton(activeStep)}
onClick={() => this.handleNextStep(activeStep, onNext)}
>
{activeStep.name === "Image type" ? (
uploadService.length > 0 ? (
this.isPendingChange() ? (
<FormattedMessage defaultMessage="Commit and next" />
) : (
<FormattedMessage defaultMessage="Next" />
)
) : this.isPendingChange() ? (
<FormattedMessage defaultMessage="Commit and create" />
) : (
<FormattedMessage defaultMessage="Create" />
)
) : activeStep.name === "Review" ? (
<FormattedMessage defaultMessage="Finish" />
) : (
<FormattedMessage defaultMessage="Next" />
)}
</Button>
<Button variant="secondary" onClick={onBack} isDisabled={activeStep.name === "Image type"}>
<FormattedMessage defaultMessage="Back" />
</Button>
<Button id="cancel-button" variant="danger" onClick={onClose}>
<FormattedMessage defaultMessage="Cancel" />
</Button>
</>
);
}}
</WizardContextConsumer>
</WizardFooter>
);
return (
<>
<Wizard
id="create-image-upload-wizard"
isOpen={this.props.isOpen}
isCompactNav
onClose={this.props.close}
footer={createImageUploadFooter}
title={formatMessage(messages.title)}
steps={steps}
/>
</>
);
}
}
CreateImageUpload.propTypes = {
blueprint: PropTypes.shape({
changed: PropTypes.bool,
description: PropTypes.string,
groups: PropTypes.array,
id: PropTypes.string,
localPendingChanges: PropTypes.array,
modules: PropTypes.array,
name: PropTypes.string,
packages: PropTypes.arrayOf(PropTypes.object),
version: PropTypes.string,
workspacePendingChanges: PropTypes.arrayOf(PropTypes.object),
}),
intl: intlShape.isRequired,
layout: PropTypes.shape({
setNotifications: PropTypes.func,
}),
};
CreateImageUpload.defaultProps = {
blueprint: {},
layout: {},
};
CreateImageUploadModal.propTypes = {
blueprint: PropTypes.shape({
changed: PropTypes.bool,
description: PropTypes.string,
groups: PropTypes.array,
id: PropTypes.string,
localPendingChanges: PropTypes.array,
modules: PropTypes.array,
name: PropTypes.string,
packages: PropTypes.arrayOf(PropTypes.object),
version: PropTypes.string,
workspacePendingChanges: PropTypes.arrayOf(PropTypes.object),
}),
composeQueueFetched: PropTypes.bool,
fetchingQueue: PropTypes.func,
clearQueue: PropTypes.func,
imageTypes: PropTypes.arrayOf(PropTypes.object),
fetchingComposeTypes: PropTypes.func,
setBlueprint: PropTypes.func,
intl: intlShape.isRequired,
startCompose: PropTypes.func,
layout: PropTypes.shape({
setNotifications: PropTypes.func,
}),
close: PropTypes.func.isRequired,
isOpen: PropTypes.bool,
};
CreateImageUploadModal.defaultProps = {
blueprint: {},
composeQueueFetched: true,
fetchingQueue() {},
clearQueue() {},
imageTypes: [],
fetchingComposeTypes() {},
setBlueprint() {},
startCompose() {},
layout: {},
isOpen: false,
};
const mapStateToProps = (state) => ({
composeQueue: state.composes.queue,
composeQueueFetched: state.composes.queueFetched,
imageTypes: state.composes.composeTypes,
});
const mapDispatchToProps = (dispatch) => ({
setBlueprint: (blueprint) => {
dispatch(setBlueprint(blueprint));
},
fetchingComposeTypes: () => {
dispatch(fetchingComposeTypes());
},
fetchingQueue: () => {
dispatch(fetchingQueue());
},
clearQueue: () => {
dispatch(clearQueue());
},
startCompose: (blueprintName, composeType, imageSize, ostree, upload) => {
dispatch(startCompose(blueprintName, composeType, imageSize, ostree, upload));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(CreateImageUpload));