WizardPage.jsx 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609
  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. let getConnectionInfo = async () => {
  190. if (!source) {
  191. return
  192. }
  193. // Check if user has permission for this endpoint
  194. try {
  195. await endpointStore.getConnectionInfo(source)
  196. } catch (err) {
  197. this.handleSourceEndpointChange(null)
  198. }
  199. }
  200. getConnectionInfo()
  201. if (!source) {
  202. return
  203. }
  204. await providerStore.loadOptionsSchema({
  205. providerName: source.type,
  206. schemaType: this.state.type,
  207. optionsType: 'source',
  208. })
  209. source && providerStore.getOptionsValues({
  210. optionsType: 'source',
  211. endpointId: source.id,
  212. providerName: source.type,
  213. })
  214. }
  215. async handleTargetEndpointChange(target: EndpointType) {
  216. wizardStore.updateData({ target, networks: null, destOptions: null })
  217. wizardStore.clearStorageMap()
  218. wizardStore.setPermalink(wizardStore.data)
  219. if (this.pages.find(p => p.id === 'storage')) {
  220. endpointStore.loadStorage(target.id, {})
  221. }
  222. // Preload destination options schema
  223. await providerStore.loadOptionsSchema({
  224. providerName: target.type,
  225. schemaType: this.state.type,
  226. optionsType: 'destination',
  227. })
  228. // Preload destination options values
  229. providerStore.getOptionsValues({
  230. optionsType: 'destination',
  231. endpointId: target.id,
  232. providerName: target.type,
  233. })
  234. }
  235. handleAddEndpoint(newEndpointType: string, newEndpointFromSource: boolean) {
  236. this.setState({
  237. showNewEndpointModal: true,
  238. newEndpointType,
  239. newEndpointFromSource,
  240. })
  241. }
  242. handleCloseNewEndpointModal(options?: { autoClose?: boolean }) {
  243. if (options) {
  244. if (this.state.newEndpointFromSource) {
  245. wizardStore.updateData({ source: endpointStore.endpoints[0] })
  246. } else {
  247. wizardStore.updateData({ target: endpointStore.endpoints[0] })
  248. }
  249. }
  250. wizardStore.setPermalink(wizardStore.data)
  251. this.setState({ showNewEndpointModal: false })
  252. }
  253. handleInstancesSearchInputChange(searchText: string) {
  254. if (wizardStore.data.source) {
  255. instanceStore.searchInstances(wizardStore.data.source, searchText)
  256. }
  257. }
  258. handleInstancesReloadClick() {
  259. if (wizardStore.data.source) {
  260. instanceStore.reloadInstances(wizardStore.data.source, this.instancesPerPage, wizardStore.data.sourceOptions)
  261. }
  262. }
  263. handleInstanceClick(instance: Instance) {
  264. wizardStore.updateData({ networks: null })
  265. wizardStore.clearStorageMap()
  266. wizardStore.toggleInstanceSelection(instance)
  267. wizardStore.setPermalink(wizardStore.data)
  268. }
  269. handleInstancePageClick(page: number) {
  270. instanceStore.setPage(page)
  271. }
  272. handleDestOptionsChange(field: Field, value: any) {
  273. wizardStore.updateData({ networks: null })
  274. wizardStore.clearStorageMap()
  275. wizardStore.updateDestOptions({ field, value })
  276. // If the field is a string and doesn't have an enum property,
  277. // we can't call destination options on "change" since too many calls will be made,
  278. // it also means a potential problem with the server not populating the "enum" prop.
  279. // Otherwise, the field has enum property, which there potentially other destination options for the new
  280. // chosen value from the enum
  281. if (field.type !== 'string' || field.enum) {
  282. this.loadExtraOptions(field, 'destination')
  283. }
  284. wizardStore.setPermalink(wizardStore.data)
  285. }
  286. handleSourceOptionsChange(field: Field, value: any) {
  287. wizardStore.updateData({ selectedInstances: [] })
  288. wizardStore.updateSourceOptions({ field, value })
  289. if (field.type !== 'string' || field.enum) {
  290. this.loadExtraOptions(field, 'source')
  291. }
  292. wizardStore.setPermalink(wizardStore.data)
  293. }
  294. handleNetworkChange(sourceNic: Nic, targetNetwork: Network, targetSecurityGroups: ?SecurityGroup[]) {
  295. wizardStore.updateNetworks({ sourceNic, targetNetwork, targetSecurityGroups })
  296. wizardStore.setPermalink(wizardStore.data)
  297. }
  298. handleStorageChange(source: Disk, target: StorageBackend, type: 'backend' | 'disk') {
  299. wizardStore.updateStorage({ source, target, type })
  300. }
  301. handleAddScheduleClick(schedule: Schedule) {
  302. wizardStore.addSchedule(schedule)
  303. }
  304. handleScheduleChange(scheduleId: string, data: Schedule) {
  305. wizardStore.updateSchedule(scheduleId, data)
  306. }
  307. handleScheduleRemove(scheduleId: string) {
  308. wizardStore.removeSchedule(scheduleId)
  309. }
  310. initializeState() {
  311. wizardStore.getDataFromPermalink()
  312. let type = this.props.match && this.props.match.params.type
  313. if (type === 'migration' || type === 'replica') {
  314. this.setState({ type })
  315. }
  316. }
  317. loadExtraOptions(field?: Field, type: 'source' | 'destination') {
  318. let endpoint = type === 'source' ? wizardStore.data.source : wizardStore.data.target
  319. if (!endpoint) {
  320. return
  321. }
  322. let envData = getFieldChangeOptions({
  323. providerName: endpoint.type,
  324. schema: type === 'source' ? providerStore.sourceSchema : providerStore.destinationSchema,
  325. data: type === 'source' ? wizardStore.data.sourceOptions : wizardStore.data.destOptions,
  326. field,
  327. type,
  328. })
  329. if (!envData) {
  330. return
  331. }
  332. providerStore.getOptionsValues({
  333. optionsType: type,
  334. endpointId: endpoint.id,
  335. providerName: endpoint.type,
  336. envData,
  337. })
  338. }
  339. async loadDataForPage(page: WizardPageType) {
  340. const loadOptions = async (endpoint: EndpointType, optionsType: 'source' | 'destination') => {
  341. let schema = optionsType === 'source' ? providerStore.sourceSchema : providerStore.destinationSchema
  342. if (schema.length > 0) {
  343. return
  344. }
  345. await providerStore.loadOptionsSchema({
  346. providerName: endpoint.type,
  347. schemaType: this.state.type,
  348. optionsType,
  349. })
  350. // Preload source options if data is set from 'Permalink'
  351. if (providerStore.sourceOptions.length === 0) {
  352. await providerStore.getOptionsValues({
  353. optionsType,
  354. endpointId: endpoint.id,
  355. providerName: endpoint.type,
  356. })
  357. await this.loadExtraOptions(undefined, optionsType)
  358. }
  359. }
  360. switch (page.id) {
  361. case 'source': {
  362. providerStore.loadProviders()
  363. endpointStore.getEndpoints()
  364. // Preload instances if data is set from 'Permalink'
  365. let source = wizardStore.data.source
  366. if (!source) {
  367. return
  368. }
  369. // Preload source options schema
  370. loadOptions(source, 'source')
  371. if (instanceStore.instances.length > 0) {
  372. return
  373. }
  374. try {
  375. // Check if user has permission for this endpoint
  376. await endpointStore.getConnectionInfo(source)
  377. } catch (err) {
  378. this.handleSourceEndpointChange(null)
  379. }
  380. break
  381. }
  382. case 'vms': {
  383. if (!wizardStore.data.source) {
  384. return
  385. }
  386. instanceStore.loadInstancesInChunks(wizardStore.data.source, this.instancesPerPage, false, wizardStore.data.sourceOptions)
  387. break
  388. }
  389. case 'target': {
  390. let target = wizardStore.data.target
  391. if (!target) {
  392. return
  393. }
  394. // Preload Storage Mapping
  395. if (this.pages.find(p => p.id === 'storage')) {
  396. endpointStore.loadStorage(target.id, {})
  397. }
  398. // Preload destination options schema
  399. loadOptions(target, 'destination')
  400. break
  401. }
  402. case 'networks':
  403. if (wizardStore.data.source && wizardStore.data.selectedInstances) {
  404. instanceStore.loadInstancesDetails({
  405. endpointId: wizardStore.data.source.id,
  406. instancesInfo: wizardStore.data.selectedInstances,
  407. env: wizardStore.data.sourceOptions,
  408. })
  409. }
  410. if (wizardStore.data.target) {
  411. let id = wizardStore.data.target.id
  412. networkStore.loadNetworks(id, wizardStore.data.destOptions)
  413. }
  414. break
  415. default:
  416. }
  417. }
  418. async createMultiple() {
  419. let typeLabel = this.state.type.charAt(0).toUpperCase() + this.state.type.substr(1)
  420. notificationStore.alert(`Creating ${typeLabel}s ...`)
  421. await wizardStore.createMultiple(this.state.type, wizardStore.data, wizardStore.storageMap)
  422. let items = wizardStore.createdItems
  423. if (!items) {
  424. notificationStore.alert(`${typeLabel}s couldn't be created`, 'error')
  425. this.setState({ nextButtonDisabled: false })
  426. return
  427. }
  428. this.handleCreationSuccess(items)
  429. }
  430. async createSingle() {
  431. let typeLabel = this.state.type.charAt(0).toUpperCase() + this.state.type.substr(1)
  432. notificationStore.alert(`Creating ${typeLabel} ...`)
  433. try {
  434. await wizardStore.create(this.state.type, wizardStore.data, wizardStore.storageMap)
  435. let item = wizardStore.createdItem
  436. if (!item) {
  437. notificationStore.alert(`${typeLabel} couldn't be created`, 'error')
  438. this.setState({ nextButtonDisabled: false })
  439. return
  440. }
  441. this.handleCreationSuccess([item])
  442. } catch (err) {
  443. this.setState({ nextButtonDisabled: false })
  444. }
  445. }
  446. separateVms() {
  447. let data = wizardStore.data
  448. let separateVms = true
  449. if (data.destOptions && data.destOptions.separate_vm != null) {
  450. separateVms = data.destOptions.separate_vm
  451. }
  452. if (data.selectedInstances && data.selectedInstances.length === 1) {
  453. separateVms = false
  454. }
  455. if (separateVms) {
  456. this.createMultiple()
  457. } else {
  458. this.createSingle()
  459. }
  460. }
  461. create() {
  462. this.setState({ nextButtonDisabled: true })
  463. this.separateVms()
  464. }
  465. scheduleReplica(replica: MainItem): Promise<void> {
  466. if (wizardStore.schedules.length === 0) {
  467. return Promise.resolve()
  468. }
  469. return scheduleStore.scheduleMultiple(replica.id, wizardStore.schedules)
  470. }
  471. executeCreatedReplica(replica: MainItem) {
  472. let options = wizardStore.data.destOptions
  473. let executeNow = true
  474. if (options && options.execute_now != null) {
  475. executeNow = options.execute_now
  476. }
  477. if (!executeNow) {
  478. return
  479. }
  480. let executeNowOptions = executionOptions.map(field => {
  481. if (options && options[field.name] != null) {
  482. return { name: field.name, value: options[field.name] }
  483. }
  484. return field
  485. })
  486. replicaStore.execute(replica.id, executeNowOptions)
  487. }
  488. render() {
  489. return (
  490. <Wrapper>
  491. <WizardTemplate
  492. pageHeaderComponent={<DetailsPageHeader
  493. user={userStore.loggedUser}
  494. onUserItemClick={item => { this.handleUserItemClick(item) }}
  495. />}
  496. pageContentComponent={<WizardPageContent
  497. pages={this.pages}
  498. page={wizardStore.currentPage}
  499. providerStore={providerStore}
  500. instanceStore={instanceStore}
  501. networkStore={networkStore}
  502. endpointStore={endpointStore}
  503. wizardData={wizardStore.data}
  504. hasStorageMap={Boolean(this.pages.find(p => p.id === 'storage'))}
  505. hasSourceOptions={Boolean(this.pages.find(p => p.id === 'source-options'))}
  506. storageMap={wizardStore.storageMap}
  507. schedules={wizardStore.schedules}
  508. nextButtonDisabled={this.state.nextButtonDisabled}
  509. type={this.state.type}
  510. onTypeChange={isReplica => { this.handleTypeChange(isReplica) }}
  511. onBackClick={() => { this.handleBackClick() }}
  512. onNextClick={() => { this.handleNextClick() }}
  513. onSourceEndpointChange={endpoint => { this.handleSourceEndpointChange(endpoint) }}
  514. onTargetEndpointChange={endpoint => { this.handleTargetEndpointChange(endpoint) }}
  515. onAddEndpoint={(type, fromSource) => { this.handleAddEndpoint(type, fromSource) }}
  516. onInstancesSearchInputChange={searchText => { this.handleInstancesSearchInputChange(searchText) }}
  517. onInstancesReloadClick={() => { this.handleInstancesReloadClick() }}
  518. onInstanceClick={instance => { this.handleInstanceClick(instance) }}
  519. onInstancePageClick={page => { this.handleInstancePageClick(page) }}
  520. onDestOptionsChange={(field, value) => { this.handleDestOptionsChange(field, value) }}
  521. onSourceOptionsChange={(field, value) => { this.handleSourceOptionsChange(field, value) }}
  522. onNetworkChange={(sourceNic, targetNetwork, secGroups) => { this.handleNetworkChange(sourceNic, targetNetwork, secGroups) }}
  523. onStorageChange={(source, target, type) => { this.handleStorageChange(source, target, type) }}
  524. onAddScheduleClick={schedule => { this.handleAddScheduleClick(schedule) }}
  525. onScheduleChange={(scheduleId, data) => { this.handleScheduleChange(scheduleId, data) }}
  526. onScheduleRemove={scheduleId => { this.handleScheduleRemove(scheduleId) }}
  527. onContentRef={ref => { this.contentRef = ref }}
  528. />}
  529. />
  530. <Modal
  531. isOpen={this.state.showNewEndpointModal}
  532. title="New Cloud Endpoint"
  533. onRequestClose={() => { this.handleCloseNewEndpointModal() }}
  534. >
  535. <Endpoint
  536. type={this.state.newEndpointType}
  537. onCancelClick={autoClose => { this.handleCloseNewEndpointModal(autoClose) }}
  538. />
  539. </Modal>
  540. </Wrapper>
  541. )
  542. }
  543. }
  544. export default WizardPage