TransferItemModal.tsx 30 KB

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