/* 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