| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529 |
- /*
- Copyright (C) 2019 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 <http://www.gnu.org/licenses/>.
- */
- // @flow
- import React from 'react'
- import { observer } from 'mobx-react'
- import styled from 'styled-components'
- import providerStore, { getFieldChangeOptions } from '../../../stores/ProviderStore'
- import replicaStore from '../../../stores/ReplicaStore'
- import migrationStore from '../../../stores/MigrationStore'
- import endpointStore from '../../../stores/EndpointStore'
- import Button from '../../atoms/Button'
- import StatusImage from '../../atoms/StatusImage'
- import Modal from '../../molecules/Modal'
- import Panel from '../../molecules/Panel'
- import { isOptionsPageValid } from '../../organisms/WizardPageContent'
- import WizardNetworks from '../../organisms/WizardNetworks'
- import WizardOptions from '../../organisms/WizardOptions'
- import WizardStorage from '../WizardStorage/WizardStorage'
- import type { MainItem, UpdateData } from '../../../types/MainItem'
- import type { NavigationItem } from '../../molecules/Panel'
- import type { Endpoint, StorageBackend, StorageMap } from '../../../types/Endpoint'
- import type { Field } from '../../../types/Field'
- import type { Instance, Nic, Disk } from '../../../types/Instance'
- import type { Network, NetworkMap } from '../../../types/Network'
- import { providerTypes } from '../../../constants'
- import configLoader from '../../../utils/Config'
- import StyleProps from '../../styleUtils/StyleProps'
- const PanelContent = styled.div`
- padding: 32px;
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- flex-grow: 1;
- min-height: 0;
- `
- const LoadingWrapper = styled.div`
- display: flex;
- flex-direction: column;
- align-items: center;
- margin: 32px 0;
- `
- const LoadingText = styled.div`
- font-size: 18px;
- margin-top: 32px;
- `
- const Buttons = styled.div`
- margin-top: 32px;
- display: flex;
- flex-shrink: 0;
- justify-content: space-between;
- `
- type Props = {
- type?: 'replica' | 'migration',
- isOpen: boolean,
- onRequestClose: () => void,
- onUpdateComplete: (redirectTo: string) => void,
- replica: MainItem,
- destinationEndpoint: Endpoint,
- sourceEndpoint: Endpoint,
- instancesDetails: Instance[],
- instancesDetailsLoading: boolean,
- networks: Network[],
- networksLoading: boolean,
- onReloadClick: () => void,
- }
- type State = {
- selectedPanel: ?string,
- destinationData: any,
- sourceData: any,
- updateDisabled: boolean,
- selectedNetworks: NetworkMap[],
- storageMap: StorageMap[],
- }
- @observer
- class EditReplica extends React.Component<Props, State> {
- state = {
- selectedPanel: null,
- destinationData: {},
- sourceData: {},
- updateDisabled: false,
- selectedNetworks: [],
- storageMap: [],
- }
- scrollableRef: HTMLElement
- componentWillMount() {
- this.loadData(true)
- this.setState({ selectedPanel: this.hasSourceOptions() ? 'source_options' : 'dest_options' })
- }
- async loadData(useCache: boolean) {
- await providerStore.loadProviders()
- if (this.hasStorageMap()) {
- endpointStore.loadStorage(this.props.destinationEndpoint.id, {})
- }
- this.loadDestinationOptions(useCache)
- if (!this.hasSourceOptions()) {
- return
- }
- this.loadOptions(this.props.sourceEndpoint, 'source', useCache)
- }
- async loadDestinationOptions(useCache: boolean) {
- await this.loadOptions(this.props.destinationEndpoint, 'destination', useCache)
- this.loadEnvDestinationOptions()
- }
- async loadOptions(endpoint: Endpoint, optionsType: 'source' | 'destination', useCache: boolean) {
- await providerStore.loadOptionsSchema({
- providerName: endpoint.type,
- schemaType: this.props.type || 'replica',
- optionsType,
- useCache,
- })
- await providerStore.getOptionsValues({
- optionsType,
- endpointId: endpoint.id,
- providerName: endpoint.type,
- useCache,
- })
- }
- hasStorageMap(): boolean {
- return providerStore.providers && providerStore.providers[this.props.destinationEndpoint.type] ?
- !!providerStore.providers[this.props.destinationEndpoint.type].types.find(t => t === providerTypes.STORAGE)
- : false
- }
- hasSourceOptions(): boolean {
- return Boolean(configLoader.config.sourceOptionsProviders.find(p => p === this.props.sourceEndpoint.type))
- }
- isUpdateDisabled() {
- let isLoadingDestOptions = this.state.selectedPanel === 'dest_options'
- && (providerStore.destinationSchemaLoading || providerStore.destinationOptionsLoading)
- let isLoadingSourceOptions = this.state.selectedPanel === 'source_options'
- && (providerStore.sourceSchemaLoading || providerStore.sourceOptionsLoading)
- let isLoadingNetwork = this.state.selectedPanel === 'network_mapping' && this.props.instancesDetailsLoading
- let isLoadingStorage = this.state.selectedPanel === 'storage_mapping'
- && (this.props.instancesDetailsLoading || endpointStore.storageLoading)
- return this.state.updateDisabled || isLoadingSourceOptions || isLoadingDestOptions || isLoadingNetwork || isLoadingStorage
- }
- parseReplicaData(environment: ?{ [string]: mixed }) {
- let data = {}
- let env = environment
- if (!env) {
- return data
- }
- Object.keys(env).forEach(key => {
- if (env[key] && typeof env[key] === 'object' && !Array.isArray(JSON.parse(JSON.stringify(env[key])))) {
- Object.keys(env[key]).forEach(subkey => {
- let destParent: any = env[key]
- if (destParent[subkey]) {
- data[`${key}/${subkey}`] = destParent[subkey]
- }
- })
- } else {
- data[key] = env[key]
- }
- })
- return data
- }
- loadEnvDestinationOptions(field?: Field) {
- let envData = getFieldChangeOptions({
- providerName: this.props.destinationEndpoint.type,
- schema: providerStore.destinationSchema,
- data: {
- ...this.parseReplicaData(this.props.replica.destination_environment),
- ...this.state.destinationData,
- },
- field,
- type: 'destination',
- })
- if (envData) {
- providerStore.getOptionsValues({
- optionsType: 'destination',
- endpointId: this.props.destinationEndpoint.id,
- providerName: this.props.destinationEndpoint.type,
- useCache: true,
- envData,
- })
- }
- }
- validateOptions(type: 'source' | 'destination') {
- let env = type === 'source' ? this.props.replica.source_environment : this.props.replica.destination_environment
- let data = type === 'source' ? this.state.sourceData : this.state.destinationData
- let schema = type === 'source' ? providerStore.sourceSchema : providerStore.destinationSchema
- let isValid = isOptionsPageValid({
- ...this.parseReplicaData(env),
- ...data,
- }, schema)
- this.setState({ updateDisabled: !isValid })
- }
- handlePanelChange(panel: string) {
- this.setState({ selectedPanel: panel })
- }
- handleReload() {
- this.props.onReloadClick()
- this.loadData(false)
- }
- handleFieldChange(type: 'source' | 'destination', field: Field, value: any) {
- let data = type === 'source' ? { ...this.state.sourceData } : { ...this.state.destinationData }
- if (field.type === 'array') {
- let oldValues: string[] = data[field.name] || []
- if (oldValues.find(v => v === value)) {
- data[field.name] = oldValues.filter(v => v !== value)
- } else {
- data[field.name] = [...oldValues, value]
- }
- } else {
- data[field.name] = value
- }
- if (type === 'source') {
- this.setState({ sourceData: data }, () => {
- this.validateOptions('source')
- })
- } else {
- this.setState({ destinationData: data }, () => {
- if (field.type !== 'string' || field.enum) {
- this.loadEnvDestinationOptions(field)
- }
- this.validateOptions('destination')
- })
- }
- }
- async handleUpdateClick() {
- this.setState({ updateDisabled: true })
- let updateData: UpdateData = {
- source: this.state.sourceData,
- destination: this.state.destinationData,
- network: this.state.selectedNetworks.length > 0 ? this.getSelectedNetworks() : [],
- storage: this.state.storageMap,
- }
- if (this.props.type === 'replica') {
- let storageConfigDefault = this.getFieldValue('destination', 'default_storage') || endpointStore.storageConfigDefault
- try {
- await replicaStore.update(this.props.replica, this.props.destinationEndpoint, updateData, storageConfigDefault)
- this.props.onRequestClose()
- this.props.onUpdateComplete(`/replica/executions/${this.props.replica.id}`)
- } catch (err) {
- this.setState({ updateDisabled: false })
- }
- } else {
- try {
- let migration: MainItem = await migrationStore.recreate(this.props.replica, this.props.sourceEndpoint, this.props.destinationEndpoint, updateData)
- migrationStore.clearDetails()
- this.props.onRequestClose()
- this.props.onUpdateComplete(`/migration/tasks/${migration.id}`)
- } catch (err) {
- this.setState({ updateDisabled: false })
- }
- }
- }
- handleNetworkChange(sourceNic: Nic, targetNetwork: Network) {
- let networkMap = this.state.selectedNetworks.filter(n => n.sourceNic.network_name !== sourceNic.network_name)
- this.setState({
- selectedNetworks: [...networkMap, { sourceNic, targetNetwork }],
- })
- }
- handleStorageChange(source: Disk, target: StorageBackend, type: 'backend' | 'disk') {
- let diskFieldName = type === 'backend' ? 'storage_backend_identifier' : 'id'
- let storageMap = this.state.storageMap
- .filter(n => n.type !== type || n.source[diskFieldName] !== source[diskFieldName])
- storageMap.push({ source, target, type })
- this.setState({ storageMap })
- }
- getFieldValue(type: 'source' | 'destination', fieldName: string, defaultValue: any) {
- let currentData = type === 'source' ? this.state.sourceData : this.state.destinationData
- if (currentData[fieldName] === undefined) {
- let replicaData = this.parseReplicaData(type === 'source' ? this.props.replica.source_environment
- : this.props.replica.destination_environment)
- if (replicaData[fieldName] !== undefined) {
- return replicaData[fieldName]
- }
- let osMapping = /^(windows|linux)_os_image$/.exec(fieldName)
- if (osMapping) {
- let osData = replicaData[`migr_image_map/${osMapping[1]}`]
- return osData
- }
- return defaultValue
- }
- return currentData[fieldName]
- }
- getSelectedNetworks(): NetworkMap[] {
- let selectedNetworks: NetworkMap[] = []
- let networkMap = this.props.replica.network_map
- if (networkMap) {
- Object.keys(networkMap).forEach(sourceNetworkName => {
- let network = this.props.networks.find(n => n.name === networkMap[sourceNetworkName] || n.id === networkMap[sourceNetworkName])
- if (!network) {
- return
- }
- selectedNetworks.push({
- sourceNic: { id: '', network_name: sourceNetworkName, mac_address: '', network_id: '' },
- targetNetwork: network,
- })
- })
- }
- selectedNetworks = selectedNetworks.map(mapping => {
- let updatedMapping = this.state.selectedNetworks.find(m => m.sourceNic.network_name === mapping.sourceNic.network_name)
- return updatedMapping || mapping
- })
- return selectedNetworks
- }
- getStorageMap(): StorageMap[] {
- let storageMap: StorageMap[] = []
- let currentStorage = this.props.replica.storage_mappings || {}
- let buildStorageMap = (type: 'backend' | 'disk', mapping: any) => {
- return {
- type,
- source: { storage_backend_identifier: mapping.source, id: mapping.disk_id },
- target: { name: mapping.destination, id: mapping.destination },
- }
- }
- let backendMappings = currentStorage.backend_mappings || []
- backendMappings.forEach(mapping => {
- storageMap.push(buildStorageMap('backend', mapping))
- })
- let diskMappings = currentStorage.disk_mappings || []
- diskMappings.forEach(mapping => {
- storageMap.push(buildStorageMap('disk', mapping))
- })
- this.state.storageMap.forEach(mapping => {
- let fieldName = mapping.type === 'backend' ? 'storage_backend_identifier' : 'id'
- let existingMapping = storageMap.find(m => m.type === mapping.type &&
- m.source[fieldName] === String(mapping.source[fieldName])
- )
- if (existingMapping) {
- existingMapping.target = mapping.target
- } else {
- storageMap.push(mapping)
- }
- })
- return storageMap
- }
- renderOptions(type: 'source' | 'destination') {
- let loading = type === 'source' ? (providerStore.sourceSchemaLoading || providerStore.sourceOptionsLoading) :
- providerStore.destinationSchemaLoading || providerStore.destinationOptionsLoading
- if (loading) {
- return this.renderLoading(`Loading ${type === 'source' ? 'source' : 'target'} options ...`)
- }
- let schema = type === 'source' ? providerStore.sourceSchema : providerStore.destinationSchema
- let fields = this.props.type === 'replica' ? schema.filter(f => !f.readOnly) : schema
- return (
- <WizardOptions
- wizardType={`replica-${type}-options-edit`}
- getFieldValue={(f, d) => this.getFieldValue(type, f, d)}
- fields={fields}
- hasStorageMap={type === 'source' ? false : this.hasStorageMap()}
- storageBackends={endpointStore.storageBackends}
- storageConfigDefault={endpointStore.storageConfigDefault}
- onChange={(f, v) => { this.handleFieldChange(type, f, v) }}
- oneColumnStyle={{ marginTop: '-16px', display: 'flex', flexDirection: 'column', width: '100%', alignItems: 'center' }}
- columnStyle={{ marginRight: 0 }}
- fieldWidth={StyleProps.inputSizes.large.width}
- onScrollableRef={ref => { this.scrollableRef = ref }}
- availableHeight={384}
- useAdvancedOptions
- layout="modal"
- />
- )
- }
- renderStorageMapping() {
- if (this.props.instancesDetailsLoading) {
- return this.renderLoading('Loading instances details ...')
- }
- if (endpointStore.storageLoading) {
- return this.renderLoading('Loading storage ...')
- }
- return (
- <WizardStorage
- storageBackends={endpointStore.storageBackends}
- instancesDetails={this.props.instancesDetails}
- storageMap={this.getStorageMap()}
- onChange={(s, t, type) => { this.handleStorageChange(s, t, type) }}
- />
- )
- }
- renderNetworkMapping() {
- return (
- <WizardNetworks
- instancesDetails={this.props.instancesDetails}
- loadingInstancesDetails={this.props.instancesDetailsLoading}
- networks={this.props.networks}
- loading={this.props.networksLoading}
- onChange={(nic, network) => { this.handleNetworkChange(nic, network) }}
- selectedNetworks={this.getSelectedNetworks()}
- />
- )
- }
- renderContent() {
- let content = null
- switch (this.state.selectedPanel) {
- case 'source_options':
- content = this.renderOptions('source')
- break
- case 'dest_options':
- content = this.renderOptions('destination')
- break
- case 'network_mapping':
- content = this.renderNetworkMapping()
- break
- case 'storage_mapping':
- content = this.renderStorageMapping()
- break
- default:
- content = null
- }
- return (
- <PanelContent>
- {content}
- <Buttons>
- <Button
- large
- onClick={this.props.onRequestClose}
- secondary
- >Cancel</Button>
- <Button
- large
- onClick={() => { this.handleUpdateClick() }}
- disabled={this.isUpdateDisabled()}
- >
- {this.props.type === 'replica' ? 'Update' : 'Create'}
- </Button>
- </Buttons>
- </PanelContent>
- )
- }
- renderLoading(message: string) {
- let loadingMessage = message || 'Loading ...'
- return (
- <LoadingWrapper>
- <StatusImage loading />
- <LoadingText>{loadingMessage}</LoadingText>
- </LoadingWrapper>
- )
- }
- render() {
- let navigationItems: NavigationItem[] = [
- { value: 'dest_options', label: 'Target Options' },
- { value: 'network_mapping', label: 'Network Mapping' },
- ]
- if (this.hasStorageMap()) {
- navigationItems.push({ value: 'storage_mapping', label: 'Storage Mapping' })
- }
- if (this.hasSourceOptions()) {
- navigationItems.splice(0, 0, { value: 'source_options', label: 'Source Options' })
- }
- return (
- <Modal
- isOpen={this.props.isOpen}
- title={`${this.props.type === 'replica' ? 'Edit Replica' : 'Recreate Migration'}`}
- onRequestClose={this.props.onRequestClose}
- contentStyle={{ width: '800px' }}
- onScrollableRef={() => this.scrollableRef}
- fixedHeight={512}
- >
- <Panel
- navigationItems={navigationItems}
- content={this.renderContent()}
- onChange={navItem => { this.handlePanelChange(navItem.value) }}
- selectedValue={this.state.selectedPanel}
- onReloadClick={() => { this.handleReload() }}
- />
- </Modal>
- )
- }
- }
- export default EditReplica
|