/*
Copyright (C) 2017 Cloudbase Solutions SRL
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
*/
/* eslint-disable dot-notation */
import React, { PropTypes } from 'react';
import withStyles from 'isomorphic-style-loader/lib/withStyles';
import s from './AddCloudConnection.scss';
import Reflux from 'reflux';
import Helper from "../Helper"
import ConnectionsStore from '../../stores/ConnectionsStore';
import ConnectionsActions from '../../actions/ConnectionsActions';
import NotificationActions from '../../actions/NotificationActions';
import Dropdown from '../NewDropdown';
import Switch from '../Switch'
import LoadingIcon from "../LoadingIcon/LoadingIcon";
const title = 'Add Cloud Connection';
const endpointStatuses = { IDLE: 0, VALIDATING: 1, ERROR: 2, SUCCESS: 3 }
const submissionTypes = { ADD: 0, EDIT: 1 }
class AddCloudConnection extends Reflux.Component {
static contextTypes = {
onSetTitle: PropTypes.func.isRequired,
};
static defaultProps = {
cloud: null,
connection: null,
type: "new",
onResizeUpdate: () => {}
}
constructor(props) {
super(props)
this.store = ConnectionsStore
this.state = {
submissionType: submissionTypes.ADD,
endpointStatus: endpointStatuses.IDLE,
showErrorMessage: false,
type: props.type, // type of operation: new/edit
connection: props.connection, // connection object (on edit)
connectionName: "", // connection name field
description: "", // connection description field
currentCloud: this.props.cloud, // chosen cloud - if adding a new endpoint
currentCloudData: null, // endpoint field data
requiredFields: [], // array that holds all the endpoint required fields - used for field validation
cloudFormsSubmitted: false // flag that indicates if the form has been submitted - used for field validation
}
}
componentWillMount() {
super.componentWillMount.call(this)
this.componentWillUnmount = false
this.context.onSetTitle(title);
if (this.state.currentCloudData == null) {
this.setState({ currentCloudData: {} })
}
this.setState({ submissionType: this.props.type === 'new' ? submissionTypes.ADD : submissionTypes.EDIT })
}
componentWillUnmount() {
super.componentWillUnmount.call(this)
this.componentWillUnmount = true
clearTimeout(this.closeTimeout)
}
componentDidMount() {
if (this.state.connection) {
this.state.allClouds.forEach(item => {
if (item.name === this.state.connection.type) {
let credentials = this.state.connection.credentials
let newCredentials = {}
for (let i in credentials) {
if (typeof credentials[i] == "object") {
newCredentials['login_type'] = i
// credentials['user_credentials'] = {}
for (let j in credentials[i]) {
// credentials['user_credentials'][j] = credentials[i][j]
newCredentials[j] = credentials[i][j]
}
} else if (typeof credentials[i] === 'boolean') {
newCredentials[i] = credentials[i]
} else {
newCredentials[i] = credentials[i] + ""
}
}
this.setState({
currentCloudData: newCredentials,
connectionName: this.state.connection.name,
description: this.state.connection.description
}, () => {
this.chooseCloud(item)
})
}
})
} else if (this.props.cloud) {
this.chooseCloud(this.props.cloud)
}
// Fixes an issue with focus when multiple modals are rendered and escape key is not captured.
// Test with adding cloud connection from wizard.
setTimeout(() => { this.rootDiv.focus() }, 100)
}
/**
* Function called upon saving an endpoint - handles both new and edit operations
*/
handleSave() {
let valid = true
let requiredFields = this.state.requiredFields
for (let i in this.state.currentCloudData) {
if (requiredFields.indexOf(i) > -1 && !this.state.currentCloudData[i]) {
valid = false
}
}
requiredFields.forEach((field) => {
if (!this.state.currentCloudData[field]) {
valid = false
}
})
if (this.state.connectionName.trim().length == 0) {
valid = false
}
if (!valid) {
NotificationActions.notify("Please fill all required fields", "error")
this.setState({ cloudFormsSubmitted: true })
} else {
let credentials = Object.assign({}, this.state.currentCloudData)
for (let key in credentials) {
if (credentials[key].label) {
credentials[key] = credentials[key].value
}
let field = this.state.currentCloud.endpoint.fields.find(function findByName(f) { return f.name == this }, key);
if (!field || !field.dataType) {
continue;
}
// Convert datatype
switch (field.dataType) {
case 'boolean':
credentials[key] = (credentials[key] === true ||
((typeof credentials[key] === 'string' || credentials[key] instanceof String) &&
credentials[key].toLowerCase() == "true"));
break;
case 'integer':
let value = parseInt(credentials[key], 10);
if (value.toString() != credentials[key]) {
valid = false;
NotificationActions.notify('"' + key + '" needs to be an integer', 'error');
}
credentials[key] = value;
break;
default:
// retain original value
break;
}
}
if (!valid) {
return;
}
// If there's a switch radio, create a hierarchical structure with the selected radio as the root.
this.state.currentCloud.endpoint.fields.forEach(field => {
if (field.type === 'switch-radio') {
credentials[credentials[field.name]] = {}
field.options.forEach(fieldOptions => {
if (fieldOptions.value === credentials[field.name]) {
fieldOptions.fields.forEach(fieldOptionField => {
credentials[credentials[field.name]][fieldOptionField.name] = credentials[fieldOptionField.name]
})
}
})
}
})
// If endpoint is new
if (this.state.type === 'new') {
this.setState({ type: 'edit' })
ConnectionsActions.newEndpoint({
name: this.state.connectionName,
description: this.state.description,
type: this.state.currentCloud.name,
connection_info: credentials
}, (response) => {
this.validateEndpoint(response.data.endpoint)
if (this.props.onConnectionAdded) {
this.props.onConnectionAdded(response.data.endpoint)
}
})
this.setState({ endpointStatus: endpointStatuses.VALIDATING })
} else { // If editing an endpoint
ConnectionsActions.editEndpoint(this.state.connection, {
name: this.state.connectionName,
description: this.state.description,
connection_info: credentials
}, (response) => {
this.validateEndpoint(response.data.endpoint)
if (this.props.onConnectionAdded && this.submissionType === submissionTypes.ADD) {
this.props.onConnectionAdded(response.data.endpoint)
}
})
this.setState({ endpointStatus: endpointStatuses.VALIDATING })
}
}
}
validateEndpoint(endpoint) {
if (this.componentWillUnmount && this.state.submissionType === submissionTypes.ADD) {
ConnectionsActions.deleteConnection(endpoint)
return
}
this.setState({ connection: endpoint })
ConnectionsActions.validateConnection(endpoint, response => {
let validation = response.data["validate-connection"]
if (validation.valid) {
this.setState({ endpointStatus: endpointStatuses.SUCCESS })
} else {
this.setState({
endpointStatus: endpointStatuses.ERROR,
errorMessage: validation.message
})
}
}, () => {
this.setState({ endpointStatus: endpointStatuses.ERROR })
})
}
/**
* Handles change `name` property
* @param e
*/
handleChangeName(e) {
this.setState({ connectionName: e.target.value })
}
/**
* Handles change `description` property
* @param e
*/
handleChangeDescription(e) {
this.setState({ description: e.target.value })
}
/**
* Handler to choose the cloud which the endpoint will be assigned to
* @param cloud
*/
chooseCloud(cloud) {
let currentCloudData = {}
if (this.state.currentCloudData !== null) {
currentCloudData = this.state.currentCloudData
}
let requiredFields = []
cloud.endpoint.fields.forEach(field => {
if (typeof currentCloudData[field.name] == "undefined") {
if (typeof field.defaultValue === 'undefined') {
currentCloudData[field.name] = "";
} else {
currentCloudData[field.name] = field.defaultValue.toString();
}
}
if (field.required) {
requiredFields.push(field.name)
}
})
this.props.onResizeUpdate()
this.setState({
currentCloud: cloud,
currentCloudData: currentCloudData,
requiredFields: requiredFields
}, this.setDefaultValues)
}
/**
* Function that goes back from endpoint validation to edit mode
*/
backToEdit() {
this.props.onResizeUpdate()
this.setState({ validateEndpoint: null })
}
/**
* Handles back operation when adding a new endpoint and want to switch cloud. Resets all previous cloud data.
*/
handleBack() {
this.props.onResizeUpdate()
this.setState({
currentCloudData: null,
currentCloud: null,
requiredFields: null,
connectionName: "",
description: null
})
}
/**
* Handles cancel edit/add endpoint
*/
handleCancel() {
if (this.state.submissionType === submissionTypes.ADD && this.state.connection && this.state.connection.id) {
ConnectionsActions.deleteConnection(this.state.connection)
}
this.props.closeHandle();
}
handleClose() {
this.props.closeHandle();
}
/**
* Sets default values for cloud fields
*/
setDefaultValues() {
this.state.currentCloud.endpoint.fields.forEach(field => {
let currentCloudData = this.state.currentCloudData
switch (field.type) {
case 'switch':
if (field.default && typeof currentCloudData[field.name] == "undefined") {
currentCloudData[field.name] = field.default
this.setState({ currentCloudData: currentCloudData })
}
break
case 'switch-radio':
field.options.forEach(option => {
if (option.default && !currentCloudData[field.name]) {
currentCloudData[field.name] = option.value
this.setRadioRequiredFields(field, option.value)
this.setState({ currentCloudData: currentCloudData })
}
}, this)
break;
case 'text':
if (field.default && typeof currentCloudData[field.name] == "undefined") {
currentCloudData[field.name] = field.default
this.setState({ currentCloudData: currentCloudData })
}
break
default:
break;
}
}, this)
}
/**
* Checks whether the field is valid. Only goes through validation if field is required
* @param field
* @returns {boolean}
*/
isValid(field) {
if (field.required && this.state.cloudFormsSubmitted) {
if (this.state.currentCloudData[field.name]) {
return !(this.state.currentCloudData[field.name] && this.state.currentCloudData[field.name].length == 0);
} else {
return false
}
} else {
return true
}
}
/**
* Dinamically change the required fields affected by the current radio selection
* @param field
* @param currentValue
*/
setRadioRequiredFields(field, currentValue) {
let requiredFields = this.state.requiredFields || [];
// Remove fields set by previous radio change
field.options.forEach(option => {
option.fields.forEach(f => {
requiredFields = requiredFields.filter(rf => rf !== f.name)
})
})
field.options.forEach(option => {
if (option.value === currentValue) {
option.fields.forEach(optionField => {
if (optionField.required) {
requiredFields.push(optionField.name);
}
})
}
})
this.setState({ requiredFields: requiredFields });
}
areFieldsDisabled() {
return (this.state.endpointStatus === endpointStatuses.VALIDATING
|| this.state.endpointStatus === endpointStatuses.SUCCESS)
}
/**
* Handler to change the endpoint field
* @param e
* @param field
*/
handleCloudFieldChange(e, field) {
let currentCloudData = this.state.currentCloudData
if (field.type == 'dropdown') {
currentCloudData[field.name] = e.value
} else if (field.type === 'switch') {
currentCloudData[field.name] = e.target.checked
} else {
currentCloudData[field.name] = e.target.value
}
if (field.type === 'switch-radio') {
this.setRadioRequiredFields(field, e.target.value)
}
this.setState({ currentCloudData: currentCloudData })
}
handleCopyErrorClick() {
let succesful = Helper.copyTextToClipboard(this.state.errorMessage)
if (succesful) {
NotificationActions.notify('The error message has been copied to clipboard.')
} else {
NotificationActions.notify('The error message couldn\'t be copied', 'error')
}
}
handleShowErrorClick() {
this.setState({
showErrorMessage: !this.state.showErrorMessage
})
}
/**
* Renders the cloud list
* @returns {XML}
*/
renderCloudList() {
let clouds = this.state.allClouds.map((cloud, index) => {
let colorType = ""
if (cloud.credentials != null && cloud.credentials.length != 0) {
colorType = ""
}
return (
this.chooseCloud(cloud)}
>
)
}, this)
return (
{clouds}
)
}
/**
* Renders individual cloud fields
* @param field
* @returns {XML}
*/
renderField(field) {
let returnValue
switch (field.type) {
case "text":
returnValue = (
)
break;
case "password":
returnValue = (
)
break;
case 'switch':
returnValue = (
{field.label + (field.required ? " *" : "")}
this.handleCloudFieldChange(e, field)}
checkedLabel="Yes"
uncheckedLabel="No"
/>
)
break
case "dropdown":
returnValue = (
{field.label + (field.required ? " *" : "")}
this.handleCloudFieldChange(e, field)}
placeholder="Choose a value"
value={field.options.find(function findOption(option) { return option.value == this},
this.state.currentCloudData[field.name])}
/>
)
break;
case "switch-radio":
let fields = ""
field.options.forEach((option) => {
if (option.value == this.state.currentCloudData[field.name]) {
fields = option.fields.map((optionField) => this.renderField(optionField))
}
})
let radioOptions = field.options.map((option, key) => (
this.handleCloudFieldChange(e, field)}
/>
)
)
returnValue = (
{ radioOptions }
{fields}
)
break;
default:
break
}
return returnValue
}
renderEndpointErrorMessage() {
if (this.state.endpointStatus !== endpointStatuses.ERROR || !this.state.showErrorMessage) {
return null
}
return (
this.handleCopyErrorClick()}
onMouseDown={e => e.stopPropagation()}
onMouseUp={e => e.stopPropagation()}
>
{this.state.errorMessage}
)
}
renderEndpointErrorTitle() {
let errorMessage = null
if (this.state.errorMessage) {
errorMessage = (
{ this.handleShowErrorClick() }}
>{this.state.showErrorMessage ? 'Hide Error' : 'Show Error'}
)
}
return (
Validation Failed
{errorMessage}
)
}
renderEndpointStatus() {
if (this.state.endpointStatus === endpointStatuses.SUCCESS) {
clearTimeout(this.closeTimeout)
this.closeTimeout = setTimeout(() => {
this.closeTimeout = null
this.handleClose()
}, 2000)
}
let endpointStatus = null
if (this.state.endpointStatus === endpointStatuses.ERROR ||
this.state.endpointStatus === endpointStatuses.SUCCESS) {
let icon = 'successIcon'
let content = 'Endpoint is Valid'
if (this.state.endpointStatus === endpointStatuses.ERROR) {
icon = 'errorIcon'
content = this.renderEndpointErrorTitle()
}
endpointStatus = (
{this.renderEndpointErrorMessage()}
)
}
return endpointStatus
}
renderButtons() {
let cancelButton = (this.state.type == "new" && this.props.cloud == null) ?
:
let saveButtonContent = 'Validate and Save'
if (this.state.endpointStatus === endpointStatuses.VALIDATING ||
this.state.endpointStatus === endpointStatuses.SUCCESS) {
let text = this.state.endpointStatus === endpointStatuses.VALIDATING ? 'Validating' : 'Saving'
saveButtonContent = {text} ...
}
let saveButton = (
)
return (
{cancelButton}
{saveButton}
)
}
/**
* Renders the new/edit endpoint form
* @param cloud
* @returns {XML}
*/
renderCloudFields(cloud) {
let fields = cloud.endpoint.fields.map(field => this.renderField(field), this)
return (
{this.renderEndpointStatus()}
6 ? " " + s.larger : "")}>
{fields}
{this.renderButtons()}
)
}
render() {
let modalBody
if (this.state.currentCloud == null) {
if (this.state.allClouds) {
modalBody = this.renderCloudList()
} else {
modalBody =
}
} else {
modalBody = this.renderCloudFields(this.state.currentCloud)
}
return (
{ this.rootDiv = rootDiv }}>
{this.props.type === 'edit' ? 'Edit Cloud Connection' : title}
{modalBody}
);
}
}
export default withStyles(AddCloudConnection, s);