WizardPageContent.jsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  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. constructor() {
  123. super()
  124. this.state = {
  125. useAdvancedOptions: false,
  126. timezone: 'local',
  127. }
  128. }
  129. componentDidMount() {
  130. this.props.onContentRef(this)
  131. }
  132. componentWillUnmount() {
  133. this.props.onContentRef(null)
  134. }
  135. getProvidersType(type: string) {
  136. if (this.props.type === 'replica') {
  137. if (type === 'source') {
  138. return providerTypes.SOURCE_REPLICA
  139. }
  140. return providerTypes.TARGET_REPLICA
  141. }
  142. if (type === 'source') {
  143. return providerTypes.SOURCE_MIGRATION
  144. }
  145. return providerTypes.TARGET_MIGRATION
  146. }
  147. getProviders(type: string) {
  148. let providers = []
  149. let providerType = this.getProvidersType(type)
  150. let providersObject = this.props.providerStore.providers
  151. if (!providersObject) {
  152. return []
  153. }
  154. Object.keys(providersObject).forEach(provider => {
  155. if (providersObject[provider].types.findIndex(t => t === providerType) > -1) {
  156. providers.push(provider)
  157. }
  158. })
  159. return providers
  160. }
  161. isNetworksPageValid() {
  162. if (this.props.networkStore.loading || this.props.instanceStore.loadingInstancesDetails) {
  163. return false
  164. }
  165. let instances = this.props.instanceStore.instancesDetails
  166. if (instances.length === 0) {
  167. return true
  168. }
  169. if (instances.find(i => i.devices)) {
  170. if (instances.find(i => i.devices.nics && i.devices.nics.length > 0)) {
  171. return this.props.wizardData.networks && this.props.wizardData.networks.length > 0
  172. }
  173. return true
  174. }
  175. return false
  176. }
  177. isOptionsPageValid() {
  178. const isValid = (field: Field): boolean => {
  179. if (this.props.wizardData.options) {
  180. let fieldValue = this.props.wizardData.options[field.name]
  181. if (fieldValue === null) {
  182. return false
  183. }
  184. if (fieldValue === undefined) {
  185. return field.default !== null && field.default !== undefined
  186. }
  187. return Boolean(fieldValue)
  188. }
  189. return field.default !== null && field.default !== undefined
  190. }
  191. let schema = this.props.providerStore.optionsSchema
  192. if (schema && schema.length > 0) {
  193. let required = schema.filter(f => f.required && f.type !== 'object')
  194. schema.forEach(f => {
  195. if (f.type === 'object' && f.properties && f.properties.length && f.properties.filter(p => isValid(p)).length > 0) {
  196. required = required.concat(f.properties.filter(p => p.required))
  197. }
  198. })
  199. let validFieldsCount = 0
  200. required.forEach(f => {
  201. if (isValid(f)) {
  202. validFieldsCount += 1
  203. }
  204. })
  205. if (validFieldsCount === required.length) {
  206. return true
  207. }
  208. }
  209. return false
  210. }
  211. isNextButtonDisabled() {
  212. if (this.props.nextButtonDisabled) {
  213. return true
  214. }
  215. switch (this.props.page.id) {
  216. case 'source':
  217. return !this.props.wizardData.source
  218. case 'target':
  219. return !this.props.wizardData.target
  220. case 'vms':
  221. return !this.props.wizardData.selectedInstances || !this.props.wizardData.selectedInstances.length
  222. case 'options':
  223. return !this.isOptionsPageValid()
  224. case 'networks':
  225. return !this.isNetworksPageValid()
  226. default:
  227. return false
  228. }
  229. }
  230. handleAdvancedOptionsToggle(useAdvancedOptions: boolean) {
  231. this.setState({ useAdvancedOptions })
  232. }
  233. handleTimezoneChange(timezone: TimezoneValue) {
  234. this.setState({ timezone })
  235. }
  236. renderHeader() {
  237. let title = this.props.page.title
  238. if (this.props.page.id === 'type') {
  239. title += ` ${this.props.type.charAt(0).toUpperCase() + this.props.type.substr(1)}`
  240. }
  241. return <Header data-test-id={`${testName}-header`}>{title}</Header>
  242. }
  243. renderBody() {
  244. let body = null
  245. switch (this.props.page.id) {
  246. case 'type':
  247. body = (
  248. <WizardType
  249. selected={this.props.type}
  250. onChange={this.props.onTypeChange}
  251. />
  252. )
  253. break
  254. case 'source':
  255. body = (
  256. <WizardEndpointList
  257. providers={this.getProviders('source')}
  258. loading={this.props.providerStore.providersLoading}
  259. otherEndpoint={this.props.wizardData.target}
  260. selectedEndpoint={this.props.wizardData.source}
  261. endpoints={this.props.endpoints}
  262. onChange={this.props.onSourceEndpointChange}
  263. onAddEndpoint={type => { this.props.onAddEndpoint(type, true) }}
  264. />
  265. )
  266. break
  267. case 'target':
  268. body = (
  269. <WizardEndpointList
  270. providers={this.getProviders('target')}
  271. loading={this.props.providerStore.providersLoading}
  272. otherEndpoint={this.props.wizardData.source}
  273. selectedEndpoint={this.props.wizardData.target}
  274. endpoints={this.props.endpoints}
  275. onChange={this.props.onTargetEndpointChange}
  276. onAddEndpoint={type => { this.props.onAddEndpoint(type, false) }}
  277. />
  278. )
  279. break
  280. case 'vms':
  281. body = (
  282. <WizardInstances
  283. instances={this.props.instanceStore.instances}
  284. loading={this.props.instanceStore.instancesLoading}
  285. searching={this.props.instanceStore.searching}
  286. searchNotFound={this.props.instanceStore.searchNotFound}
  287. reloading={this.props.instanceStore.reloading}
  288. onSearchInputChange={this.props.onInstancesSearchInputChange}
  289. onNextPageClick={this.props.onInstancesNextPageClick}
  290. onPreviousPageClick={this.props.onInstancesPreviousPageClick}
  291. hasNextPage={this.props.instanceStore.hasNextPage}
  292. currentPage={this.props.instanceStore.currentPage}
  293. loadingPage={this.props.instanceStore.loadingPage}
  294. onReloadClick={this.props.onInstancesReloadClick}
  295. onInstanceClick={this.props.onInstanceClick}
  296. selectedInstances={this.props.wizardData.selectedInstances}
  297. />
  298. )
  299. break
  300. case 'options':
  301. body = (
  302. <WizardOptions
  303. loading={this.props.providerStore.optionsSchemaLoading || this.props.providerStore.destinationOptionsLoading}
  304. selectedInstances={this.props.wizardData.selectedInstances}
  305. fields={this.props.providerStore.optionsSchema}
  306. onChange={this.props.onOptionsChange}
  307. data={this.props.wizardData.options}
  308. useAdvancedOptions={this.state.useAdvancedOptions}
  309. wizardType={this.props.type}
  310. onAdvancedOptionsToggle={useAdvancedOptions => { this.handleAdvancedOptionsToggle(useAdvancedOptions) }}
  311. />
  312. )
  313. break
  314. case 'networks':
  315. body = (
  316. <WizardNetworks
  317. networks={this.props.networkStore.networks}
  318. selectedNetworks={this.props.wizardData.networks}
  319. loading={this.props.networkStore.loading}
  320. instancesDetails={this.props.instanceStore.instancesDetails}
  321. loadingInstancesDetails={this.props.instanceStore.loadingInstancesDetails}
  322. onChange={this.props.onNetworkChange}
  323. />
  324. )
  325. break
  326. case 'schedule':
  327. body = (
  328. <Schedule
  329. schedules={this.props.wizardData.schedules}
  330. onAddScheduleClick={this.props.onAddScheduleClick}
  331. onChange={this.props.onScheduleChange}
  332. onRemove={this.props.onScheduleRemove}
  333. timezone={this.state.timezone}
  334. onTimezoneChange={timezone => { this.handleTimezoneChange(timezone) }}
  335. secondaryEmpty
  336. />
  337. )
  338. break
  339. case 'summary':
  340. body = (
  341. <WizardSummary
  342. data={this.props.wizardData}
  343. wizardType={this.props.type}
  344. />
  345. )
  346. break
  347. default:
  348. }
  349. return <Body>{body}</Body>
  350. }
  351. renderNavigationActions() {
  352. let sourceEndpoint = this.props.wizardData.source && this.props.wizardData.source.type
  353. let targetEndpoint = this.props.wizardData.target && this.props.wizardData.target.type
  354. let currentPageIndex = wizardConfig.pages.findIndex(p => p.id === this.props.page.id)
  355. let isLastPage = currentPageIndex === wizardConfig.pages.length - 1
  356. return (
  357. <Navigation>
  358. <Button secondary onClick={this.props.onBackClick}>Back</Button>
  359. <IconRepresentation>
  360. <EndpointLogos height={32} endpoint={sourceEndpoint || ''} />
  361. <WizardTypeIcon
  362. dangerouslySetInnerHTML={{
  363. __html: this.props.type === 'replica'
  364. ? migrationArrowImage(Palette.alert) : migrationArrowImage(Palette.primary),
  365. }}
  366. />
  367. <EndpointLogos height={32} endpoint={targetEndpoint} />
  368. </IconRepresentation>
  369. <Button
  370. onClick={this.props.onNextClick}
  371. disabled={this.isNextButtonDisabled()}
  372. >{isLastPage ? 'Finish' : 'Next'}</Button>
  373. </Navigation>
  374. )
  375. }
  376. render() {
  377. if (!this.props.page) {
  378. return null
  379. }
  380. return (
  381. <Wrapper>
  382. {this.renderHeader()}
  383. {this.renderBody()}
  384. <Footer>
  385. {this.renderNavigationActions()}
  386. <WizardBreadcrumbs selected={this.props.page} wizardType={this.props.type} />
  387. </Footer>
  388. </Wrapper>
  389. )
  390. }
  391. }
  392. export default WizardPageContent