WizardPage.jsx 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625
  1. /*
  2. Copyright (C) 2017 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. // @flow
  15. import React from 'react'
  16. import styled from 'styled-components'
  17. import autobind from 'autobind-decorator'
  18. import { observer } from 'mobx-react'
  19. import WizardTemplate from '../../templates/WizardTemplate'
  20. import DetailsPageHeader from '../../organisms/DetailsPageHeader'
  21. import WizardPageContent from '../../organisms/WizardPageContent'
  22. import Modal from '../../molecules/Modal'
  23. import Endpoint from '../../organisms/Endpoint'
  24. import userStore from '../../../stores/UserStore'
  25. import providerStore, { getFieldChangeOptions } from '../../../stores/ProviderStore'
  26. import endpointStore from '../../../stores/EndpointStore'
  27. import wizardStore from '../../../stores/WizardStore'
  28. import instanceStore from '../../../stores/InstanceStore'
  29. import networkStore from '../../../stores/NetworkStore'
  30. import notificationStore from '../../../stores/NotificationStore'
  31. import scheduleStore from '../../../stores/ScheduleStore'
  32. import replicaStore from '../../../stores/ReplicaStore'
  33. import KeyboardManager from '../../../utils/KeyboardManager'
  34. import { wizardPages, executionOptions, providerTypes } from '../../../constants'
  35. import configLoader from '../../../utils/Config'
  36. import type { MainItem } from '../../../types/MainItem'
  37. import type { Endpoint as EndpointType, StorageBackend } from '../../../types/Endpoint'
  38. import type { Instance, Nic, Disk } from '../../../types/Instance'
  39. import type { Field } from '../../../types/Field'
  40. import type { Network, SecurityGroup } from '../../../types/Network'
  41. import type { Schedule } from '../../../types/Schedule'
  42. import type { WizardPage as WizardPageType } from '../../../types/WizardData'
  43. const Wrapper = styled.div``
  44. type Props = {
  45. match: any,
  46. location: { search: string },
  47. history: any,
  48. }
  49. type WizardType = 'migration' | 'replica'
  50. type State = {
  51. type: WizardType,
  52. showNewEndpointModal: boolean,
  53. nextButtonDisabled: boolean,
  54. newEndpointType: ?string,
  55. newEndpointFromSource?: boolean,
  56. }
  57. @observer
  58. class WizardPage extends React.Component<Props, State> {
  59. state = {
  60. type: 'migration',
  61. showNewEndpointModal: false,
  62. nextButtonDisabled: false,
  63. newEndpointType: null,
  64. }
  65. contentRef: WizardPageContent
  66. get instancesPerPage() {
  67. const min = 3
  68. const max = Infinity
  69. const instancesTableDiff = 505
  70. const instancesItemHeight = 67
  71. return Math.min(max, Math.max(min, Math.floor((window.innerHeight - instancesTableDiff) / instancesItemHeight)))
  72. }
  73. get pages() {
  74. let sourceProvider = wizardStore.data.source ? wizardStore.data.source.type : ''
  75. let destProvider = wizardStore.data.target ? wizardStore.data.target.type || '' : ''
  76. let pages = wizardPages
  77. let sourceOptionsProviders = configLoader.config.sourceOptionsProviders
  78. let hasStorageMapping = () => providerStore.providers && providerStore.providers[destProvider]
  79. ? !!providerStore.providers[destProvider].types.find(t => t === providerTypes.STORAGE) : false
  80. return pages
  81. .filter(p => !p.excludeFrom || p.excludeFrom !== this.state.type)
  82. .filter(p => p.id !== 'storage' || hasStorageMapping())
  83. .filter(p => p.id !== 'source-options'
  84. || sourceOptionsProviders.find(p => p === sourceProvider))
  85. }
  86. componentWillMount() {
  87. this.initializeState()
  88. this.handleResize()
  89. }
  90. componentDidMount() {
  91. document.title = 'Coriolis Wizard'
  92. KeyboardManager.onEnter('wizard', () => { this.handleEnterKey() })
  93. KeyboardManager.onEsc('wizard', () => { this.handleEscKey() })
  94. window.addEventListener('resize', this.handleResize)
  95. }
  96. componentWillReceiveProps(newProps: Props) {
  97. if (newProps.location.search === this.props.location.search) {
  98. return
  99. }
  100. wizardStore.clearData()
  101. this.initializeState()
  102. }
  103. componentWillUnmount() {
  104. wizardStore.clearData()
  105. instanceStore.cancelIntancesChunksLoading()
  106. KeyboardManager.removeKeyDown('wizard')
  107. window.removeEventListener('resize', this.handleResize, false)
  108. }
  109. @autobind
  110. handleResize() {
  111. instanceStore.updateInstancesPerPage(this.instancesPerPage)
  112. }
  113. handleEnterKey() {
  114. if (this.contentRef && !this.contentRef.isNextButtonDisabled()) {
  115. this.handleNextClick()
  116. }
  117. }
  118. handleEscKey() {
  119. this.handleBackClick()
  120. }
  121. async handleCreationSuccess(items: MainItem[]) {
  122. let typeLabel = this.state.type.charAt(0).toUpperCase() + this.state.type.substr(1)
  123. notificationStore.alert(`${typeLabel} was succesfully created`, 'success')
  124. let schedulePromise = Promise.resolve()
  125. if (this.state.type === 'replica') {
  126. items.forEach(replica => {
  127. this.executeCreatedReplica(replica)
  128. schedulePromise = this.scheduleReplica(replica)
  129. })
  130. }
  131. if (items.length === 1) {
  132. let location = `/${this.state.type}/`
  133. if (this.state.type === 'replica') {
  134. location += 'executions/'
  135. } else {
  136. location += 'tasks/'
  137. }
  138. await schedulePromise
  139. this.props.history.push(location + items[0].id)
  140. } else {
  141. this.props.history.push(`/${this.state.type}s`)
  142. }
  143. }
  144. handleUserItemClick(item: { value: string }) {
  145. switch (item.value) {
  146. case 'signout':
  147. userStore.logout()
  148. break
  149. default:
  150. }
  151. }
  152. handleTypeChange(isReplica: ?boolean) {
  153. wizardStore.updateData({
  154. target: null,
  155. networks: null,
  156. destOptions: null,
  157. sourceOptions: null,
  158. selectedInstances: null,
  159. source: null,
  160. })
  161. wizardStore.clearStorageMap()
  162. wizardStore.setPermalink(wizardStore.data)
  163. this.setState({ type: isReplica ? 'replica' : 'migration' })
  164. }
  165. handleBackClick() {
  166. let currentPageIndex = this.pages.findIndex(p => p.id === wizardStore.currentPage.id)
  167. if (currentPageIndex === 0) {
  168. window.history.back()
  169. return
  170. }
  171. let page = this.pages[currentPageIndex - 1]
  172. this.loadDataForPage(page)
  173. wizardStore.setCurrentPage(page)
  174. }
  175. handleNextClick() {
  176. let currentPageIndex = this.pages.findIndex(p => p.id === wizardStore.currentPage.id)
  177. if (currentPageIndex === this.pages.length - 1) {
  178. this.create()
  179. return
  180. }
  181. let page = this.pages[currentPageIndex + 1]
  182. this.loadDataForPage(page)
  183. wizardStore.setCurrentPage(page)
  184. }
  185. async handleSourceEndpointChange(source: ?EndpointType) {
  186. wizardStore.updateData({ source, selectedInstances: null, networks: null, sourceOptions: null })
  187. wizardStore.clearStorageMap()
  188. wizardStore.setPermalink(wizardStore.data)
  189. if (!source) {
  190. return
  191. }
  192. await providerStore.loadOptionsSchema({
  193. providerName: source.type,
  194. schemaType: this.state.type,
  195. optionsType: 'source',
  196. useCache: true,
  197. })
  198. source && providerStore.getOptionsValues({
  199. optionsType: 'source',
  200. endpointId: source.id,
  201. providerName: source.type,
  202. useCache: true,
  203. })
  204. }
  205. async handleTargetEndpointChange(target: EndpointType) {
  206. wizardStore.updateData({ target, networks: null, destOptions: null })
  207. wizardStore.clearStorageMap()
  208. wizardStore.setPermalink(wizardStore.data)
  209. if (this.pages.find(p => p.id === 'storage')) {
  210. endpointStore.loadStorage(target.id, {})
  211. }
  212. // Preload destination options schema
  213. await providerStore.loadOptionsSchema({
  214. providerName: target.type,
  215. schemaType: this.state.type,
  216. optionsType: 'destination',
  217. useCache: true,
  218. })
  219. // Preload destination options values
  220. providerStore.getOptionsValues({
  221. optionsType: 'destination',
  222. endpointId: target.id,
  223. providerName: target.type,
  224. useCache: true,
  225. })
  226. }
  227. handleAddEndpoint(newEndpointType: string, newEndpointFromSource: boolean) {
  228. this.setState({
  229. showNewEndpointModal: true,
  230. newEndpointType,
  231. newEndpointFromSource,
  232. })
  233. }
  234. handleCloseNewEndpointModal(options?: { autoClose?: boolean }) {
  235. if (options) {
  236. if (this.state.newEndpointFromSource) {
  237. wizardStore.updateData({ source: endpointStore.endpoints[0] })
  238. } else {
  239. wizardStore.updateData({ target: endpointStore.endpoints[0] })
  240. }
  241. }
  242. wizardStore.setPermalink(wizardStore.data)
  243. this.setState({ showNewEndpointModal: false })
  244. }
  245. handleInstancesSearchInputChange(searchText: string) {
  246. if (wizardStore.data.source) {
  247. instanceStore.searchInstances(wizardStore.data.source, searchText)
  248. }
  249. }
  250. handleInstancesReloadClick() {
  251. if (wizardStore.data.source) {
  252. instanceStore.reloadInstances(wizardStore.data.source, this.instancesPerPage, wizardStore.data.sourceOptions)
  253. }
  254. }
  255. handleInstanceClick(instance: Instance) {
  256. wizardStore.updateData({ networks: null })
  257. wizardStore.clearStorageMap()
  258. wizardStore.toggleInstanceSelection(instance)
  259. wizardStore.setPermalink(wizardStore.data)
  260. }
  261. handleInstancePageClick(page: number) {
  262. instanceStore.setPage(page)
  263. }
  264. handleDestOptionsChange(field: Field, value: any) {
  265. wizardStore.updateData({ networks: null })
  266. wizardStore.clearStorageMap()
  267. wizardStore.updateDestOptions({ field, value })
  268. // If the field is a string and doesn't have an enum property,
  269. // we can't call destination options on "change" since too many calls will be made,
  270. // it also means a potential problem with the server not populating the "enum" prop.
  271. // Otherwise, the field has enum property, which there potentially other destination options for the new
  272. // chosen value from the enum
  273. if (field.type !== 'string' || field.enum) {
  274. this.loadExtraOptions(field, 'destination')
  275. }
  276. wizardStore.setPermalink(wizardStore.data)
  277. }
  278. handleSourceOptionsChange(field: Field, value: any) {
  279. wizardStore.updateData({ selectedInstances: [] })
  280. wizardStore.updateSourceOptions({ field, value })
  281. if (field.type !== 'string' || field.enum) {
  282. this.loadExtraOptions(field, 'source')
  283. }
  284. wizardStore.setPermalink(wizardStore.data)
  285. }
  286. handleNetworkChange(sourceNic: Nic, targetNetwork: Network, targetSecurityGroups: ?SecurityGroup[]) {
  287. wizardStore.updateNetworks({ sourceNic, targetNetwork, targetSecurityGroups })
  288. wizardStore.setPermalink(wizardStore.data)
  289. }
  290. handleStorageChange(source: Disk, target: StorageBackend, type: 'backend' | 'disk') {
  291. wizardStore.updateStorage({ source, target, type })
  292. }
  293. handleAddScheduleClick(schedule: Schedule) {
  294. wizardStore.addSchedule(schedule)
  295. }
  296. handleScheduleChange(scheduleId: string, data: Schedule) {
  297. wizardStore.updateSchedule(scheduleId, data)
  298. }
  299. handleScheduleRemove(scheduleId: string) {
  300. wizardStore.removeSchedule(scheduleId)
  301. }
  302. async handleReloadOptionsClick() {
  303. let optionsType: 'source' | 'destination' = wizardStore.currentPage.id === 'source-options' ? 'source' : 'destination'
  304. let endpoint = optionsType === 'source' ? wizardStore.data.source : wizardStore.data.target
  305. if (!endpoint) {
  306. return
  307. }
  308. await providerStore.loadOptionsSchema({
  309. providerName: endpoint.type,
  310. schemaType: this.state.type,
  311. optionsType,
  312. })
  313. await providerStore.getOptionsValues({
  314. optionsType,
  315. endpointId: endpoint.id,
  316. providerName: endpoint.type,
  317. })
  318. await this.loadExtraOptions(undefined, optionsType, false)
  319. }
  320. initializeState() {
  321. wizardStore.getDataFromPermalink()
  322. let type = this.props.match && this.props.match.params.type
  323. if (type === 'migration' || type === 'replica') {
  324. this.setState({ type })
  325. }
  326. }
  327. loadExtraOptions(field?: Field, type: 'source' | 'destination', useCache: boolean = true) {
  328. let endpoint = type === 'source' ? wizardStore.data.source : wizardStore.data.target
  329. if (!endpoint) {
  330. return
  331. }
  332. let envData = getFieldChangeOptions({
  333. providerName: endpoint.type,
  334. schema: type === 'source' ? providerStore.sourceSchema : providerStore.destinationSchema,
  335. data: type === 'source' ? wizardStore.data.sourceOptions : wizardStore.data.destOptions,
  336. field,
  337. type,
  338. })
  339. if (!envData) {
  340. return
  341. }
  342. providerStore.getOptionsValues({
  343. optionsType: type,
  344. endpointId: endpoint.id,
  345. providerName: endpoint.type,
  346. envData,
  347. useCache,
  348. })
  349. }
  350. async loadDataForPage(page: WizardPageType) {
  351. const loadOptions = async (endpoint: EndpointType, optionsType: 'source' | 'destination') => {
  352. let schema = optionsType === 'source' ? providerStore.sourceSchema : providerStore.destinationSchema
  353. if (schema.length > 0) {
  354. return
  355. }
  356. await providerStore.loadOptionsSchema({
  357. providerName: endpoint.type,
  358. schemaType: this.state.type,
  359. optionsType,
  360. useCache: true,
  361. })
  362. // Preload source options if data is set from 'Permalink'
  363. if (providerStore.sourceOptions.length === 0) {
  364. await providerStore.getOptionsValues({
  365. optionsType,
  366. endpointId: endpoint.id,
  367. providerName: endpoint.type,
  368. useCache: true,
  369. })
  370. await this.loadExtraOptions(undefined, optionsType)
  371. }
  372. }
  373. switch (page.id) {
  374. case 'source': {
  375. providerStore.loadProviders()
  376. endpointStore.getEndpoints()
  377. // Preload instances if data is set from 'Permalink'
  378. let source = wizardStore.data.source
  379. if (!source) {
  380. return
  381. }
  382. // Preload source options schema
  383. loadOptions(source, 'source')
  384. break
  385. }
  386. case 'vms': {
  387. if (!wizardStore.data.source) {
  388. return
  389. }
  390. instanceStore.loadInstancesInChunks({
  391. endpoint: wizardStore.data.source,
  392. vmsPerPage: this.instancesPerPage,
  393. env: wizardStore.data.sourceOptions,
  394. useCache: true,
  395. })
  396. break
  397. }
  398. case 'target': {
  399. let target = wizardStore.data.target
  400. if (!target) {
  401. return
  402. }
  403. // Preload Storage Mapping
  404. if (this.pages.find(p => p.id === 'storage')) {
  405. endpointStore.loadStorage(target.id, {})
  406. }
  407. // Preload destination options schema
  408. loadOptions(target, 'destination')
  409. break
  410. }
  411. case 'networks':
  412. this.loadNetworks(true)
  413. break
  414. default:
  415. }
  416. }
  417. loadNetworks(cache: boolean) {
  418. if (wizardStore.data.source && wizardStore.data.selectedInstances) {
  419. instanceStore.loadInstancesDetails({
  420. endpointId: wizardStore.data.source.id,
  421. instancesInfo: wizardStore.data.selectedInstances,
  422. env: wizardStore.data.sourceOptions,
  423. cache,
  424. })
  425. }
  426. if (wizardStore.data.target) {
  427. let id = wizardStore.data.target.id
  428. networkStore.loadNetworks(id, wizardStore.data.destOptions, { cache })
  429. }
  430. }
  431. async createMultiple() {
  432. let typeLabel = this.state.type.charAt(0).toUpperCase() + this.state.type.substr(1)
  433. notificationStore.alert(`Creating ${typeLabel}s ...`)
  434. await wizardStore.createMultiple(this.state.type, wizardStore.data, wizardStore.storageMap)
  435. let items = wizardStore.createdItems
  436. if (!items) {
  437. notificationStore.alert(`${typeLabel}s couldn't be created`, 'error')
  438. this.setState({ nextButtonDisabled: false })
  439. return
  440. }
  441. this.handleCreationSuccess(items)
  442. }
  443. async createSingle() {
  444. let typeLabel = this.state.type.charAt(0).toUpperCase() + this.state.type.substr(1)
  445. notificationStore.alert(`Creating ${typeLabel} ...`)
  446. try {
  447. await wizardStore.create(this.state.type, wizardStore.data, wizardStore.storageMap)
  448. let item = wizardStore.createdItem
  449. if (!item) {
  450. notificationStore.alert(`${typeLabel} couldn't be created`, 'error')
  451. this.setState({ nextButtonDisabled: false })
  452. return
  453. }
  454. this.handleCreationSuccess([item])
  455. } catch (err) {
  456. this.setState({ nextButtonDisabled: false })
  457. }
  458. }
  459. separateVms() {
  460. let data = wizardStore.data
  461. let separateVms = true
  462. if (data.destOptions && data.destOptions.separate_vm != null) {
  463. separateVms = data.destOptions.separate_vm
  464. }
  465. if (data.selectedInstances && data.selectedInstances.length === 1) {
  466. separateVms = false
  467. }
  468. if (separateVms) {
  469. this.createMultiple()
  470. } else {
  471. this.createSingle()
  472. }
  473. }
  474. create() {
  475. this.setState({ nextButtonDisabled: true })
  476. this.separateVms()
  477. }
  478. scheduleReplica(replica: MainItem): Promise<void> {
  479. if (wizardStore.schedules.length === 0) {
  480. return Promise.resolve()
  481. }
  482. return scheduleStore.scheduleMultiple(replica.id, wizardStore.schedules)
  483. }
  484. executeCreatedReplica(replica: MainItem) {
  485. let options = wizardStore.data.destOptions
  486. let executeNow = true
  487. if (options && options.execute_now != null) {
  488. executeNow = options.execute_now
  489. }
  490. if (!executeNow) {
  491. return
  492. }
  493. let executeNowOptions = executionOptions.map(field => {
  494. if (options && options[field.name] != null) {
  495. return { name: field.name, value: options[field.name] }
  496. }
  497. return field
  498. })
  499. replicaStore.execute(replica.id, executeNowOptions)
  500. }
  501. render() {
  502. return (
  503. <Wrapper>
  504. <WizardTemplate
  505. pageHeaderComponent={<DetailsPageHeader
  506. user={userStore.loggedUser}
  507. onUserItemClick={item => { this.handleUserItemClick(item) }}
  508. />}
  509. pageContentComponent={<WizardPageContent
  510. pages={this.pages}
  511. page={wizardStore.currentPage}
  512. providerStore={providerStore}
  513. instanceStore={instanceStore}
  514. networkStore={networkStore}
  515. endpointStore={endpointStore}
  516. wizardData={wizardStore.data}
  517. hasStorageMap={Boolean(this.pages.find(p => p.id === 'storage'))}
  518. hasSourceOptions={Boolean(this.pages.find(p => p.id === 'source-options'))}
  519. storageMap={wizardStore.storageMap}
  520. schedules={wizardStore.schedules}
  521. nextButtonDisabled={this.state.nextButtonDisabled}
  522. type={this.state.type}
  523. onTypeChange={isReplica => { this.handleTypeChange(isReplica) }}
  524. onBackClick={() => { this.handleBackClick() }}
  525. onNextClick={() => { this.handleNextClick() }}
  526. onSourceEndpointChange={endpoint => { this.handleSourceEndpointChange(endpoint) }}
  527. onTargetEndpointChange={endpoint => { this.handleTargetEndpointChange(endpoint) }}
  528. onAddEndpoint={(type, fromSource) => { this.handleAddEndpoint(type, fromSource) }}
  529. onInstancesSearchInputChange={searchText => { this.handleInstancesSearchInputChange(searchText) }}
  530. onInstancesReloadClick={() => { this.handleInstancesReloadClick() }}
  531. onInstanceClick={instance => { this.handleInstanceClick(instance) }}
  532. onInstancePageClick={page => { this.handleInstancePageClick(page) }}
  533. onDestOptionsChange={(field, value) => { this.handleDestOptionsChange(field, value) }}
  534. onSourceOptionsChange={(field, value) => { this.handleSourceOptionsChange(field, value) }}
  535. onNetworkChange={(sourceNic, targetNetwork, secGroups) => { this.handleNetworkChange(sourceNic, targetNetwork, secGroups) }}
  536. onStorageChange={(source, target, type) => { this.handleStorageChange(source, target, type) }}
  537. onAddScheduleClick={schedule => { this.handleAddScheduleClick(schedule) }}
  538. onScheduleChange={(scheduleId, data) => { this.handleScheduleChange(scheduleId, data) }}
  539. onScheduleRemove={scheduleId => { this.handleScheduleRemove(scheduleId) }}
  540. onContentRef={ref => { this.contentRef = ref }}
  541. onReloadOptionsClick={() => { this.handleReloadOptionsClick() }}
  542. onReloadNetworksClick={() => { this.loadNetworks(false) }}
  543. />}
  544. />
  545. <Modal
  546. isOpen={this.state.showNewEndpointModal}
  547. title="New Cloud Endpoint"
  548. onRequestClose={() => { this.handleCloseNewEndpointModal() }}
  549. >
  550. <Endpoint
  551. type={this.state.newEndpointType}
  552. onCancelClick={autoClose => { this.handleCloseNewEndpointModal(autoClose) }}
  553. />
  554. </Modal>
  555. </Wrapper>
  556. )
  557. }
  558. }
  559. export default WizardPage