/* 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 . */ import * as React from 'react' import styled from 'styled-components' import { observer } from 'mobx-react' import { toJS } from 'mobx' import autobind from 'autobind-decorator' import { CSSTransitionGroup } from 'react-transition-group' import configLoader from '../../../../utils/Config' import ToggleButtonBar from '../../../ui/ToggleButtonBar/ToggleButtonBar' import FieldInput from '../../../ui/FieldInput/FieldInput' import StatusImage from '../../../ui/StatusComponents/StatusImage/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 { ThemePalette, ThemeProps } from '../../../Theme' import endpointImage from './images/endpoint.svg' import { MinionPool } from '../../../../@types/MinionPool' import { MinionPoolStoreUtils } from '../../../../stores/MinionPoolStore' export const INSTANCE_OSMORPHING_MINION_POOL_MAPPINGS = 'instance_osmorphing_minion_pool_mappings' 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; &.field-group-transition-appear { opacity: 0.01; } &.field-group-transition-appear-active { opacity: 1; transition: opacity 250ms ease-out; } ` 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: ${ThemePalette.grayscale[3]}; height: 1px; ` const GroupFields = styled.div` display: flex; justify-content: space-between; ` const Column = styled.div` margin-top: -16px; ` const FieldInputStyled = styled(FieldInput)` width: ${props => props.width || ThemeProps.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` ${ThemeProps.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: ${ThemePalette.grayscale[4]}; ` export const shouldRenderField = (field: Field) => (field.type !== 'array' || (field.enum && field.enum.length && field.enum.length > 0)) && (field.type !== 'object' || field.properties) type FieldRender = { field: Field, component: React.ReactNode, column: number, } type Props = { fields: Field[], minionPools: MinionPool[] isSource?: boolean, selectedInstances?: Instance[] | null, showSeparatePerVm?: boolean data?: { [prop: string]: any } | null, getFieldValue?: ( fieldName: string, defaultValue: any, parentFieldName: string | undefined ) => any, onChange: (field: Field, value: any, parentFieldName?: string) => void, useAdvancedOptions?: boolean, hasStorageMap: boolean, storageBackends?: StorageBackend[], onAdvancedOptionsToggle?: (showAdvanced: boolean) => void, wizardType: string, oneColumnStyle?: { [prop: string]: any }, 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, parentFieldName?: string) { if (this.props.getFieldValue) { return this.props.getFieldValue(fieldName, defaultValue, parentFieldName) } if (!this.props.data) { return defaultValue } if (parentFieldName) { if (this.props.data[parentFieldName] && this.props.data[parentFieldName][fieldName] !== undefined) { return this.props.data[parentFieldName][fieldName] } return defaultValue } if (!this.props.data || this.props.data[fieldName] === undefined) { return defaultValue } return this.props.data[fieldName] } getDefaultSimpleFieldsSchema() { let fieldsSchema: Field[] = [] if (this.props.minionPools.length) { fieldsSchema.push({ name: 'minion_pool_id', label: `${this.props.isSource ? 'Source' : 'Target'} Minion Pool`, type: 'string', enum: this.props.minionPools.map(minionPool => ({ label: minionPool.name, value: minionPool.id, disabled: !MinionPoolStoreUtils.isActive(minionPool), subtitleLabel: !MinionPoolStoreUtils.isActive(minionPool) ? `Pool is in ${minionPool.status} status instead of being ALLOCATED.` : '', })), }) } if (this.props.showSeparatePerVm) { const dictionaryLabel = LabelDictionary.get('separate_vm') const label = this.props.wizardType === 'migration' ? dictionaryLabel : dictionaryLabel.replace('Migration', 'Replica') fieldsSchema.push({ name: 'separate_vm', label, type: 'boolean', default: true, nullableBoolean: false, }) } if (this.props.wizardType === 'migration' || this.props.wizardType === 'migration-destination-options-edit') { fieldsSchema.push({ name: 'skip_os_morphing', type: 'boolean', default: false, nullableBoolean: false, }) } if (this.props.wizardType === 'migration' || this.props.wizardType === 'replica' || this.props.wizardType === 'migration-destination-options-edit' || this.props.wizardType === 'replica-destination-options-edit') { let titleFieldSchema: Field = { name: 'title', type: 'string' } if (this.props.showSeparatePerVm && this.getFieldValue('separate_vm', true)) { titleFieldSchema = { ...titleFieldSchema, disabled: true, description: 'When using \'Separate Migration/VM\', the title is automatically set based on the names of the selected instances', } } fieldsSchema.push(titleFieldSchema) } if (this.props.wizardType === 'replica') { fieldsSchema.push({ name: 'execute_now', type: 'boolean', default: true, nullableBoolean: false, }) const executeNowValue = this.getFieldValue('execute_now', true) fieldsSchema.push({ name: 'execute_now_options', type: 'object', properties: executionOptions, disabled: !executeNowValue, description: !executeNowValue ? 'Enable \'Execute Now\' to set \'Execute Now Options\'' : `Set the options for ${this.props.wizardType} execution`, }) } else if (this.props.wizardType === 'migration' || this.props.wizardType === 'migration-destination-options-edit') { fieldsSchema = [...fieldsSchema, ...migrationFields] } return fieldsSchema } getDefaultAdvancedFieldsSchema() { const fieldsSchema: Field[] = [] if (this.props.minionPools.length && this.props.selectedInstances && this.props.selectedInstances.length) { const properties: Field[] = this.props.selectedInstances.map(instance => ({ name: instance.instance_name || instance.id, label: instance.name, type: 'string', enum: this.props.minionPools.map(minionPool => ({ name: minionPool.name, id: minionPool.id, })), })) fieldsSchema.push({ name: INSTANCE_OSMORPHING_MINION_POOL_MAPPINGS, label: 'Instance OSMorphing Minion Pool Mappings', type: 'object', properties, }) } 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 }] const 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) : [] const 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) { const renderOsMorphingLabels = (propName: string) => ( propName.indexOf('/') > -1 ? propName.split('/')[propName.split('/').length - 1] : propName ) additionalProps = { valueCallback: (f: any) => this.getFieldValue(f.name, f.default, field.name), onChange: (value: any, f: any) => { this.props.onChange(f, value, field.name) }, labelRenderer: field.name === INSTANCE_OSMORPHING_MINION_POOL_MAPPINGS ? renderOsMorphingLabels : null, properties: field.properties, } } else { additionalProps = { value: this.getFieldValue(field.name, field.default, field.groupName), onChange: (value: any) => { this.props.onChange(field, value) }, } } const optionsLoadingReqFields = this.props.optionsLoadingSkipFields || [] return ( fn === field.name)} // eslint-disable-next-line react/jsx-props-no-spreading {...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.getDefaultSimpleFieldsSchema() fieldsSchema = fieldsSchema.concat(this.props.fields.filter(f => f.required)) if (this.props.useAdvancedOptions) { fieldsSchema = fieldsSchema.concat(this.getDefaultAdvancedFieldsSchema()) fieldsSchema = fieldsSchema.concat(this.props.fields.filter(f => !f.required)) } const nonNullableBooleans: string[] = fieldsSchema.filter(f => f.type === 'boolean' && f.nullableBoolean === false).map(f => f.name) // Add subfields for enums which have them let subFields: any[] = [] fieldsSchema.forEach(f => { if (!f.subFields) { return } const value = this.getFieldValue(f.name, f.default) if (!f.subFields) { return } let subField: Field | undefined if (f.type === 'boolean') { subField = value ? f.subFields[1] : f.subFields[0] } else { subField = f.subFields.find(sf => sf.name === `${String(value)}_options`) } if (subField?.properties) { subFields = [...subFields, ...subField.properties] } }) fieldsSchema = [...fieldsSchema, ...subFields] let executeNowColumn: number const 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 } const usableField = toJS(field) if (field.type === 'boolean' && !nonNullableBooleans.find(name => name === field.name)) { usableField.nullableBoolean = true } return { column, component: this.renderOptionsField(usableField), field: usableField, } }) const groups = this.generateGroups(fields) return ( {groups.map((g, i) => { const getColumnInGroup = (field: any, fieldIndex: number) => ( g.name ? fieldIndex % 2 : field.column ) return ( 0 ? 'field-group-transition' : ''} transitionAppear transitionAppearTimeout={250} in={false} > {g.name ? ( {LabelDictionary.get(g.name)} ) : null} {g.fields.map((f, j) => (getColumnInGroup(f, j) === 0 && f.component))} {g.fields.map((f, j) => getColumnInGroup(f, j) === 1 && f.component)} ) })} ) } renderLoading() { if (!this.props.loading) { return null } return ( Loading options... ) } renderOptions() { if (this.props.loading) { return null } const onAdvancedOptionsToggle = this.props.onAdvancedOptionsToggle return ( {onAdvancedOptionsToggle ? ( { onAdvancedOptionsToggle(item.value === 'advanced') }} /> ) : null} {this.renderOptionsFields()} ) } render() { return ( {this.renderOptions()} {this.renderLoading()} ) } } export default WizardOptions