TransferItemModal.tsx 32 KB

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