WizardPageContent.jsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472
  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 { observer } from 'mobx-react'
  18. import EndpointLogos from '../../atoms/EndpointLogos'
  19. import WizardType from '../../molecules/WizardType'
  20. import Button from '../../atoms/Button'
  21. import WizardBreadcrumbs from '../../molecules/WizardBreadcrumbs'
  22. import WizardEndpointList from '../WizardEndpointList'
  23. import WizardInstances from '../WizardInstances'
  24. import WizardNetworks from '../WizardNetworks'
  25. import WizardStorage from '../WizardStorage'
  26. import WizardOptions from '../WizardOptions'
  27. import Schedule from '../Schedule'
  28. import WizardSummary from '../WizardSummary'
  29. import StyleProps from '../../styleUtils/StyleProps'
  30. import Palette from '../../styleUtils/Palette'
  31. import { providerTypes, wizardPages } from '../../../constants'
  32. import type { WizardData, WizardPage } from '../../../types/WizardData'
  33. import type { Endpoint, StorageBackend, StorageMap } from '../../../types/Endpoint'
  34. import type { Instance, Nic, Disk } from '../../../types/Instance'
  35. import type { Field } from '../../../types/Field'
  36. import type { Network } from '../../../types/Network'
  37. import type { Schedule as ScheduleType } from '../../../types/Schedule'
  38. import instanceStore from '../../../stores/InstanceStore'
  39. import providerStore from '../../../stores/ProviderStore'
  40. import endpointStore from '../../../stores/EndpointStore'
  41. import networkStore from '../../../stores/NetworkStore'
  42. import migrationArrowImage from './images/migration.js'
  43. const Wrapper = styled.div`
  44. ${StyleProps.exactWidth(`${parseInt(StyleProps.contentWidth, 10) + 64}px`)}
  45. margin: 64px auto 32px auto;
  46. position: absolute;
  47. top: 0;
  48. left: 0;
  49. right: 0;
  50. bottom: 0;
  51. display: flex;
  52. flex-direction: column;
  53. `
  54. const Header = styled.div`
  55. text-align: center;
  56. font-size: 32px;
  57. font-weight: ${StyleProps.fontWeights.light};
  58. color: ${Palette.primary};
  59. margin-bottom: 32px;
  60. `
  61. const Body = styled.div`
  62. flex-grow: 1;
  63. overflow: auto;
  64. display: flex;
  65. justify-content: center;
  66. padding: 0 32px;
  67. `
  68. const Navigation = styled.div`
  69. display: flex;
  70. justify-content: space-between;
  71. padding: 16px 32px 0 32px;
  72. margin-bottom: 80px;
  73. `
  74. const IconRepresentation = styled.div`
  75. display: flex;
  76. justify-content: center;
  77. flex-grow: 1;
  78. margin: 0 76px;
  79. `
  80. const Footer = styled.div``
  81. const WizardTypeIcon = styled.div`
  82. width: 60px;
  83. height: 32px;
  84. display: flex;
  85. justify-content: center;
  86. align-items: center;
  87. margin: 0 32px;
  88. `
  89. export const isOptionsPageValid = (data: ?any, schema: Field[]) => {
  90. const isValid = (field: Field): boolean => {
  91. if (data) {
  92. let fieldValue = data[field.name]
  93. if (fieldValue === null) {
  94. return false
  95. }
  96. if (fieldValue === undefined) {
  97. return field.default != null
  98. }
  99. return Boolean(fieldValue)
  100. }
  101. return field.default != null
  102. }
  103. if (schema && schema.length > 0) {
  104. let required = schema.filter(f => f.required && f.type !== 'object')
  105. schema.forEach(f => {
  106. if (f.type === 'object' && f.properties && f.properties.filter && f.properties.filter(p => isValid(p)).length > 0) {
  107. required = required.concat(f.properties.filter(p => p.required))
  108. }
  109. })
  110. let validFieldsCount = 0
  111. required.forEach(f => {
  112. if (isValid(f)) {
  113. validFieldsCount += 1
  114. }
  115. })
  116. if (validFieldsCount === required.length) {
  117. return true
  118. }
  119. }
  120. return false
  121. }
  122. type Props = {
  123. page: { id: string, title: string },
  124. type: 'replica' | 'migration',
  125. nextButtonDisabled: boolean,
  126. providerStore: typeof providerStore,
  127. instanceStore: typeof instanceStore,
  128. networkStore: typeof networkStore,
  129. endpointStore: typeof endpointStore,
  130. wizardData: WizardData,
  131. schedules: ScheduleType[],
  132. storageMap: StorageMap[],
  133. hasStorageMap: boolean,
  134. pages: WizardPage[],
  135. onTypeChange: (isReplicaChecked: ?boolean) => void,
  136. onBackClick: () => void,
  137. onNextClick: () => void,
  138. onSourceEndpointChange: (endpoint: Endpoint) => void,
  139. onTargetEndpointChange: (endpoint: Endpoint) => void,
  140. onAddEndpoint: (provider: string, fromSource: boolean) => void,
  141. onInstancesSearchInputChange: (searchText: string) => void,
  142. onInstancesReloadClick: () => void,
  143. onInstanceClick: (instance: Instance) => void,
  144. onInstancePageClick: (page: number) => void,
  145. onDestOptionsChange: (field: Field, value: any) => void,
  146. onSourceOptionsChange: (field: Field, value: any) => void,
  147. onNetworkChange: (nic: Nic, network: Network) => void,
  148. onStorageChange: (sourceStorage: Disk, targetStorage: StorageBackend, type: 'backend' | 'disk') => void,
  149. onAddScheduleClick: (schedule: ScheduleType) => void,
  150. onScheduleChange: (scheduleId: string, schedule: ScheduleType) => void,
  151. onScheduleRemove: (scheudleId: string) => void,
  152. onContentRef: (ref: any) => void,
  153. }
  154. type TimezoneValue = 'local' | 'utc'
  155. type State = {
  156. useAdvancedOptions: boolean,
  157. timezone: TimezoneValue,
  158. }
  159. const testName = 'wpContent'
  160. @observer
  161. class WizardPageContent extends React.Component<Props, State> {
  162. state = {
  163. useAdvancedOptions: false,
  164. timezone: 'local',
  165. }
  166. componentDidMount() {
  167. this.props.onContentRef(this)
  168. }
  169. componentWillUnmount() {
  170. this.props.onContentRef(null)
  171. }
  172. getProvidersType(type: string) {
  173. if (this.props.type === 'replica') {
  174. if (type === 'source') {
  175. return providerTypes.SOURCE_REPLICA
  176. }
  177. return providerTypes.TARGET_REPLICA
  178. }
  179. if (type === 'source') {
  180. return providerTypes.SOURCE_MIGRATION
  181. }
  182. return providerTypes.TARGET_MIGRATION
  183. }
  184. getProviders(type: string): string[] {
  185. let providers = []
  186. let providerType = this.getProvidersType(type)
  187. let providersObject = this.props.providerStore.providers
  188. if (!providersObject) {
  189. return []
  190. }
  191. Object.keys(providersObject).forEach(provider => {
  192. if (providersObject[provider].types.findIndex(t => t === providerType) > -1) {
  193. providers.push(provider)
  194. }
  195. })
  196. providers.sort((a, b) => a.localeCompare(b))
  197. return providers
  198. }
  199. isNetworksPageValid() {
  200. if (this.props.networkStore.loading || this.props.instanceStore.loadingInstancesDetails) {
  201. return false
  202. }
  203. let instances = this.props.instanceStore.instancesDetails
  204. if (instances.length === 0) {
  205. return true
  206. }
  207. if (instances.find(i => i.devices)) {
  208. if (instances.find(i => i.devices.nics && i.devices.nics.length > 0)) {
  209. return this.props.wizardData.networks && this.props.wizardData.networks.length > 0
  210. }
  211. return true
  212. }
  213. return false
  214. }
  215. isNextButtonDisabled() {
  216. if (this.props.nextButtonDisabled) {
  217. return true
  218. }
  219. switch (this.props.page.id) {
  220. case 'source':
  221. return !this.props.wizardData.source
  222. case 'target':
  223. return !this.props.wizardData.target
  224. case 'vms':
  225. return !this.props.wizardData.selectedInstances || !this.props.wizardData.selectedInstances.length
  226. case 'dest-options':
  227. return !isOptionsPageValid(this.props.wizardData.destOptions, this.props.providerStore.destinationSchema)
  228. case 'networks':
  229. return !this.isNetworksPageValid()
  230. default:
  231. return false
  232. }
  233. }
  234. handleAdvancedOptionsToggle(useAdvancedOptions: boolean) {
  235. this.setState({ useAdvancedOptions })
  236. }
  237. handleTimezoneChange(timezone: TimezoneValue) {
  238. this.setState({ timezone })
  239. }
  240. renderHeader() {
  241. let title = this.props.page.title
  242. if (this.props.page.id === 'type') {
  243. title += ` ${this.props.type.charAt(0).toUpperCase() + this.props.type.substr(1)}`
  244. }
  245. return <Header data-test-id={`${testName}-header`}>{title}</Header>
  246. }
  247. renderBody() {
  248. let body = null
  249. switch (this.props.page.id) {
  250. case 'type':
  251. body = (
  252. <WizardType
  253. selected={this.props.type}
  254. onChange={this.props.onTypeChange}
  255. />
  256. )
  257. break
  258. case 'source':
  259. body = (
  260. <WizardEndpointList
  261. providers={this.getProviders('source')}
  262. loading={this.props.providerStore.providersLoading}
  263. otherEndpoint={this.props.wizardData.target}
  264. selectedEndpoint={this.props.wizardData.source}
  265. endpoints={this.props.endpointStore.endpoints}
  266. onChange={this.props.onSourceEndpointChange}
  267. onAddEndpoint={type => { this.props.onAddEndpoint(type, true) }}
  268. />
  269. )
  270. break
  271. case 'target':
  272. body = (
  273. <WizardEndpointList
  274. providers={this.getProviders('target')}
  275. loading={this.props.providerStore.providersLoading}
  276. otherEndpoint={this.props.wizardData.source}
  277. selectedEndpoint={this.props.wizardData.target}
  278. endpoints={this.props.endpointStore.endpoints}
  279. onChange={this.props.onTargetEndpointChange}
  280. onAddEndpoint={type => { this.props.onAddEndpoint(type, false) }}
  281. />
  282. )
  283. break
  284. case 'vms':
  285. body = (
  286. <WizardInstances
  287. instances={this.props.instanceStore.instances}
  288. instancesPerPage={this.props.instanceStore.instancesPerPage}
  289. chunksLoading={this.props.instanceStore.chunksLoading}
  290. currentPage={this.props.instanceStore.currentPage}
  291. searchText={this.props.instanceStore.searchText}
  292. loading={this.props.instanceStore.instancesLoading}
  293. searching={this.props.instanceStore.searching}
  294. searchNotFound={this.props.instanceStore.searchNotFound}
  295. reloading={this.props.instanceStore.reloading}
  296. onSearchInputChange={this.props.onInstancesSearchInputChange}
  297. onReloadClick={this.props.onInstancesReloadClick}
  298. onInstanceClick={this.props.onInstanceClick}
  299. onPageClick={this.props.onInstancePageClick}
  300. selectedInstances={this.props.wizardData.selectedInstances}
  301. />
  302. )
  303. break
  304. case 'source-options':
  305. body = (
  306. <WizardOptions
  307. loading={this.props.providerStore.sourceSchemaLoading}
  308. fields={this.props.providerStore.sourceSchema}
  309. onChange={this.props.onSourceOptionsChange}
  310. data={this.props.wizardData.sourceOptions}
  311. useAdvancedOptions
  312. hasStorageMap={false}
  313. wizardType={`${this.props.type}-source-options`}
  314. />
  315. )
  316. break
  317. case 'dest-options':
  318. body = (
  319. <WizardOptions
  320. loading={this.props.providerStore.destinationSchemaLoading || this.props.providerStore.destinationOptionsLoading}
  321. selectedInstances={this.props.wizardData.selectedInstances}
  322. fields={this.props.providerStore.destinationSchema}
  323. onChange={this.props.onDestOptionsChange}
  324. data={this.props.wizardData.destOptions}
  325. useAdvancedOptions={this.state.useAdvancedOptions}
  326. hasStorageMap={this.props.hasStorageMap}
  327. storageBackends={this.props.endpointStore.storageBackends}
  328. wizardType={this.props.type}
  329. onAdvancedOptionsToggle={useAdvancedOptions => { this.handleAdvancedOptionsToggle(useAdvancedOptions) }}
  330. />
  331. )
  332. break
  333. case 'networks':
  334. body = (
  335. <WizardNetworks
  336. networks={this.props.networkStore.networks}
  337. selectedNetworks={this.props.wizardData.networks}
  338. loading={this.props.networkStore.loading}
  339. instancesDetails={this.props.instanceStore.instancesDetails}
  340. loadingInstancesDetails={this.props.instanceStore.loadingInstancesDetails}
  341. onChange={this.props.onNetworkChange}
  342. />
  343. )
  344. break
  345. case 'storage':
  346. body = (
  347. <WizardStorage
  348. storageBackends={this.props.endpointStore.storageBackends}
  349. instancesDetails={this.props.instanceStore.instancesDetails}
  350. storageMap={this.props.storageMap}
  351. defaultStorage={String(this.props.wizardData.destOptions ? this.props.wizardData.destOptions.default_storage : '')}
  352. onChange={this.props.onStorageChange}
  353. />
  354. )
  355. break
  356. case 'schedule':
  357. body = (
  358. <Schedule
  359. schedules={this.props.schedules}
  360. onAddScheduleClick={this.props.onAddScheduleClick}
  361. onChange={this.props.onScheduleChange}
  362. onRemove={this.props.onScheduleRemove}
  363. timezone={this.state.timezone}
  364. onTimezoneChange={timezone => { this.handleTimezoneChange(timezone) }}
  365. secondaryEmpty
  366. />
  367. )
  368. break
  369. case 'summary':
  370. body = (
  371. <WizardSummary
  372. data={this.props.wizardData}
  373. schedules={this.props.schedules}
  374. storageMap={this.props.storageMap}
  375. wizardType={this.props.type}
  376. instancesDetails={this.props.instanceStore.instancesDetails}
  377. defaultStorage={
  378. this.props.endpointStore.storageBackends.find(
  379. s => this.props.wizardData.destOptions ?
  380. s.name === this.props.wizardData.destOptions.default_storage :
  381. false
  382. )
  383. }
  384. />
  385. )
  386. break
  387. default:
  388. }
  389. return <Body>{body}</Body>
  390. }
  391. renderNavigationActions() {
  392. let sourceEndpoint = this.props.wizardData.source && this.props.wizardData.source.type
  393. let targetEndpoint = this.props.wizardData.target && this.props.wizardData.target.type
  394. let currentPageIndex = wizardPages.findIndex(p => p.id === this.props.page.id)
  395. let isLastPage = currentPageIndex === wizardPages.length - 1
  396. return (
  397. <Navigation>
  398. <Button secondary onClick={this.props.onBackClick}>Back</Button>
  399. <IconRepresentation>
  400. <EndpointLogos height={32} endpoint={sourceEndpoint || ''} />
  401. <WizardTypeIcon
  402. dangerouslySetInnerHTML={{
  403. __html: this.props.type === 'replica'
  404. ? migrationArrowImage(Palette.alert) : migrationArrowImage(Palette.primary),
  405. }}
  406. />
  407. <EndpointLogos height={32} endpoint={targetEndpoint} />
  408. </IconRepresentation>
  409. <Button
  410. onClick={this.props.onNextClick}
  411. disabled={this.isNextButtonDisabled()}
  412. >{isLastPage ? 'Finish' : 'Next'}</Button>
  413. </Navigation>
  414. )
  415. }
  416. render() {
  417. if (!this.props.page) {
  418. return null
  419. }
  420. return (
  421. <Wrapper>
  422. {this.renderHeader()}
  423. {this.renderBody()}
  424. <Footer>
  425. {this.renderNavigationActions()}
  426. <WizardBreadcrumbs
  427. selected={this.props.page}
  428. pages={this.props.pages}
  429. />
  430. </Footer>
  431. </Wrapper>
  432. )
  433. }
  434. }
  435. export default WizardPageContent