TransferItemModal.tsx 29 KB

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