TransferItemModal.tsx 29 KB

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