TransferItemModal.tsx 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877
  1. /*
  2. Copyright (C) 2019 Cloudbase Solutions SRL
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <http://www.gnu.org/licenses/>.
  13. */
  14. import React from 'react'
  15. import { observer } from 'mobx-react'
  16. import styled from 'styled-components'
  17. import providerStore, { getFieldChangeOptions } from '@src/stores/ProviderStore'
  18. import replicaStore from '@src/stores/ReplicaStore'
  19. import migrationStore from '@src/stores/MigrationStore'
  20. import endpointStore from '@src/stores/EndpointStore'
  21. import { OptionsSchemaPlugin } from '@src/plugins'
  22. import Button from '@src/components/ui/Button'
  23. import StatusImage from '@src/components/ui/StatusComponents/StatusImage'
  24. import Modal from '@src/components/ui/Modal'
  25. import Panel from '@src/components/ui/Panel'
  26. import WizardNetworks, { WizardNetworksChangeObject } from '@src/components/modules/WizardModule/WizardNetworks'
  27. import WizardOptions, { findInvalidFields, INSTANCE_OSMORPHING_MINION_POOL_MAPPINGS } from '@src/components/modules/WizardModule/WizardOptions'
  28. import WizardStorage from '@src/components/modules/WizardModule/WizardStorage'
  29. import type {
  30. UpdateData, TransferItemDetails, MigrationItemDetails,
  31. } from '@src/@types/MainItem'
  32. import {
  33. Endpoint, EndpointUtils, StorageBackend, StorageMap,
  34. } from '@src/@types/Endpoint'
  35. import type { Field } from '@src/@types/Field'
  36. import type {
  37. Instance, InstanceScript,
  38. } from '@src/@types/Instance'
  39. import {
  40. Network, NetworkMap, NetworkUtils, SecurityGroup,
  41. } from '@src/@types/Network'
  42. import { providerTypes, migrationFields } from '@src/constants'
  43. import configLoader from '@src/utils/Config'
  44. import LoadingButton from '@src/components/ui/LoadingButton'
  45. import minionPoolStore from '@src/stores/MinionPoolStore'
  46. import WizardScripts from '@src/components/modules/WizardModule/WizardScripts'
  47. import networkStore from '@src/stores/NetworkStore'
  48. import { ThemeProps } from '@src/components/Theme'
  49. import ObjectUtils from '@src/utils/ObjectUtils'
  50. const PanelContent = styled.div<any>`
  51. display: flex;
  52. flex-direction: column;
  53. justify-content: space-between;
  54. flex-grow: 1;
  55. min-height: 0;
  56. `
  57. const LoadingWrapper = styled.div<any>`
  58. display: flex;
  59. flex-direction: column;
  60. align-items: center;
  61. margin: 32px 0;
  62. `
  63. const LoadingText = styled.div<any>`
  64. font-size: 18px;
  65. margin-top: 32px;
  66. `
  67. const ErrorWrapper = styled.div<any>`
  68. display: flex;
  69. flex-direction: column;
  70. height: 100%;
  71. align-items: center;
  72. justify-content: center;
  73. padding: 32px;
  74. `
  75. const ErrorMessage = styled.div<any>`
  76. margin-top: 16px;
  77. text-align: center;
  78. `
  79. const Buttons = styled.div<any>`
  80. padding: 32px;
  81. display: flex;
  82. flex-shrink: 0;
  83. justify-content: space-between;
  84. `
  85. type Props = {
  86. type?: 'replica' | 'migration',
  87. isOpen: boolean,
  88. onRequestClose: () => void,
  89. onUpdateComplete: (redirectTo: string) => void,
  90. replica: TransferItemDetails,
  91. destinationEndpoint: Endpoint,
  92. sourceEndpoint: Endpoint,
  93. instancesDetails: Instance[],
  94. instancesDetailsLoading: boolean,
  95. networks: Network[],
  96. networksLoading: boolean,
  97. onReloadClick: () => void,
  98. }
  99. type State = {
  100. selectedPanel: string | null,
  101. destinationData: any,
  102. sourceData: any,
  103. updateDisabled: boolean,
  104. updating: boolean,
  105. selectedNetworks: NetworkMap[],
  106. defaultStorage: { value: string | null, busType?: string | null } | undefined,
  107. storageMap: StorageMap[],
  108. sourceFailed: boolean,
  109. destinationFailedMessage: string | null,
  110. uploadedScripts: InstanceScript[],
  111. removedScripts: InstanceScript[],
  112. }
  113. @observer
  114. class TransferItemModal extends React.Component<Props, State> {
  115. state: State = {
  116. selectedPanel: 'source_options',
  117. destinationData: {},
  118. sourceData: {},
  119. updateDisabled: false,
  120. updating: false,
  121. selectedNetworks: [],
  122. defaultStorage: undefined,
  123. storageMap: [],
  124. uploadedScripts: [],
  125. sourceFailed: false,
  126. destinationFailedMessage: null,
  127. removedScripts: [],
  128. }
  129. scrollableRef: HTMLElement | null | undefined
  130. UNSAFE_componentWillMount() {
  131. this.loadData(true)
  132. }
  133. get requiresWindowsImage() {
  134. return this.props.instancesDetails.some(i => i.os_type === 'windows')
  135. }
  136. getStorageMap(storageBackends: StorageBackend[]): StorageMap[] {
  137. const storageMap: StorageMap[] = []
  138. const currentStorage = this.props.replica.storage_mappings
  139. const buildStorageMap = (type: 'backend' | 'disk', mapping: any): StorageMap => {
  140. const busTypeInfo = EndpointUtils.getBusTypeStorageId(storageBackends, mapping.destination)
  141. const backend = storageBackends.find(b => b.name === busTypeInfo.id)
  142. const newStorageMap: StorageMap = {
  143. type,
  144. source: { storage_backend_identifier: mapping.source, id: mapping.disk_id },
  145. target: { name: busTypeInfo.id!, id: backend ? backend.id : busTypeInfo.id },
  146. }
  147. if (busTypeInfo.busType) {
  148. newStorageMap.targetBusType = busTypeInfo.busType
  149. }
  150. return newStorageMap
  151. }
  152. const backendMappings = currentStorage?.backend_mappings || []
  153. backendMappings.forEach(mapping => {
  154. storageMap.push(buildStorageMap('backend', mapping))
  155. })
  156. const diskMappings = currentStorage?.disk_mappings || []
  157. diskMappings.forEach(mapping => {
  158. storageMap.push(buildStorageMap('disk', mapping))
  159. })
  160. this.state.storageMap.forEach(mapping => {
  161. const fieldName = mapping.type === 'backend' ? 'storage_backend_identifier' : 'id'
  162. const existingMapping = storageMap.find(m => m.type === mapping.type
  163. && m.source[fieldName] === String(mapping.source[fieldName]))
  164. if (existingMapping) {
  165. existingMapping.target = mapping.target
  166. if (mapping.targetBusType !== undefined) {
  167. existingMapping.targetBusType = mapping.targetBusType
  168. }
  169. } else {
  170. storageMap.push(mapping)
  171. }
  172. })
  173. return storageMap
  174. }
  175. getSelectedNetworks(): NetworkMap[] {
  176. const selectedNetworks: NetworkMap[] = []
  177. const networkMap: any = this.props.replica.network_map
  178. if (networkMap) {
  179. Object.keys(networkMap).forEach(sourceNetworkName => {
  180. // if the network mapping was updated, just use the new mapping instead of the old one
  181. const updatedMapping = this.state.selectedNetworks.find(m => m.sourceNic.network_name === sourceNetworkName)
  182. if (updatedMapping) {
  183. selectedNetworks.push(updatedMapping)
  184. return
  185. }
  186. // add extra information to the current network mapping
  187. const destNetObj: any = networkMap[sourceNetworkName]
  188. const portKeyInfo = NetworkUtils.getPortKeyNetworkId(this.props.networks, destNetObj)
  189. const destNetId = String(typeof destNetObj === 'string' || !destNetObj
  190. || !destNetObj.id ? portKeyInfo.id : destNetObj.id)
  191. const network = this.props.networks.find(n => n.name === destNetId || n.id === destNetId) || null
  192. const mapping: NetworkMap = {
  193. sourceNic: {
  194. id: '', network_name: sourceNetworkName, mac_address: '', network_id: '',
  195. },
  196. targetNetwork: network,
  197. }
  198. if (destNetObj.security_groups) {
  199. const destSecGroupsInfo = network?.security_groups || []
  200. const secInfo = destNetObj.security_groups.map((s: SecurityGroup) => {
  201. const foundSecGroupInfo = destSecGroupsInfo
  202. .find((si: any) => (si.id ? si.id === s : si === s))
  203. return foundSecGroupInfo || { id: s, name: s }
  204. })
  205. mapping.targetSecurityGroups = secInfo
  206. }
  207. if (portKeyInfo.portKey) {
  208. mapping.targetPortKey = portKeyInfo.portKey
  209. }
  210. selectedNetworks.push(mapping)
  211. })
  212. }
  213. // add any new networks mappings that were not in the original network mappings
  214. this.state.selectedNetworks.forEach(mapping => {
  215. if (!selectedNetworks.find(m => m.sourceNic.network_name === mapping.sourceNic.network_name)) {
  216. selectedNetworks.push(mapping)
  217. }
  218. })
  219. return selectedNetworks
  220. }
  221. getDefaultStorage(): { value: string | null, busType?: string | null } {
  222. if (this.state.defaultStorage) {
  223. return this.state.defaultStorage
  224. }
  225. const buildDefaultStorage = (defaultValue: string | null | undefined) => {
  226. const busTypeInfo = EndpointUtils.getBusTypeStorageId(endpointStore.storageBackends, defaultValue || null)
  227. const defaultStorage: { value: string | null, busType?: string | null } = {
  228. value: busTypeInfo.id,
  229. }
  230. if (busTypeInfo.busType) {
  231. defaultStorage.busType = busTypeInfo.busType
  232. }
  233. return defaultStorage
  234. }
  235. if (this.props.replica.storage_mappings?.default) {
  236. return buildDefaultStorage(this.props.replica.storage_mappings.default)
  237. }
  238. if (endpointStore.storageConfigDefault) {
  239. return buildDefaultStorage(endpointStore.storageConfigDefault)
  240. }
  241. return { value: null }
  242. }
  243. getFieldValue(opts: { type: 'source' | 'destination', fieldName: string, defaultValue: any, parentFieldName?: string }) {
  244. const {
  245. type, fieldName, defaultValue, parentFieldName,
  246. } = opts
  247. const currentData = type === 'source' ? this.state.sourceData : this.state.destinationData
  248. const replicaMinionMappings = this.props.replica[INSTANCE_OSMORPHING_MINION_POOL_MAPPINGS]
  249. if (parentFieldName) {
  250. if (currentData[parentFieldName]
  251. && currentData[parentFieldName][fieldName] !== undefined) {
  252. return currentData[parentFieldName][fieldName]
  253. }
  254. if (parentFieldName === INSTANCE_OSMORPHING_MINION_POOL_MAPPINGS
  255. && replicaMinionMappings
  256. && replicaMinionMappings[fieldName] !== undefined
  257. ) {
  258. return replicaMinionMappings[fieldName]
  259. }
  260. }
  261. if (currentData[fieldName] !== undefined) {
  262. return currentData[fieldName]
  263. }
  264. if (fieldName === 'title') {
  265. let title = this.props.instancesDetails?.[0]?.name
  266. if (this.props.instancesDetails && this.props.instancesDetails.length > 1) {
  267. title += ` (+${this.props.instancesDetails.length - 1} more)`
  268. }
  269. return title
  270. }
  271. if (fieldName === 'minion_pool_id') {
  272. return type === 'source' ? this.props.replica.origin_minion_pool_id : this.props.replica.destination_minion_pool_id
  273. }
  274. const replicaData: any = type === 'source' ? this.props.replica.source_environment
  275. : this.props.replica.destination_environment
  276. if (parentFieldName) {
  277. if (replicaData[parentFieldName]?.[fieldName] !== undefined) {
  278. return replicaData[parentFieldName][fieldName]
  279. }
  280. }
  281. if (replicaData[fieldName] !== undefined) {
  282. return replicaData[fieldName]
  283. }
  284. const endpoint = type === 'source' ? this.props.sourceEndpoint : this.props.destinationEndpoint
  285. const plugin = OptionsSchemaPlugin.for(endpoint.type)
  286. const osMapping = /^(windows|linux)/.exec(fieldName)
  287. if (osMapping) {
  288. const osData = replicaData[`${plugin.migrationImageMapFieldName}/${osMapping[0]}`]
  289. return osData
  290. }
  291. const anyData = this.props.replica as any
  292. if (migrationFields.find(f => f.name === fieldName) && anyData[fieldName]) {
  293. return anyData[fieldName]
  294. }
  295. if (fieldName === 'skip_os_morphing' && this.props.type === 'migration') {
  296. return migrationStore.getDefaultSkipOsMorphing(anyData)
  297. }
  298. return defaultValue
  299. }
  300. async loadData(useCache: boolean) {
  301. minionPoolStore.loadMinionPools()
  302. await providerStore.loadProviders()
  303. const loadAllOptions = async (type: 'source' | 'destination') => {
  304. const endpoint = type === 'source' ? this.props.sourceEndpoint : this.props.destinationEndpoint
  305. try {
  306. await this.loadOptions(endpoint, type, useCache)
  307. this.loadExtraOptions({ type, useCache })
  308. } catch (err) {
  309. if (type === 'source') {
  310. this.setState(prevState => {
  311. let selectedPanel = prevState.selectedPanel
  312. if (selectedPanel === 'source_options') {
  313. selectedPanel = 'dest_options'
  314. }
  315. return { sourceFailed: true, selectedPanel }
  316. })
  317. }
  318. }
  319. }
  320. loadAllOptions('source')
  321. loadAllOptions('destination')
  322. }
  323. async loadOptions(endpoint: Endpoint, optionsType: 'source' | 'destination', useCache: boolean) {
  324. try {
  325. await providerStore.loadOptionsSchema({
  326. providerName: endpoint.type,
  327. requiresWindowsImage: this.requiresWindowsImage,
  328. optionsType,
  329. useCache,
  330. })
  331. } catch (err) {
  332. if (optionsType === 'destination' || this.props.type === 'migration') {
  333. const destinationFailedMessage = this.props.type === 'replica'
  334. ? 'An error has occurred during the loading of the Replica\'s options for editing. There could be connection issues with the destination platform. Please retry the operation.'
  335. : 'An error has occurred during loading of the source or destination platforms\' environment options for editing of the Migration\'s parameters. You may still recreate the Migration with the same parameters as the original one by clicking "Create".'
  336. this.setState({ destinationFailedMessage })
  337. }
  338. throw err
  339. }
  340. await providerStore.getOptionsValues({
  341. optionsType,
  342. endpointId: endpoint.id,
  343. providerName: endpoint.type,
  344. useCache,
  345. requiresWindowsImage: this.requiresWindowsImage,
  346. })
  347. }
  348. loadExtraOptions(opts: {
  349. field?: Field,
  350. type: 'source' | 'destination',
  351. useCache?: boolean,
  352. parentFieldName?: string
  353. }) {
  354. const {
  355. field, type, useCache, parentFieldName,
  356. } = opts
  357. const endpoint = type === 'source' ? this.props.sourceEndpoint : this.props.destinationEndpoint
  358. const env = type === 'source' ? this.props.replica.source_environment : this.props.replica.destination_environment
  359. const stateEnv = type === 'source' ? this.state.sourceData : this.state.destinationData
  360. const envData = getFieldChangeOptions({
  361. providerName: endpoint.type,
  362. schema: type === 'source' ? providerStore.sourceSchema : providerStore.destinationSchema,
  363. data: ObjectUtils.mergeDeep(env, stateEnv),
  364. field: field || null,
  365. type,
  366. parentFieldName,
  367. })
  368. if (!envData) {
  369. return
  370. }
  371. providerStore.getOptionsValues({
  372. optionsType: type,
  373. endpointId: endpoint.id,
  374. providerName: endpoint.type,
  375. useCache,
  376. envData,
  377. requiresWindowsImage: this.requiresWindowsImage,
  378. })
  379. if (type === 'destination') {
  380. networkStore.loadNetworks(endpoint.id, envData, { cache: true })
  381. if (this.hasStorageMap()) {
  382. endpointStore.loadStorage(this.props.destinationEndpoint.id, envData, { cache: true })
  383. }
  384. }
  385. }
  386. hasStorageMap(): boolean {
  387. return providerStore.providers?.[this.props.destinationEndpoint.type]
  388. ? !!providerStore.providers[this.props.destinationEndpoint.type].types.find(t => t === providerTypes.STORAGE)
  389. : false
  390. }
  391. isUpdateDisabled() {
  392. const isDestFailed = this.props.type === 'replica' && this.state.destinationFailedMessage
  393. return this.state.updateDisabled || isDestFailed
  394. }
  395. isLoadingDestOptions() {
  396. return providerStore.destinationSchemaLoading
  397. || providerStore.destinationOptionsPrimaryLoading
  398. }
  399. isLoadingSourceOptions() {
  400. return providerStore.sourceSchemaLoading
  401. || providerStore.sourceOptionsPrimaryLoading
  402. }
  403. isLoadingNetwork() {
  404. return this.props.instancesDetailsLoading
  405. }
  406. isLoadingStorage() {
  407. return this.props.instancesDetailsLoading || endpointStore.storageLoading
  408. }
  409. isLoading() {
  410. return this.isLoadingSourceOptions()
  411. || this.isLoadingDestOptions() || this.isLoadingNetwork()
  412. || this.isLoadingStorage()
  413. }
  414. validateOptions(type: 'source' | 'destination') {
  415. const env = type === 'source' ? this.props.replica.source_environment : this.props.replica.destination_environment
  416. const data = type === 'source' ? this.state.sourceData : this.state.destinationData
  417. const schema = type === 'source' ? providerStore.sourceSchema : providerStore.destinationSchema
  418. const invalidFields = findInvalidFields(ObjectUtils.mergeDeep(env, data), schema)
  419. this.setState({ updateDisabled: invalidFields.length > 0 })
  420. }
  421. handlePanelChange(panel: string) {
  422. this.setState({ selectedPanel: panel })
  423. }
  424. handleReload() {
  425. this.props.onReloadClick()
  426. this.loadData(false)
  427. }
  428. handleFieldChange(opts: { type: 'source' | 'destination', field: Field, value: any, parentFieldName?: string }) {
  429. const {
  430. type, field, value, parentFieldName,
  431. } = opts
  432. const data = type === 'source' ? { ...this.state.sourceData } : { ...this.state.destinationData }
  433. if (field.type === 'array') {
  434. const replicaData: any = type === 'source' ? this.props.replica.source_environment
  435. : this.props.replica.destination_environment
  436. const currentValues: string[] = data[field.name] || []
  437. const oldValues: string[] = replicaData[field.name] || []
  438. let values: string[] = currentValues
  439. if (!currentValues.length) {
  440. values = [...oldValues]
  441. }
  442. if (values.find(v => v === value)) {
  443. data[field.name] = values.filter(v => v !== value)
  444. } else {
  445. data[field.name] = [...values, value]
  446. }
  447. } else if (field.groupName) {
  448. if (!data[field.groupName]) {
  449. data[field.groupName] = {}
  450. }
  451. data[field.groupName][field.name] = value
  452. } else if (parentFieldName) {
  453. data[parentFieldName] = data[parentFieldName] || {}
  454. data[parentFieldName][field.name] = value
  455. } else {
  456. data[field.name] = value
  457. }
  458. if (field.subFields) {
  459. field.subFields.forEach(subField => {
  460. const subFieldKeys = Object.keys(data).filter(k => k.indexOf(subField.name) > -1)
  461. subFieldKeys.forEach(k => {
  462. delete data[k]
  463. })
  464. })
  465. }
  466. const handleStateUpdate = () => {
  467. if (field.type !== 'string' || field.enum) {
  468. this.loadExtraOptions({ field, type, parentFieldName })
  469. }
  470. this.validateOptions(type)
  471. }
  472. if (type === 'source') {
  473. this.setState({ sourceData: data }, () => { handleStateUpdate() })
  474. } else {
  475. this.setState({ destinationData: data }, () => { handleStateUpdate() })
  476. }
  477. }
  478. async handleUpdateClick() {
  479. this.setState({ updating: true })
  480. const updateData: UpdateData = {
  481. source: this.state.sourceData,
  482. destination: this.state.destinationData,
  483. network: this.state.selectedNetworks.length > 0 ? this.getSelectedNetworks() : [],
  484. storage: this.state.storageMap,
  485. uploadedScripts: this.state.uploadedScripts,
  486. removedScripts: this.state.removedScripts,
  487. }
  488. if (this.props.type === 'replica') {
  489. try {
  490. await replicaStore.update({
  491. replica: this.props.replica as any,
  492. sourceEndpoint: this.props.sourceEndpoint,
  493. destinationEndpoint: this.props.destinationEndpoint,
  494. updateData,
  495. defaultStorage: this.getDefaultStorage(),
  496. storageConfigDefault: endpointStore.storageConfigDefault,
  497. })
  498. this.props.onRequestClose()
  499. this.props.onUpdateComplete(`/replicas/${this.props.replica.id}/executions`)
  500. } catch (err) {
  501. this.setState({ updating: false })
  502. }
  503. } else {
  504. try {
  505. const defaultStorage = EndpointUtils.getBusTypeStorageId(endpointStore.storageBackends, this.props.replica.storage_mappings?.default || null)
  506. const replicaDefaultStorage: { value: string | null, busType?: string | null } = {
  507. value: defaultStorage.id,
  508. busType: defaultStorage.busType,
  509. }
  510. const migration: MigrationItemDetails = await migrationStore.recreate({
  511. migration: this.props.replica as any,
  512. sourceEndpoint: this.props.sourceEndpoint,
  513. destEndpoint: this.props.destinationEndpoint,
  514. updateData,
  515. defaultStorage: replicaDefaultStorage,
  516. updatedDefaultStorage: this.state.defaultStorage,
  517. replicationCount: this.props.replica.replication_count,
  518. })
  519. migrationStore.clearDetails()
  520. this.props.onRequestClose()
  521. this.props.onUpdateComplete(`/migrations/${migration.id}/tasks`)
  522. } catch (err) {
  523. this.setState({ updating: false })
  524. }
  525. }
  526. }
  527. handleNetworkChange(changeObject: WizardNetworksChangeObject) {
  528. const networkMap = this.state.selectedNetworks
  529. .filter(n => n.sourceNic.network_name !== changeObject.nic.network_name)
  530. this.setState({
  531. selectedNetworks: [...networkMap, {
  532. sourceNic: changeObject.nic,
  533. targetNetwork: changeObject.network,
  534. targetSecurityGroups: changeObject.securityGroups,
  535. targetPortKey: changeObject.portKey,
  536. }],
  537. })
  538. }
  539. handleCancelScript(global: 'windows' | 'linux' | null, instanceName: string | null) {
  540. this.setState(prevState => ({
  541. uploadedScripts: prevState.uploadedScripts
  542. .filter(s => (global ? s.global !== global : s.instanceId !== instanceName)),
  543. }))
  544. }
  545. handleScriptUpload(script: InstanceScript) {
  546. this.setState(prevState => ({
  547. uploadedScripts: [
  548. ...prevState.uploadedScripts,
  549. script,
  550. ],
  551. }))
  552. }
  553. handleScriptDataRemove(script: InstanceScript) {
  554. this.setState(prevState => ({
  555. removedScripts: [
  556. ...prevState.removedScripts,
  557. script,
  558. ],
  559. }))
  560. }
  561. handleStorageChange(mapping: StorageMap) {
  562. this.setState(prevState => {
  563. const diskFieldName = mapping.type === 'backend' ? 'storage_backend_identifier' : 'id'
  564. const storageMap = prevState.storageMap
  565. .filter(n => n.type !== mapping.type || n.source[diskFieldName] !== mapping.source[diskFieldName])
  566. storageMap.push(mapping)
  567. return { storageMap }
  568. })
  569. }
  570. renderDestinationFailedMessage() {
  571. return (
  572. <ErrorWrapper>
  573. <StatusImage status="ERROR" />
  574. <ErrorMessage>{this.state.destinationFailedMessage}</ErrorMessage>
  575. </ErrorWrapper>
  576. )
  577. }
  578. renderOptions(type: 'source' | 'destination') {
  579. const loading = type === 'source' ? (providerStore.sourceSchemaLoading || providerStore.sourceOptionsPrimaryLoading)
  580. : (providerStore.destinationSchemaLoading || providerStore.destinationOptionsPrimaryLoading)
  581. if (this.state.destinationFailedMessage) {
  582. return this.renderDestinationFailedMessage()
  583. }
  584. if (loading) {
  585. return this.renderLoading(`Loading ${type === 'source' ? 'source' : 'target'} options ...`)
  586. }
  587. const optionsLoading = type === 'source' ? providerStore.sourceOptionsSecondaryLoading
  588. : providerStore.destinationOptionsSecondaryLoading
  589. const schema = type === 'source' ? providerStore.sourceSchema : providerStore.destinationSchema
  590. const fields = this.props.type === 'replica' ? schema.filter(f => !f.readOnly) : schema
  591. const extraOptionsConfig = configLoader.config.extraOptionsApiCalls.find(o => {
  592. const provider = type === 'source' ? this.props.sourceEndpoint.type : this.props.destinationEndpoint.type
  593. return o.name === provider && o.types.find(t => t === type)
  594. })
  595. let optionsLoadingSkipFields: string[] = []
  596. if (extraOptionsConfig) {
  597. optionsLoadingSkipFields = extraOptionsConfig.requiredFields
  598. }
  599. const endpoint = type === 'source' ? this.props.sourceEndpoint : this.props.destinationEndpoint
  600. let dictionaryKey = ''
  601. if (endpoint) {
  602. dictionaryKey = `${endpoint.type}-${type}`
  603. }
  604. const minionPools = minionPoolStore.minionPools
  605. .filter(m => m.platform === type && m.endpoint_id === endpoint.id)
  606. return (
  607. <WizardOptions
  608. minionPools={minionPools}
  609. wizardType={`${this.props.type || 'replica'}-${type}-options-edit`}
  610. getFieldValue={(f, d, pf) => this.getFieldValue({
  611. type, fieldName: f, defaultValue: d, parentFieldName: pf,
  612. })}
  613. fields={fields}
  614. selectedInstances={type === 'destination' ? this.props.instancesDetails : null}
  615. hasStorageMap={type === 'source' ? false : this.hasStorageMap()}
  616. storageBackends={endpointStore.storageBackends}
  617. onChange={(f, v, fp) => {
  618. this.handleFieldChange({
  619. type, field: f, value: v, parentFieldName: fp,
  620. })
  621. }}
  622. oneColumnStyle={{
  623. marginTop: '-16px', display: 'flex', flexDirection: 'column', width: '100%', alignItems: 'center',
  624. }}
  625. fieldWidth={ThemeProps.inputSizes.large.width}
  626. onScrollableRef={ref => { this.scrollableRef = ref }}
  627. availableHeight={384}
  628. useAdvancedOptions
  629. layout="modal"
  630. isSource={type === 'source'}
  631. optionsLoading={optionsLoading}
  632. optionsLoadingSkipFields={[...optionsLoadingSkipFields, 'description', 'execute_now',
  633. 'execute_now_options', ...migrationFields.map(f => f.name)]}
  634. dictionaryKey={dictionaryKey}
  635. executeNowOptionsDisabled={!providerStore.hasExecuteNowOptions(this.props.sourceEndpoint.type)}
  636. />
  637. )
  638. }
  639. renderStorageMapping() {
  640. if (this.props.instancesDetailsLoading) {
  641. return this.renderLoading('Loading instances details ...')
  642. }
  643. return (
  644. <WizardStorage
  645. loading={endpointStore.storageLoading}
  646. defaultStorage={this.getDefaultStorage()}
  647. onDefaultStorageChange={(value, busType) => { this.setState({ defaultStorage: { value, busType } }) }}
  648. defaultStorageLayout="modal"
  649. storageBackends={endpointStore.storageBackends}
  650. instancesDetails={this.props.instancesDetails}
  651. storageMap={this.getStorageMap(endpointStore.storageBackends)}
  652. onChange={mapping => { this.handleStorageChange(mapping) }}
  653. style={{ padding: '32px 32px 0 32px', width: 'calc(100% - 64px)' }}
  654. titleWidth={160}
  655. onScrollableRef={ref => { this.scrollableRef = ref }}
  656. />
  657. )
  658. }
  659. renderNetworkMapping() {
  660. return (
  661. <WizardNetworks
  662. instancesDetails={this.props.instancesDetails}
  663. loadingInstancesDetails={this.props.instancesDetailsLoading}
  664. networks={this.props.networks}
  665. loading={this.props.networksLoading}
  666. onChange={change => {
  667. this.handleNetworkChange(change)
  668. }}
  669. selectedNetworks={this.getSelectedNetworks()}
  670. style={{ padding: '32px 32px 0 32px', width: 'calc(100% - 64px)' }}
  671. titleWidth={160}
  672. />
  673. )
  674. }
  675. renderUserScripts() {
  676. return (
  677. <WizardScripts
  678. instances={this.props.instancesDetails}
  679. loadingInstances={this.props.instancesDetailsLoading}
  680. onScriptUpload={s => { this.handleScriptUpload(s) }}
  681. onScriptDataRemove={s => { this.handleScriptDataRemove(s) }}
  682. onCancelScript={(g, i) => { this.handleCancelScript(g, i) }}
  683. uploadedScripts={this.state.uploadedScripts}
  684. removedScripts={this.state.removedScripts}
  685. userScriptData={this.props.replica?.user_scripts}
  686. scrollableRef={(r: HTMLElement) => { this.scrollableRef = r }}
  687. style={{ padding: '32px 32px 0 32px', width: 'calc(100% - 64px)' }}
  688. />
  689. )
  690. }
  691. renderContent() {
  692. let content = null
  693. switch (this.state.selectedPanel) {
  694. case 'source_options':
  695. content = this.renderOptions('source')
  696. break
  697. case 'dest_options':
  698. content = this.renderOptions('destination')
  699. break
  700. case 'network_mapping':
  701. content = this.renderNetworkMapping()
  702. break
  703. case 'user_scripts':
  704. content = this.renderUserScripts()
  705. break
  706. case 'storage_mapping':
  707. content = this.renderStorageMapping()
  708. break
  709. default:
  710. content = null
  711. }
  712. return (
  713. <PanelContent>
  714. {content}
  715. <Buttons>
  716. <Button
  717. large
  718. onClick={this.props.onRequestClose}
  719. secondary
  720. >Close
  721. </Button>
  722. {this.isLoading() ? (
  723. <LoadingButton large>Loading ...</LoadingButton>
  724. ) : this.state.updating ? (
  725. <LoadingButton large>Updating ...</LoadingButton>
  726. ) : (
  727. <Button
  728. large
  729. onClick={() => { this.handleUpdateClick() }}
  730. disabled={this.isUpdateDisabled()}
  731. >
  732. {this.props.type === 'replica' ? 'Update' : 'Create'}
  733. </Button>
  734. )}
  735. </Buttons>
  736. </PanelContent>
  737. )
  738. }
  739. renderLoading(message: string) {
  740. const loadingMessage = message || 'Loading ...'
  741. return (
  742. <LoadingWrapper>
  743. <StatusImage loading />
  744. <LoadingText>{loadingMessage}</LoadingText>
  745. </LoadingWrapper>
  746. )
  747. }
  748. render() {
  749. const navigationItems: Panel['props']['navigationItems'] = [
  750. {
  751. value: 'source_options',
  752. label: 'Source Options',
  753. disabled: this.state.sourceFailed,
  754. title: this.state.sourceFailed ? 'There are source platform errors, source options can\'t be updated' : '',
  755. loading: this.isLoadingSourceOptions(),
  756. },
  757. {
  758. value: 'dest_options',
  759. label: 'Target Options',
  760. loading: this.isLoadingDestOptions(),
  761. },
  762. {
  763. value: 'network_mapping',
  764. label: 'Network Mapping',
  765. loading: this.isLoadingNetwork(),
  766. },
  767. {
  768. value: 'user_scripts',
  769. label: 'User Scripts',
  770. loading: this.props.instancesDetailsLoading,
  771. },
  772. ]
  773. if (this.hasStorageMap()) {
  774. navigationItems.push({
  775. value: 'storage_mapping',
  776. label: 'Storage Mapping',
  777. loading: this.isLoadingStorage(),
  778. })
  779. }
  780. return (
  781. <Modal
  782. isOpen={this.props.isOpen}
  783. title={`${this.props.type === 'replica' ? 'Edit Replica' : 'Recreate Migration'}`}
  784. onRequestClose={this.props.onRequestClose}
  785. contentStyle={{ width: '800px' }}
  786. onScrollableRef={() => this.scrollableRef}
  787. fixedHeight={512}
  788. >
  789. <Panel
  790. navigationItems={navigationItems}
  791. content={this.renderContent()}
  792. onChange={navItem => { this.handlePanelChange(navItem.value) }}
  793. selectedValue={this.state.selectedPanel}
  794. onReloadClick={() => { this.handleReload() }}
  795. reloadLabel={this.props.type === 'replica' ? 'Reload All Replica Options' : 'Reload All Migration Options'}
  796. />
  797. </Modal>
  798. )
  799. }
  800. }
  801. export default TransferItemModal