/* 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 React from 'react' import styled from 'styled-components' import { observer } from 'mobx-react' import EndpointLogos from '@src/components/modules/EndpointModule/EndpointLogos' import WizardType from '@src/components/modules/WizardModule/WizardType' import Button from '@src/components/ui/Button' import InfoIcon from '@src/components/ui/InfoIcon' import WizardBreadcrumbs from '@src/components/modules/WizardModule/WizardBreadcrumbs' import WizardEndpointList from '@src/components/modules/WizardModule/WizardEndpointList' import WizardInstances from '@src/components/modules/WizardModule/WizardInstances' import WizardNetworks, { WizardNetworksChangeObject } from '@src/components/modules/WizardModule/WizardNetworks' import WizardStorage from '@src/components/modules/WizardModule/WizardStorage' import WizardOptions from '@src/components/modules/WizardModule/WizardOptions' import WizardScripts from '@src/components/modules/WizardModule/WizardScripts' import Schedule from '@src/components/modules/TransferModule/Schedule' import WizardSummary from '@src/components/modules/WizardModule/WizardSummary' import { ThemePalette, ThemeProps } from '@src/components/Theme' import { providerTypes, wizardPages, migrationFields } from '@src/constants' import configLoader from '@src/utils/Config' import type { WizardData, WizardPage } from '@src/@types/WizardData' import { Endpoint, EndpointUtils, StorageMap } from '@src/@types/Endpoint' import type { Instance, InstanceScript, } from '@src/@types/Instance' import type { Field } from '@src/@types/Field' import type { Schedule as ScheduleType } from '@src/@types/Schedule' import instanceStore from '@src/stores/InstanceStore' import providerStore from '@src/stores/ProviderStore' import endpointStore from '@src/stores/EndpointStore' import networkStore from '@src/stores/NetworkStore' import { ProviderTypes } from '@src/@types/Providers' import minionPoolStore from '@src/stores/MinionPoolStore' import LoadingButton from '@src/components/ui/LoadingButton' import transferItemIcon from './images/transferItemIcon' const Wrapper = styled.div` ${ThemeProps.exactWidth(`${parseInt(ThemeProps.contentWidth, 10) + 64}px`)} margin: 64px auto 32px auto; position: absolute; top: 0; left: 0; right: 0; bottom: 0; display: flex; flex-direction: column; ` const Header = styled.div` display: flex; position: relative; margin-bottom: 32px; align-items: center; ` const HeaderLabel = styled.div` text-align: center; font-size: 32px; font-weight: ${ThemeProps.fontWeights.light}; color: ${ThemePalette.primary}; width: 100%; ` const HeaderReload = styled.div` display: flex; align-items: center; position: absolute; right: 0; ` const HeaderReloadLabel = styled.div` font-size: 10px; color: ${ThemePalette.grayscale[4]}; &:hover { color: ${ThemePalette.primary}; } cursor: pointer; ` const Body = styled.div` flex-grow: 1; overflow: auto; display: flex; justify-content: center; padding: 0 32px; ` const Navigation = styled.div` display: flex; justify-content: space-between; padding: 16px 32px 0 32px; margin-bottom: 80px; ` const IconRepresentation = styled.div` display: flex; justify-content: center; flex-grow: 1; margin: 0 76px; ` const Footer = styled.div`` const WizardTypeIcon = styled.div` width: 60px; height: 32px; display: flex; justify-content: center; align-items: center; margin: 0 32px; ` export const isOptionsPageValid = (data: any, schema: Field[]) => { const isValid = (field: Field): boolean => { if (data) { const fieldValue = data[field.name] if (fieldValue === null) { return false } if (fieldValue === undefined) { return field.default != null } return Boolean(fieldValue) } return field.default != null } if (!schema || schema.length === 0) { return true } let required = schema.filter(f => f.required && f.type !== 'object') schema.forEach(f => { if (f.type === 'object' && f.properties && f.properties.filter && f.properties.filter(p => isValid(p)).length > 0) { required = required.concat(f.properties.filter(p => p.required)) } if (f.enum && f.subFields) { const value = data && data[f.name] const subField = f.subFields.find(sf => sf.name === `${String(value)}_options`) if (subField && subField.properties) { required = required.concat(subField.properties.filter(p => p.required)) } } }) let validFieldsCount = 0 required.forEach(f => { if (isValid(f)) { validFieldsCount += 1 } }) if (validFieldsCount === required.length) { return true } return false } type Props = { page: { id: string, title: string }, type: 'replica' | 'migration', nextButtonDisabled: boolean, providerStore: typeof providerStore, instanceStore: typeof instanceStore, networkStore: typeof networkStore, endpointStore: typeof endpointStore, minionPoolStore: typeof minionPoolStore, wizardData: WizardData, schedules: ScheduleType[], storageMap: StorageMap[], onStorageReloadClick: () => void, defaultStorage: { value: string | null, busType?: string | null } | undefined, hasStorageMap: boolean, hasSourceOptions: boolean, pages: WizardPage[], uploadedUserScripts: InstanceScript[], showLoadingButton: boolean, onTypeChange: (isReplicaChecked: boolean | null) => void, onBackClick: () => void, onNextClick: () => void, onSourceEndpointChange: (endpoint: Endpoint) => void, onTargetEndpointChange: (endpoint: Endpoint) => void, onAddEndpoint: (provider: ProviderTypes, fromSource: boolean) => void, onInstancesSearchInputChange: (searchText: string) => void, onInstancesReloadClick: () => void, onInstanceClick: (instance: Instance) => void, onInstancePageClick: (page: number) => void, onDestOptionsChange: (field: Field, value: any, parentFieldName?: string) => void, onSourceOptionsChange: (field: Field, value: any, parentFieldName?: string) => void, onNetworkChange: (changeObject: WizardNetworksChangeObject) => void, onStorageChange: (mapping: StorageMap) => void, onDefaultStorageChange: (value: string | null, busType?: string | null) => void, onAddScheduleClick: (schedule: ScheduleType) => void, onScheduleChange: (scheduleId: string, schedule: ScheduleType) => void, onScheduleRemove: (scheudleId: string) => void, onContentRef: (ref: any) => void, onReloadOptionsClick: () => void, onReloadNetworksClick: () => void, onUserScriptUpload: (instanceScript: InstanceScript) => void, onCancelUploadedScript: (global: string | null, instanceName: string | null) => void, } type TimezoneValue = 'local' | 'utc' type State = { useAdvancedOptions: boolean, timezone: TimezoneValue, } @observer class WizardPageContent extends React.Component { state: State = { useAdvancedOptions: false, timezone: 'local', } componentDidMount() { this.props.onContentRef(this) } componentWillUnmount() { this.props.onContentRef(null) } getProvidersType(type: string) { return type === 'source' ? providerTypes.SOURCE_REPLICA : providerTypes.TARGET_REPLICA } getProviders(direction: string): ProviderTypes[] { const validProviders: { [provider in ProviderTypes]: true } = {} as { [provider in ProviderTypes]: true } const providerType = this.getProvidersType(direction) const providersObject = this.props.providerStore.providers if (!providersObject) { return [] } Object.keys(providersObject).forEach(provider => { const usableProvider = provider as ProviderTypes if (providersObject[usableProvider].types.findIndex(t => t === providerType) > -1) { validProviders[usableProvider] = true } }) return this.props.providerStore.providerNames.filter(p => validProviders[p]) } areOptionsLoading(type: 'source' | 'destination'): boolean { if (type === 'source') { return this.props.providerStore.sourceSchemaLoading || this.props.providerStore.sourceOptionsPrimaryLoading || this.props.minionPoolStore.loadingMinionPools } return this.props.providerStore.destinationSchemaLoading || this.props.providerStore.destinationOptionsPrimaryLoading || this.props.minionPoolStore.loadingMinionPools } isNetworksPageValid() { if (this.props.networkStore.loading || this.props.instanceStore.loadingInstancesDetails) { return false } const instances = this.props.instanceStore.instancesDetails if (instances.length === 0) { return true } if (instances.find(i => i.devices)) { if (instances.find(i => i.devices.nics && i.devices.nics.length > 0)) { return this.props.wizardData.networks && this.props.wizardData.networks.length > 0 } return true } return false } isNextButtonDisabled() { if (this.props.nextButtonDisabled) { return true } switch (this.props.page.id) { case 'source': return !this.props.wizardData.source case 'target': return !this.props.wizardData.target case 'vms': return !this.props.wizardData.selectedInstances || !this.props.wizardData.selectedInstances.length case 'source-options': return !isOptionsPageValid( this.props.wizardData.sourceOptions, this.props.providerStore.sourceSchema, ) case 'dest-options': return !isOptionsPageValid( this.props.wizardData.destOptions, this.props.providerStore.destinationSchema, ) case 'networks': return !this.isNetworksPageValid() default: return false } } handleAdvancedOptionsToggle(useAdvancedOptions: boolean) { this.setState({ useAdvancedOptions }) } handleTimezoneChange(timezone: TimezoneValue) { this.setState({ timezone }) } renderHeader() { let title = this.props.page.title const pageId = this.props.page.id if (pageId === 'type') { title += ` ${this.props.type.charAt(0).toUpperCase() + this.props.type.substr(1)}` } const optionsReload = (type: 'source' | 'destination') => ({ label: 'Reload Options', action: () => { this.props.onReloadOptionsClick() }, tip: 'Options may be cached by the UI. Here you can reload them from the API.', visible: !this.areOptionsLoading(type), }) const reloadPages: any = { 'source-options': optionsReload('source'), 'dest-options': optionsReload('destination'), networks: { label: 'Reload Networks', action: () => { this.props.onReloadNetworksClick() }, tip: 'Networks and instances info may be cached by the UI. Here you can reload them from the API.', visible: !this.props.instanceStore.loadingInstancesDetails, }, } return (
{title} {reloadPages[pageId]?.visible ? ( { reloadPages[pageId].action() }}> {reloadPages[pageId].label} ) : null}
) } renderBody() { let body = null const getOptionsLoadingSkipFields = (type: 'source' | 'destination') => { const extraOptionsConfig = configLoader.config.extraOptionsApiCalls.find(o => { const provider = type === 'source' ? this.props.wizardData.source && this.props.wizardData.source.type : this.props.wizardData.target && this.props.wizardData.target.type return o.name === provider && o.types.find(t => t === type) }) let optionsLoadingRequiredFields: string[] = [] if (extraOptionsConfig) { optionsLoadingRequiredFields = extraOptionsConfig.requiredFields } return optionsLoadingRequiredFields } const getDefaultStorage = (): { value: string | null, busType?: string | null } => { if (this.props.defaultStorage) { return this.props.defaultStorage } if (endpointStore.storageConfigDefault) { const busTypeInfo = EndpointUtils.getBusTypeStorageId(endpointStore.storageBackends, endpointStore.storageConfigDefault || null) const defaultStorage: { value: string | null, busType?: string | null } = { value: busTypeInfo.id, } if (busTypeInfo.busType) { defaultStorage.busType = busTypeInfo.busType } return defaultStorage } return { value: null } } switch (this.props.page.id) { case 'type': body = ( ) break case 'source': body = ( { this.props.onAddEndpoint(type, true) }} /> ) break case 'target': body = ( { this.props.onAddEndpoint(type, false) }} /> ) break case 'vms': body = ( ) break case 'source-options': body = ( m.platform === 'source' && m.endpoint_id === this.props.wizardData.source?.id)} optionsLoading={this.props.providerStore.sourceOptionsSecondaryLoading} optionsLoadingSkipFields={getOptionsLoadingSkipFields('source')} fields={this.props.providerStore.sourceSchema} onChange={this.props.onSourceOptionsChange} data={this.props.wizardData.sourceOptions} useAdvancedOptions hasStorageMap={false} wizardType={`${this.props.type}-source-options`} layout="page" isSource dictionaryKey={`${this.props.wizardData.source ? this.props.wizardData.source.type : ''}-source`} /> ) break case 'dest-options': body = ( m.platform === 'destination' && m.endpoint_id === this.props.wizardData.target?.id)} optionsLoading={this.props.providerStore.destinationOptionsSecondaryLoading} optionsLoadingSkipFields={[ ...getOptionsLoadingSkipFields('destination'), 'title', 'execute_now', 'execute_now_options', ...migrationFields.map(f => f.name)]} selectedInstances={this.props.wizardData.selectedInstances} showSeparatePerVm={ Boolean(this.props.wizardData.selectedInstances && this.props.wizardData.selectedInstances.length > 1) } fields={this.props.providerStore.destinationSchema} onChange={this.props.onDestOptionsChange} data={this.props.wizardData.destOptions} useAdvancedOptions={this.state.useAdvancedOptions} hasStorageMap={this.props.hasStorageMap} storageBackends={this.props.endpointStore.storageBackends} wizardType={this.props.type} onAdvancedOptionsToggle={useAdvancedOptions => { this.handleAdvancedOptionsToggle(useAdvancedOptions) }} layout="page" dictionaryKey={`${this.props.wizardData.target ? this.props.wizardData.target.type : ''}-destination`} /> ) break case 'networks': body = ( ) break case 'storage': body = ( ) break case 'scripts': body = ( {}} /> ) break case 'schedule': body = ( { this.handleTimezoneChange(timezone) }} secondaryEmpty /> ) break case 'summary': body = ( ) break default: } return {body} } renderNavigationActions() { const sourceEndpoint = this.props.wizardData.source && this.props.wizardData.source.type const targetEndpoint = this.props.wizardData.target && this.props.wizardData.target.type const currentPageIndex = wizardPages.findIndex(p => p.id === this.props.page.id) const isLastPage = currentPageIndex === wizardPages.length - 1 return ( {this.props.showLoadingButton ? ( Loading ... ) : ( )} ) } render() { if (!this.props.page) { return null } return ( {this.renderHeader()} {this.renderBody()} ) } } export default WizardPageContent