/*
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 React from 'react'
import styled from 'styled-components'
import autobind from 'autobind-decorator'
import { observer } from 'mobx-react'
import WizardTemplate from '../../templates/WizardTemplate'
import DetailsPageHeader from '../../organisms/DetailsPageHeader'
import WizardPageContent from '../../organisms/WizardPageContent'
import Modal from '../../molecules/Modal'
import Endpoint from '../../organisms/Endpoint'
import userStore from '../../../stores/UserStore'
import providerStore, { getFieldChangeOptions } from '../../../stores/ProviderStore'
import endpointStore from '../../../stores/EndpointStore'
import wizardStore from '../../../stores/WizardStore'
import instanceStore from '../../../stores/InstanceStore'
import networkStore from '../../../stores/NetworkStore'
import notificationStore from '../../../stores/NotificationStore'
import scheduleStore from '../../../stores/ScheduleStore'
import replicaStore from '../../../stores/ReplicaStore'
import KeyboardManager from '../../../utils/KeyboardManager'
import { wizardPages, executionOptions, providerTypes } from '../../../constants'
import configLoader from '../../../utils/Config'
import type { MainItem } from '../../../types/MainItem'
import type { Endpoint as EndpointType, StorageBackend } from '../../../types/Endpoint'
import type { Instance, Nic, Disk } from '../../../types/Instance'
import type { Field } from '../../../types/Field'
import type { Network, SecurityGroup } from '../../../types/Network'
import type { Schedule } from '../../../types/Schedule'
import type { WizardPage as WizardPageType } from '../../../types/WizardData'
const Wrapper = styled.div``
type Props = {
match: any,
location: { search: string },
history: any,
}
type WizardType = 'migration' | 'replica'
type State = {
type: WizardType,
showNewEndpointModal: boolean,
nextButtonDisabled: boolean,
newEndpointType: ?string,
newEndpointFromSource?: boolean,
}
@observer
class WizardPage extends React.Component {
state = {
type: 'migration',
showNewEndpointModal: false,
nextButtonDisabled: false,
newEndpointType: null,
}
contentRef: WizardPageContent
get instancesPerPage() {
const min = 3
const max = Infinity
const instancesTableDiff = 505
const instancesItemHeight = 67
return Math.min(max, Math.max(min, Math.floor((window.innerHeight - instancesTableDiff) / instancesItemHeight)))
}
get pages() {
let sourceProvider = wizardStore.data.source ? wizardStore.data.source.type : ''
let destProvider = wizardStore.data.target ? wizardStore.data.target.type || '' : ''
let pages = wizardPages
let sourceOptionsProviders = configLoader.config.sourceOptionsProviders
let hasStorageMapping = () => providerStore.providers && providerStore.providers[destProvider]
? !!providerStore.providers[destProvider].types.find(t => t === providerTypes.STORAGE) : false
return pages
.filter(p => !p.excludeFrom || p.excludeFrom !== this.state.type)
.filter(p => p.id !== 'storage' || hasStorageMapping())
.filter(p => p.id !== 'source-options'
|| sourceOptionsProviders.find(p => p === sourceProvider))
}
componentWillMount() {
this.initializeState()
this.handleResize()
}
componentDidMount() {
document.title = 'Coriolis Wizard'
KeyboardManager.onEnter('wizard', () => { this.handleEnterKey() })
KeyboardManager.onEsc('wizard', () => { this.handleEscKey() })
window.addEventListener('resize', this.handleResize)
}
componentWillReceiveProps(newProps: Props) {
if (newProps.location.search === this.props.location.search) {
return
}
wizardStore.clearData()
this.initializeState()
}
componentWillUnmount() {
wizardStore.clearData()
instanceStore.cancelIntancesChunksLoading()
KeyboardManager.removeKeyDown('wizard')
window.removeEventListener('resize', this.handleResize, false)
}
@autobind
handleResize() {
instanceStore.updateInstancesPerPage(this.instancesPerPage)
}
handleEnterKey() {
if (this.contentRef && !this.contentRef.isNextButtonDisabled()) {
this.handleNextClick()
}
}
handleEscKey() {
this.handleBackClick()
}
async handleCreationSuccess(items: MainItem[]) {
let typeLabel = this.state.type.charAt(0).toUpperCase() + this.state.type.substr(1)
notificationStore.alert(`${typeLabel} was succesfully created`, 'success')
let schedulePromise = Promise.resolve()
if (this.state.type === 'replica') {
items.forEach(replica => {
this.executeCreatedReplica(replica)
schedulePromise = this.scheduleReplica(replica)
})
}
if (items.length === 1) {
let location = `/${this.state.type}/`
if (this.state.type === 'replica') {
location += 'executions/'
} else {
location += 'tasks/'
}
await schedulePromise
this.props.history.push(location + items[0].id)
} else {
this.props.history.push(`/${this.state.type}s`)
}
}
handleUserItemClick(item: { value: string }) {
switch (item.value) {
case 'signout':
userStore.logout()
break
default:
}
}
handleTypeChange(isReplica: ?boolean) {
wizardStore.updateData({
target: null,
networks: null,
destOptions: null,
sourceOptions: null,
selectedInstances: null,
source: null,
})
wizardStore.clearStorageMap()
wizardStore.setPermalink(wizardStore.data)
this.setState({ type: isReplica ? 'replica' : 'migration' })
}
handleBackClick() {
let currentPageIndex = this.pages.findIndex(p => p.id === wizardStore.currentPage.id)
if (currentPageIndex === 0) {
window.history.back()
return
}
let page = this.pages[currentPageIndex - 1]
this.loadDataForPage(page)
wizardStore.setCurrentPage(page)
}
handleNextClick() {
let currentPageIndex = this.pages.findIndex(p => p.id === wizardStore.currentPage.id)
if (currentPageIndex === this.pages.length - 1) {
this.create()
return
}
let page = this.pages[currentPageIndex + 1]
this.loadDataForPage(page)
wizardStore.setCurrentPage(page)
}
async handleSourceEndpointChange(source: ?EndpointType) {
wizardStore.updateData({ source, selectedInstances: null, networks: null, sourceOptions: null })
wizardStore.clearStorageMap()
wizardStore.setPermalink(wizardStore.data)
if (!source) {
return
}
await providerStore.loadOptionsSchema({
providerName: source.type,
schemaType: this.state.type,
optionsType: 'source',
useCache: true,
})
source && providerStore.getOptionsValues({
optionsType: 'source',
endpointId: source.id,
providerName: source.type,
useCache: true,
})
}
async handleTargetEndpointChange(target: EndpointType) {
wizardStore.updateData({ target, networks: null, destOptions: null })
wizardStore.clearStorageMap()
wizardStore.setPermalink(wizardStore.data)
if (this.pages.find(p => p.id === 'storage')) {
endpointStore.loadStorage(target.id, {})
}
// Preload destination options schema
await providerStore.loadOptionsSchema({
providerName: target.type,
schemaType: this.state.type,
optionsType: 'destination',
useCache: true,
})
// Preload destination options values
providerStore.getOptionsValues({
optionsType: 'destination',
endpointId: target.id,
providerName: target.type,
useCache: true,
})
}
handleAddEndpoint(newEndpointType: string, newEndpointFromSource: boolean) {
this.setState({
showNewEndpointModal: true,
newEndpointType,
newEndpointFromSource,
})
}
handleCloseNewEndpointModal(options?: { autoClose?: boolean }) {
if (options) {
if (this.state.newEndpointFromSource) {
wizardStore.updateData({ source: endpointStore.endpoints[0] })
} else {
wizardStore.updateData({ target: endpointStore.endpoints[0] })
}
}
wizardStore.setPermalink(wizardStore.data)
this.setState({ showNewEndpointModal: false })
}
handleInstancesSearchInputChange(searchText: string) {
if (wizardStore.data.source) {
instanceStore.searchInstances(wizardStore.data.source, searchText)
}
}
handleInstancesReloadClick() {
if (wizardStore.data.source) {
instanceStore.reloadInstances(wizardStore.data.source, this.instancesPerPage, wizardStore.data.sourceOptions)
}
}
handleInstanceClick(instance: Instance) {
wizardStore.updateData({ networks: null })
wizardStore.clearStorageMap()
wizardStore.toggleInstanceSelection(instance)
wizardStore.setPermalink(wizardStore.data)
}
handleInstancePageClick(page: number) {
instanceStore.setPage(page)
}
handleDestOptionsChange(field: Field, value: any) {
wizardStore.updateData({ networks: null })
wizardStore.clearStorageMap()
wizardStore.updateDestOptions({ field, value })
// If the field is a string and doesn't have an enum property,
// we can't call destination options on "change" since too many calls will be made,
// it also means a potential problem with the server not populating the "enum" prop.
// Otherwise, the field has enum property, which there potentially other destination options for the new
// chosen value from the enum
if (field.type !== 'string' || field.enum) {
this.loadExtraOptions(field, 'destination')
}
wizardStore.setPermalink(wizardStore.data)
}
handleSourceOptionsChange(field: Field, value: any) {
wizardStore.updateData({ selectedInstances: [] })
wizardStore.updateSourceOptions({ field, value })
if (field.type !== 'string' || field.enum) {
this.loadExtraOptions(field, 'source')
}
wizardStore.setPermalink(wizardStore.data)
}
handleNetworkChange(sourceNic: Nic, targetNetwork: Network, targetSecurityGroups: ?SecurityGroup[]) {
wizardStore.updateNetworks({ sourceNic, targetNetwork, targetSecurityGroups })
wizardStore.setPermalink(wizardStore.data)
}
handleStorageChange(source: Disk, target: StorageBackend, type: 'backend' | 'disk') {
wizardStore.updateStorage({ source, target, type })
}
handleAddScheduleClick(schedule: Schedule) {
wizardStore.addSchedule(schedule)
}
handleScheduleChange(scheduleId: string, data: Schedule) {
wizardStore.updateSchedule(scheduleId, data)
}
handleScheduleRemove(scheduleId: string) {
wizardStore.removeSchedule(scheduleId)
}
async handleReloadOptionsClick() {
let optionsType: 'source' | 'destination' = wizardStore.currentPage.id === 'source-options' ? 'source' : 'destination'
let endpoint = optionsType === 'source' ? wizardStore.data.source : wizardStore.data.target
if (!endpoint) {
return
}
await providerStore.loadOptionsSchema({
providerName: endpoint.type,
schemaType: this.state.type,
optionsType,
})
await providerStore.getOptionsValues({
optionsType,
endpointId: endpoint.id,
providerName: endpoint.type,
})
await this.loadExtraOptions(undefined, optionsType, false)
}
initializeState() {
wizardStore.getDataFromPermalink()
let type = this.props.match && this.props.match.params.type
if (type === 'migration' || type === 'replica') {
this.setState({ type })
}
}
loadExtraOptions(field?: Field, type: 'source' | 'destination', useCache: boolean = true) {
let endpoint = type === 'source' ? wizardStore.data.source : wizardStore.data.target
if (!endpoint) {
return
}
let envData = getFieldChangeOptions({
providerName: endpoint.type,
schema: type === 'source' ? providerStore.sourceSchema : providerStore.destinationSchema,
data: type === 'source' ? wizardStore.data.sourceOptions : wizardStore.data.destOptions,
field,
type,
})
if (!envData) {
return
}
providerStore.getOptionsValues({
optionsType: type,
endpointId: endpoint.id,
providerName: endpoint.type,
envData,
useCache,
})
}
async loadDataForPage(page: WizardPageType) {
const loadOptions = async (endpoint: EndpointType, optionsType: 'source' | 'destination') => {
let schema = optionsType === 'source' ? providerStore.sourceSchema : providerStore.destinationSchema
if (schema.length > 0) {
return
}
await providerStore.loadOptionsSchema({
providerName: endpoint.type,
schemaType: this.state.type,
optionsType,
useCache: true,
})
// Preload source options if data is set from 'Permalink'
if (providerStore.sourceOptions.length === 0) {
await providerStore.getOptionsValues({
optionsType,
endpointId: endpoint.id,
providerName: endpoint.type,
useCache: true,
})
await this.loadExtraOptions(undefined, optionsType)
}
}
switch (page.id) {
case 'source': {
providerStore.loadProviders()
endpointStore.getEndpoints()
// Preload instances if data is set from 'Permalink'
let source = wizardStore.data.source
if (!source) {
return
}
// Preload source options schema
loadOptions(source, 'source')
break
}
case 'vms': {
if (!wizardStore.data.source) {
return
}
instanceStore.loadInstancesInChunks({
endpoint: wizardStore.data.source,
vmsPerPage: this.instancesPerPage,
env: wizardStore.data.sourceOptions,
useCache: true,
})
break
}
case 'target': {
let target = wizardStore.data.target
if (!target) {
return
}
// Preload Storage Mapping
if (this.pages.find(p => p.id === 'storage')) {
endpointStore.loadStorage(target.id, {})
}
// Preload destination options schema
loadOptions(target, 'destination')
break
}
case 'networks':
this.loadNetworks(true)
break
default:
}
}
loadNetworks(cache: boolean) {
if (wizardStore.data.source && wizardStore.data.selectedInstances) {
instanceStore.loadInstancesDetails({
endpointId: wizardStore.data.source.id,
instancesInfo: wizardStore.data.selectedInstances,
env: wizardStore.data.sourceOptions,
cache,
})
}
if (wizardStore.data.target) {
let id = wizardStore.data.target.id
networkStore.loadNetworks(id, wizardStore.data.destOptions, { cache })
}
}
async createMultiple() {
let typeLabel = this.state.type.charAt(0).toUpperCase() + this.state.type.substr(1)
notificationStore.alert(`Creating ${typeLabel}s ...`)
await wizardStore.createMultiple(this.state.type, wizardStore.data, wizardStore.storageMap)
let items = wizardStore.createdItems
if (!items) {
notificationStore.alert(`${typeLabel}s couldn't be created`, 'error')
this.setState({ nextButtonDisabled: false })
return
}
this.handleCreationSuccess(items)
}
async createSingle() {
let typeLabel = this.state.type.charAt(0).toUpperCase() + this.state.type.substr(1)
notificationStore.alert(`Creating ${typeLabel} ...`)
try {
await wizardStore.create(this.state.type, wizardStore.data, wizardStore.storageMap)
let item = wizardStore.createdItem
if (!item) {
notificationStore.alert(`${typeLabel} couldn't be created`, 'error')
this.setState({ nextButtonDisabled: false })
return
}
this.handleCreationSuccess([item])
} catch (err) {
this.setState({ nextButtonDisabled: false })
}
}
separateVms() {
let data = wizardStore.data
let separateVms = true
if (data.destOptions && data.destOptions.separate_vm != null) {
separateVms = data.destOptions.separate_vm
}
if (data.selectedInstances && data.selectedInstances.length === 1) {
separateVms = false
}
if (separateVms) {
this.createMultiple()
} else {
this.createSingle()
}
}
create() {
this.setState({ nextButtonDisabled: true })
this.separateVms()
}
scheduleReplica(replica: MainItem): Promise {
if (wizardStore.schedules.length === 0) {
return Promise.resolve()
}
return scheduleStore.scheduleMultiple(replica.id, wizardStore.schedules)
}
executeCreatedReplica(replica: MainItem) {
let options = wizardStore.data.destOptions
let executeNow = true
if (options && options.execute_now != null) {
executeNow = options.execute_now
}
if (!executeNow) {
return
}
let executeNowOptions = executionOptions.map(field => {
if (options && options[field.name] != null) {
return { name: field.name, value: options[field.name] }
}
return field
})
replicaStore.execute(replica.id, executeNowOptions)
}
render() {
return (
{ this.handleUserItemClick(item) }}
/>}
pageContentComponent={ p.id === 'storage'))}
hasSourceOptions={Boolean(this.pages.find(p => p.id === 'source-options'))}
storageMap={wizardStore.storageMap}
schedules={wizardStore.schedules}
nextButtonDisabled={this.state.nextButtonDisabled}
type={this.state.type}
onTypeChange={isReplica => { this.handleTypeChange(isReplica) }}
onBackClick={() => { this.handleBackClick() }}
onNextClick={() => { this.handleNextClick() }}
onSourceEndpointChange={endpoint => { this.handleSourceEndpointChange(endpoint) }}
onTargetEndpointChange={endpoint => { this.handleTargetEndpointChange(endpoint) }}
onAddEndpoint={(type, fromSource) => { this.handleAddEndpoint(type, fromSource) }}
onInstancesSearchInputChange={searchText => { this.handleInstancesSearchInputChange(searchText) }}
onInstancesReloadClick={() => { this.handleInstancesReloadClick() }}
onInstanceClick={instance => { this.handleInstanceClick(instance) }}
onInstancePageClick={page => { this.handleInstancePageClick(page) }}
onDestOptionsChange={(field, value) => { this.handleDestOptionsChange(field, value) }}
onSourceOptionsChange={(field, value) => { this.handleSourceOptionsChange(field, value) }}
onNetworkChange={(sourceNic, targetNetwork, secGroups) => { this.handleNetworkChange(sourceNic, targetNetwork, secGroups) }}
onStorageChange={(source, target, type) => { this.handleStorageChange(source, target, type) }}
onAddScheduleClick={schedule => { this.handleAddScheduleClick(schedule) }}
onScheduleChange={(scheduleId, data) => { this.handleScheduleChange(scheduleId, data) }}
onScheduleRemove={scheduleId => { this.handleScheduleRemove(scheduleId) }}
onContentRef={ref => { this.contentRef = ref }}
onReloadOptionsClick={() => { this.handleReloadOptionsClick() }}
onReloadNetworksClick={() => { this.loadNetworks(false) }}
/>}
/>
{ this.handleCloseNewEndpointModal() }}
>
{ this.handleCloseNewEndpointModal(autoClose) }}
/>
)
}
}
export default WizardPage