WizardPageContent.jsx 13 KB

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