/*
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 .
*/
// @flow
import * as React from 'react'
import styled from 'styled-components'
import { observer } from 'mobx-react'
import autobind from 'autobind-decorator'
import configLoader from '../../../utils/Config'
import StyleProps from '../../styleUtils/StyleProps'
import ToggleButtonBar from '../../atoms/ToggleButtonBar'
import FieldInput from '../../molecules/FieldInput'
import StatusImage from '../../atoms/StatusImage'
import type { Field } from '../../../types/Field'
import type { Instance } from '../../../types/Instance'
import type { StorageBackend } from '../../../types/Endpoint'
import { executionOptions, migrationFields } from '../../../constants'
import LabelDictionary from '../../../utils/LabelDictionary'
import Palette from '../../styleUtils/Palette'
import endpointImage from './images/endpoint.svg'
const Wrapper = styled.div`
display: flex;
min-height: 0;
flex-direction: column;
width: 100%;
`
const Options = styled.div`
display: flex;
flex-direction: column;
min-height: 0;
`
const Fields = styled.div`
${props => props.padding ? `padding: ${props.padding}px;` : ''}
display: flex;
flex-direction: column;
overflow: auto;
`
const Group = styled.div`
display: flex;
flex-direction: column;
flex-shrink: 0;
`
const GroupName = styled.div`
display: flex;
align-items: center;
margin: 48px 0 24px 0;
`
const GroupNameText = styled.div`
margin: 0 32px;
font-size: 16px;
`
const GroupNameBar = styled.div`
flex-grow: 1;
background: ${Palette.grayscale[3]};
height: 1px;
`
const GroupFields = styled.div`
display: flex;
justify-content: space-between;
`
const OneColumn = styled.div``
const Column = styled.div`
margin-top: -16px;
`
const FieldInputStyled = styled(FieldInput)`
width: ${props => props.width || StyleProps.inputSizes.wizard.width}px;
justify-content: space-between;
margin-top: 16px;
`
const LoadingWrapper = styled.div`
margin-top: 32px;
display: flex;
flex-direction: column;
align-items: center;
`
const LoadingText = styled.div`
margin-top: 38px;
font-size: 18px;
`
const EndpointImage = styled.div`
${StyleProps.exactSize('96px')};
background: url('${endpointImage}') center no-repeat;
`
const NoSourceFieldsWrapper = styled.div`
margin-top: 16px;
display: flex;
flex-direction: column;
align-items: center;
`
const NoSourceFieldsMessage = styled.div`
font-size: 18px;
margin-top: 16px;
`
const NoSourceFieldsSubMessage = styled.div`
margin-top: 16px;
color: ${Palette.grayscale[4]};
`
export const shouldRenderField = (field: Field) => {
return (field.type !== 'array' || (field.enum && field.enum.length && field.enum.length > 0)) &&
(field.type !== 'object' || field.properties)
}
type FieldRender = {
field: Field,
component: React.Node,
column: number,
}
type Props = {
fields: Field[],
isSource?: boolean,
selectedInstances?: ?Instance[],
data?: ?{ [string]: mixed },
getFieldValue?: (fieldName: string, defaultValue: any) => any,
onChange: (field: Field, value: any) => void,
useAdvancedOptions?: boolean,
hasStorageMap: boolean,
storageBackends?: StorageBackend[],
storageConfigDefault?: string,
onAdvancedOptionsToggle?: (showAdvanced: boolean) => void,
wizardType: string,
oneColumnStyle?: { [string]: mixed },
fieldWidth?: number,
onScrollableRef?: (ref: HTMLElement) => void,
availableHeight?: number,
layout?: 'page' | 'modal',
loading?: boolean,
optionsLoading?: boolean,
optionsLoadingSkipFields?: string[],
dictionaryKey: string,
}
@observer
class WizardOptions extends React.Component {
componentDidMount() {
window.addEventListener('resize', this.handleResize)
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize, false)
}
getFieldValue(fieldName: string, defaultValue: any) {
if (this.props.getFieldValue) {
return this.props.getFieldValue(fieldName, defaultValue)
}
if (!this.props.data || this.props.data[fieldName] === undefined) {
return defaultValue
}
return this.props.data[fieldName]
}
getDefaultFieldsSchema() {
let fieldsSchema = []
if (this.props.wizardType === 'migration' || this.props.wizardType === 'replica') {
fieldsSchema.push({ name: 'description', type: 'string' })
}
if (this.props.wizardType === 'migration' || this.props.wizardType === 'migration-destination-options-edit') {
fieldsSchema.unshift({ name: 'skip_os_morphing', type: 'boolean', default: false })
}
if (this.props.selectedInstances && this.props.selectedInstances.length > 1) {
let dictionaryLabel = LabelDictionary.get('separate_vm')
let label = this.props.wizardType === 'migration' ? dictionaryLabel : dictionaryLabel.replace('Migration', 'Replica')
fieldsSchema.unshift({ name: 'separate_vm', label, type: 'boolean', default: true })
}
if (this.props.wizardType === 'replica') {
fieldsSchema.push({ name: 'execute_now', type: 'boolean', default: true })
let executeNowValue = this.getFieldValue('execute_now', true)
if (executeNowValue) {
fieldsSchema.push({
name: 'execute_now_options',
type: 'object',
properties: executionOptions,
})
}
} else if (this.props.wizardType === 'migration' || this.props.wizardType === 'migration-destination-options-edit') {
fieldsSchema = [...fieldsSchema, ...migrationFields]
}
return fieldsSchema
}
isPassword(fieldName: string): boolean {
return fieldName.indexOf('password') > -1 || Boolean(configLoader.config.passwordFields.find(f => f === fieldName))
}
@autobind
handleResize() {
this.setState({})
}
generateGroups(fields: FieldRender[]) {
let groups: Array<{ fields: FieldRender[], name?: string }> = [{ fields }]
let workerFields = fields.filter(f => f.field.name.indexOf('migr_') === 0)
if (workerFields.length > 1) {
groups = [
{ fields: fields.filter(f => f.field.name.indexOf('migr_') === -1) },
{ name: 'Temporary Migration Worker Options', fields: workerFields.map((f, i) => ({ ...f, column: i % 2 })) },
]
}
fields.forEach(f => {
if (f.field.groupName) {
groups[0].fields = groups[0].fields ? groups[0].fields.filter(gf => gf.field.name !== f.field.name) : []
let group = groups.find(g => g.name && g.name === f.field.groupName)
if (!group) {
groups.push({
name: f.field.groupName,
fields: [f],
})
} else {
group.fields.push(f)
}
}
})
return groups
}
renderOptionsField(field: Field) {
let additionalProps
if (field.type === 'object' && field.properties) {
additionalProps = {
valueCallback: f => this.getFieldValue(f.name, f.default),
onChange: (value, f) => { this.props.onChange(f, value) },
properties: field.properties,
}
} else {
additionalProps = {
value: this.getFieldValue(field.name, field.default),
onChange: value => { this.props.onChange(field, value) },
}
}
let optionsLoadingReqFields = this.props.optionsLoadingSkipFields || []
return (
fn === field.name)}
{...additionalProps}
/>
)
}
renderNoFieldsMessage() {
return (
No Source Options
There are no options for the specified source cloud provider.
)
}
renderOptionsFields() {
if (this.props.fields.length === 0 && this.props.isSource) {
return this.renderNoFieldsMessage()
}
let fieldsSchema: Field[] = this.getDefaultFieldsSchema()
let nonNullableBooleans: string[] = fieldsSchema.filter(f => f.type === 'boolean').map(f => f.name)
fieldsSchema = fieldsSchema.concat(this.props.fields.filter(f => f.required))
if (this.props.useAdvancedOptions) {
fieldsSchema = fieldsSchema.concat(this.props.fields.filter(f => !f.required))
}
// Add subfields for enums which have them
let subFields = []
fieldsSchema.forEach(f => {
if (!f.enum || !f.subFields) {
return
}
let value = this.getFieldValue(f.name, f.default)
if (!f.subFields) {
return
}
let subField = f.subFields.find(f => f.name === `${String(value)}_options`)
if (subField && subField.properties) {
subFields = [...subFields, ...subField.properties]
}
})
fieldsSchema = [...fieldsSchema, ...subFields]
let executeNowColumn
let fields: FieldRender[] = fieldsSchema.filter(f => shouldRenderField(f)).map((field, i) => {
let column: number = i % 2
if (field.name === 'execute_now') {
executeNowColumn = column
}
if (field.name === 'execute_now_options') {
column = executeNowColumn
}
if (field.type === 'boolean' && !nonNullableBooleans.find(name => name === field.name)) {
field.nullableBoolean = true
}
return {
column,
component: this.renderOptionsField(field),
field,
}
})
let availableHeight = this.props.availableHeight || (window.innerHeight - 450)
if (fields.length * 96 < availableHeight) {
return (
{fields.map(f => f.component)}
)
}
let groups = this.generateGroups(fields)
return (
{groups.map(g => (
{g.name ? (
{g.name}
) : null}
{g.fields.map(f => f.column === 0 && f.component)}
{g.fields.map(f => f.column === 1 && f.component)}
))}
)
}
renderLoading() {
if (!this.props.loading) {
return null
}
return (
Loading options...
)
}
renderOptions() {
if (this.props.loading) {
return null
}
let onAdvancedOptionsToggle = this.props.onAdvancedOptionsToggle
return (
{onAdvancedOptionsToggle ? { onAdvancedOptionsToggle(item.value === 'advanced') }}
/> : null}
{this.renderOptionsFields()}
)
}
render() {
return (
{this.renderOptions()}
{this.renderLoading()}
)
}
}
export default WizardOptions