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