WizardPageContent.jsx 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559
  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 InfoIcon from '../../atoms/InfoIcon'
  22. import WizardBreadcrumbs from '../../molecules/WizardBreadcrumbs'
  23. import WizardEndpointList from '../WizardEndpointList'
  24. import WizardInstances from '../WizardInstances'
  25. import WizardNetworks from '../WizardNetworks'
  26. import WizardStorage from '../WizardStorage'
  27. import WizardOptions from '../WizardOptions'
  28. import WizardScripts from '../WizardScripts'
  29. import Schedule from '../Schedule'
  30. import WizardSummary from '../WizardSummary'
  31. import StyleProps from '../../styleUtils/StyleProps'
  32. import Palette from '../../styleUtils/Palette'
  33. import { providerTypes, wizardPages, migrationFields } from '../../../constants'
  34. import configLoader from '../../../utils/Config'
  35. import type { WizardData, WizardPage } from '../../../types/WizardData'
  36. import type { Endpoint, StorageBackend, StorageMap } from '../../../types/Endpoint'
  37. import type { Instance, Nic, Disk, InstanceScript } from '../../../types/Instance'
  38. import type { Field } from '../../../types/Field'
  39. import type { Network, SecurityGroup } from '../../../types/Network'
  40. import type { Schedule as ScheduleType } from '../../../types/Schedule'
  41. import instanceStore from '../../../stores/InstanceStore'
  42. import providerStore from '../../../stores/ProviderStore'
  43. import endpointStore from '../../../stores/EndpointStore'
  44. import networkStore from '../../../stores/NetworkStore'
  45. import migrationArrowImage from './images/migration.js'
  46. const Wrapper = styled.div`
  47. ${StyleProps.exactWidth(`${parseInt(StyleProps.contentWidth, 10) + 64}px`)}
  48. margin: 64px auto 32px auto;
  49. position: absolute;
  50. top: 0;
  51. left: 0;
  52. right: 0;
  53. bottom: 0;
  54. display: flex;
  55. flex-direction: column;
  56. `
  57. const Header = styled.div`
  58. display: flex;
  59. position: relative;
  60. margin-bottom: 32px;
  61. align-items: center;
  62. `
  63. const HeaderLabel = styled.div`
  64. text-align: center;
  65. font-size: 32px;
  66. font-weight: ${StyleProps.fontWeights.light};
  67. color: ${Palette.primary};
  68. width: 100%;
  69. `
  70. const HeaderReload = styled.div`
  71. display: flex;
  72. align-items: center;
  73. position: absolute;
  74. right: 0;
  75. `
  76. const HeaderReloadLabel = styled.div`
  77. font-size: 10px;
  78. color: ${Palette.grayscale[4]};
  79. &:hover {
  80. color: ${Palette.primary};
  81. }
  82. cursor: pointer;
  83. `
  84. const Body = styled.div`
  85. flex-grow: 1;
  86. overflow: auto;
  87. display: flex;
  88. justify-content: center;
  89. padding: 0 32px;
  90. `
  91. const Navigation = styled.div`
  92. display: flex;
  93. justify-content: space-between;
  94. padding: 16px 32px 0 32px;
  95. margin-bottom: 80px;
  96. `
  97. const IconRepresentation = styled.div`
  98. display: flex;
  99. justify-content: center;
  100. flex-grow: 1;
  101. margin: 0 76px;
  102. `
  103. const Footer = styled.div``
  104. const WizardTypeIcon = styled.div`
  105. width: 60px;
  106. height: 32px;
  107. display: flex;
  108. justify-content: center;
  109. align-items: center;
  110. margin: 0 32px;
  111. `
  112. export const isOptionsPageValid = (data: ?any, schema: Field[]) => {
  113. const isValid = (field: Field): boolean => {
  114. if (data) {
  115. let fieldValue = data[field.name]
  116. if (fieldValue === null) {
  117. return false
  118. }
  119. if (fieldValue === undefined) {
  120. return field.default != null
  121. }
  122. return Boolean(fieldValue)
  123. }
  124. return field.default != null
  125. }
  126. if (schema && schema.length > 0) {
  127. let required = schema.filter(f => f.required && f.type !== 'object')
  128. schema.forEach(f => {
  129. if (f.type === 'object' && f.properties && f.properties.filter && f.properties.filter(p => isValid(p)).length > 0) {
  130. required = required.concat(f.properties.filter(p => p.required))
  131. }
  132. if (f.enum && f.subFields) {
  133. let value = data && data[f.name]
  134. let subField = f.subFields.find(f => f.name === `${String(value)}_options`)
  135. if (subField && subField.properties) {
  136. required = required.concat(subField.properties.filter(p => p.required))
  137. }
  138. }
  139. })
  140. let validFieldsCount = 0
  141. required.forEach(f => {
  142. if (isValid(f)) {
  143. validFieldsCount += 1
  144. }
  145. })
  146. if (validFieldsCount === required.length) {
  147. return true
  148. }
  149. }
  150. return false
  151. }
  152. type Props = {
  153. page: { id: string, title: string },
  154. type: 'replica' | 'migration',
  155. nextButtonDisabled: boolean,
  156. providerStore: typeof providerStore,
  157. instanceStore: typeof instanceStore,
  158. networkStore: typeof networkStore,
  159. endpointStore: typeof endpointStore,
  160. wizardData: WizardData,
  161. schedules: ScheduleType[],
  162. storageMap: StorageMap[],
  163. hasStorageMap: boolean,
  164. hasSourceOptions: boolean,
  165. pages: WizardPage[],
  166. uploadedUserScripts: InstanceScript[],
  167. onTypeChange: (isReplicaChecked: ?boolean) => void,
  168. onBackClick: () => void,
  169. onNextClick: () => void,
  170. onSourceEndpointChange: (endpoint: Endpoint) => void,
  171. onTargetEndpointChange: (endpoint: Endpoint) => void,
  172. onAddEndpoint: (provider: string, fromSource: boolean) => void,
  173. onInstancesSearchInputChange: (searchText: string) => void,
  174. onInstancesReloadClick: () => void,
  175. onInstanceClick: (instance: Instance) => void,
  176. onInstancePageClick: (page: number) => void,
  177. onDestOptionsChange: (field: Field, value: any) => void,
  178. onSourceOptionsChange: (field: Field, value: any) => void,
  179. onNetworkChange: (nic: Nic, network: Network, secGroups: ?SecurityGroup[]) => void,
  180. onStorageChange: (sourceStorage: Disk, targetStorage: StorageBackend, type: 'backend' | 'disk') => void,
  181. onAddScheduleClick: (schedule: ScheduleType) => void,
  182. onScheduleChange: (scheduleId: string, schedule: ScheduleType) => void,
  183. onScheduleRemove: (scheudleId: string) => void,
  184. onContentRef: (ref: any) => void,
  185. onReloadOptionsClick: () => void,
  186. onReloadNetworksClick: () => void,
  187. onUserScriptUpload: (instanceScript: InstanceScript) => void,
  188. onCancelUploadedScript: (global: ?string, instanceName: ?string) => void,
  189. }
  190. type TimezoneValue = 'local' | 'utc'
  191. type State = {
  192. useAdvancedOptions: boolean,
  193. timezone: TimezoneValue,
  194. }
  195. const testName = 'wpContent'
  196. @observer
  197. class WizardPageContent extends React.Component<Props, State> {
  198. state = {
  199. useAdvancedOptions: false,
  200. timezone: 'local',
  201. }
  202. componentDidMount() {
  203. this.props.onContentRef(this)
  204. }
  205. componentWillUnmount() {
  206. this.props.onContentRef(null)
  207. }
  208. getProvidersType(type: string) {
  209. return type === 'source' ? providerTypes.SOURCE_REPLICA : providerTypes.TARGET_REPLICA
  210. }
  211. getProviders(type: string): string[] {
  212. let validProviders = {}
  213. let providerType = this.getProvidersType(type)
  214. let providersObject = this.props.providerStore.providers
  215. if (!providersObject) {
  216. return []
  217. }
  218. Object.keys(providersObject).forEach(provider => {
  219. if (providersObject[provider].types.findIndex(t => t === providerType) > -1) {
  220. validProviders[provider] = true
  221. }
  222. })
  223. return this.props.providerStore.providerNames.filter(p => validProviders[p])
  224. }
  225. isNetworksPageValid() {
  226. if (this.props.networkStore.loading || this.props.instanceStore.loadingInstancesDetails) {
  227. return false
  228. }
  229. let instances = this.props.instanceStore.instancesDetails
  230. if (instances.length === 0) {
  231. return true
  232. }
  233. if (instances.find(i => i.devices)) {
  234. if (instances.find(i => i.devices.nics && i.devices.nics.length > 0)) {
  235. return this.props.wizardData.networks && this.props.wizardData.networks.length > 0
  236. }
  237. return true
  238. }
  239. return false
  240. }
  241. isNextButtonDisabled() {
  242. if (this.props.nextButtonDisabled) {
  243. return true
  244. }
  245. switch (this.props.page.id) {
  246. case 'source':
  247. return !this.props.wizardData.source
  248. case 'target':
  249. return !this.props.wizardData.target
  250. case 'vms':
  251. return !this.props.wizardData.selectedInstances || !this.props.wizardData.selectedInstances.length
  252. case 'source-options':
  253. return !isOptionsPageValid(this.props.wizardData.sourceOptions, this.props.providerStore.sourceSchema)
  254. case 'dest-options':
  255. return !isOptionsPageValid(this.props.wizardData.destOptions, this.props.providerStore.destinationSchema)
  256. case 'networks':
  257. return !this.isNetworksPageValid()
  258. default:
  259. return false
  260. }
  261. }
  262. handleAdvancedOptionsToggle(useAdvancedOptions: boolean) {
  263. this.setState({ useAdvancedOptions })
  264. }
  265. handleTimezoneChange(timezone: TimezoneValue) {
  266. this.setState({ timezone })
  267. }
  268. renderHeader() {
  269. let title = this.props.page.title
  270. let pageId = this.props.page.id
  271. if (pageId === 'type') {
  272. title += ` ${this.props.type.charAt(0).toUpperCase() + this.props.type.substr(1)}`
  273. }
  274. let optionsReload = {
  275. label: 'Reload Options',
  276. action: () => { this.props.onReloadOptionsClick() },
  277. tip: 'Options may be cached by the UI. Here you can reload them from the API.',
  278. }
  279. let reloadPages = {
  280. 'source-options': optionsReload,
  281. 'dest-options': optionsReload,
  282. networks: {
  283. label: 'Reload Networks',
  284. action: () => { this.props.onReloadNetworksClick() },
  285. tip: 'Networks and instances info may be cached by the UI. Here you can reload them from the API.',
  286. },
  287. }
  288. return (
  289. <Header>
  290. <HeaderLabel data-test-id={`${testName}-header`}>{title}</HeaderLabel>
  291. {reloadPages[pageId] ? (
  292. <HeaderReload>
  293. <HeaderReloadLabel onClick={() => { reloadPages[pageId].action() }}>
  294. {reloadPages[pageId].label}
  295. </HeaderReloadLabel>
  296. <InfoIcon
  297. text={reloadPages[pageId].tip}
  298. marginBottom={0}
  299. marginLeft={8}
  300. filled
  301. />
  302. </HeaderReload>
  303. ) : null}
  304. </Header>
  305. )
  306. }
  307. renderBody() {
  308. let body = null
  309. let getOptionsLoadingSkipFields = (type: 'source' | 'destination') => {
  310. let extraOptionsConfig = configLoader.config.extraOptionsApiCalls.find(o => {
  311. let provider = type === 'source' ? this.props.wizardData.source && this.props.wizardData.source.type
  312. : this.props.wizardData.target && this.props.wizardData.target.type
  313. return o.name === provider && o.types.find(t => t === type)
  314. })
  315. let optionsLoadingRequiredFields = []
  316. if (extraOptionsConfig) {
  317. optionsLoadingRequiredFields = extraOptionsConfig.requiredFields
  318. }
  319. return optionsLoadingRequiredFields
  320. }
  321. switch (this.props.page.id) {
  322. case 'type':
  323. body = (
  324. <WizardType
  325. selected={this.props.type}
  326. onChange={this.props.onTypeChange}
  327. />
  328. )
  329. break
  330. case 'source':
  331. body = (
  332. <WizardEndpointList
  333. providers={this.getProviders('source')}
  334. loading={this.props.providerStore.providersLoading}
  335. otherEndpoint={this.props.wizardData.target}
  336. selectedEndpoint={this.props.wizardData.source}
  337. endpoints={this.props.endpointStore.endpoints}
  338. onChange={this.props.onSourceEndpointChange}
  339. onAddEndpoint={type => { this.props.onAddEndpoint(type, true) }}
  340. />
  341. )
  342. break
  343. case 'target':
  344. body = (
  345. <WizardEndpointList
  346. providers={this.getProviders('target')}
  347. loading={this.props.providerStore.providersLoading}
  348. otherEndpoint={this.props.wizardData.source}
  349. selectedEndpoint={this.props.wizardData.target}
  350. endpoints={this.props.endpointStore.endpoints}
  351. onChange={this.props.onTargetEndpointChange}
  352. onAddEndpoint={type => { this.props.onAddEndpoint(type, false) }}
  353. />
  354. )
  355. break
  356. case 'vms':
  357. body = (
  358. <WizardInstances
  359. instances={this.props.instanceStore.instances}
  360. instancesPerPage={this.props.instanceStore.instancesPerPage}
  361. chunksLoading={this.props.instanceStore.chunksLoading}
  362. currentPage={this.props.instanceStore.currentPage}
  363. searchText={this.props.instanceStore.searchText}
  364. loading={this.props.instanceStore.instancesLoading}
  365. searching={this.props.instanceStore.searching}
  366. searchNotFound={this.props.instanceStore.searchNotFound}
  367. reloading={this.props.instanceStore.reloading}
  368. onSearchInputChange={this.props.onInstancesSearchInputChange}
  369. onReloadClick={this.props.onInstancesReloadClick}
  370. onInstanceClick={this.props.onInstanceClick}
  371. onPageClick={this.props.onInstancePageClick}
  372. selectedInstances={this.props.wizardData.selectedInstances}
  373. hasSourceOptions={this.props.hasSourceOptions}
  374. />
  375. )
  376. break
  377. case 'source-options':
  378. body = (
  379. <WizardOptions
  380. loading={this.props.providerStore.sourceSchemaLoading || this.props.providerStore.sourceOptionsPrimaryLoading}
  381. optionsLoading={this.props.providerStore.sourceOptionsSecondaryLoading}
  382. optionsLoadingSkipFields={getOptionsLoadingSkipFields('source')}
  383. fields={this.props.providerStore.sourceSchema}
  384. onChange={this.props.onSourceOptionsChange}
  385. data={this.props.wizardData.sourceOptions}
  386. useAdvancedOptions
  387. hasStorageMap={false}
  388. wizardType={`${this.props.type}-source-options`}
  389. layout="page"
  390. />
  391. )
  392. break
  393. case 'dest-options':
  394. body = (
  395. <WizardOptions
  396. loading={this.props.providerStore.destinationSchemaLoading || this.props.providerStore.destinationOptionsPrimaryLoading}
  397. optionsLoading={this.props.providerStore.destinationOptionsSecondaryLoading}
  398. optionsLoadingSkipFields={[
  399. ...getOptionsLoadingSkipFields('destination'), 'description', 'execute_now',
  400. 'execute_now_options', 'default_storage', ...migrationFields.map(f => f.name)]}
  401. selectedInstances={this.props.wizardData.selectedInstances}
  402. fields={this.props.providerStore.destinationSchema}
  403. onChange={this.props.onDestOptionsChange}
  404. data={this.props.wizardData.destOptions}
  405. useAdvancedOptions={this.state.useAdvancedOptions}
  406. hasStorageMap={this.props.hasStorageMap}
  407. storageBackends={this.props.endpointStore.storageBackends}
  408. storageConfigDefault={this.props.endpointStore.storageConfigDefault}
  409. wizardType={this.props.type}
  410. onAdvancedOptionsToggle={useAdvancedOptions => { this.handleAdvancedOptionsToggle(useAdvancedOptions) }}
  411. layout="page"
  412. />
  413. )
  414. break
  415. case 'networks':
  416. body = (
  417. <WizardNetworks
  418. networks={this.props.networkStore.networks}
  419. selectedNetworks={this.props.wizardData.networks}
  420. loading={this.props.networkStore.loading}
  421. instancesDetails={this.props.instanceStore.instancesDetails}
  422. loadingInstancesDetails={this.props.instanceStore.loadingInstancesDetails}
  423. onChange={this.props.onNetworkChange}
  424. />
  425. )
  426. break
  427. case 'storage':
  428. body = (
  429. <WizardStorage
  430. storageBackends={this.props.endpointStore.storageBackends}
  431. instancesDetails={this.props.instanceStore.instancesDetails}
  432. storageMap={this.props.storageMap}
  433. onChange={this.props.onStorageChange}
  434. />
  435. )
  436. break
  437. case 'scripts':
  438. body = (
  439. <WizardScripts
  440. instances={this.props.instanceStore.instancesDetails}
  441. onScriptUpload={this.props.onUserScriptUpload}
  442. onCancelScript={this.props.onCancelUploadedScript}
  443. uploadedScripts={this.props.uploadedUserScripts}
  444. />
  445. )
  446. break
  447. case 'schedule':
  448. body = (
  449. <Schedule
  450. schedules={this.props.schedules}
  451. onAddScheduleClick={this.props.onAddScheduleClick}
  452. onChange={this.props.onScheduleChange}
  453. onRemove={this.props.onScheduleRemove}
  454. timezone={this.state.timezone}
  455. onTimezoneChange={timezone => { this.handleTimezoneChange(timezone) }}
  456. secondaryEmpty
  457. />
  458. )
  459. break
  460. case 'summary':
  461. body = (
  462. <WizardSummary
  463. data={this.props.wizardData}
  464. schedules={this.props.schedules}
  465. storageMap={this.props.storageMap}
  466. wizardType={this.props.type}
  467. instancesDetails={this.props.instanceStore.instancesDetails}
  468. sourceSchema={this.props.providerStore.sourceSchema}
  469. destinationSchema={this.props.providerStore.destinationSchema}
  470. uploadedUserScripts={this.props.uploadedUserScripts}
  471. />
  472. )
  473. break
  474. default:
  475. }
  476. return <Body>{body}</Body>
  477. }
  478. renderNavigationActions() {
  479. let sourceEndpoint = this.props.wizardData.source && this.props.wizardData.source.type
  480. let targetEndpoint = this.props.wizardData.target && this.props.wizardData.target.type
  481. let currentPageIndex = wizardPages.findIndex(p => p.id === this.props.page.id)
  482. let isLastPage = currentPageIndex === wizardPages.length - 1
  483. return (
  484. <Navigation>
  485. <Button secondary onClick={this.props.onBackClick}>Back</Button>
  486. <IconRepresentation>
  487. <EndpointLogos height={32} endpoint={sourceEndpoint || ''} />
  488. <WizardTypeIcon
  489. dangerouslySetInnerHTML={{
  490. __html: this.props.type === 'replica'
  491. ? migrationArrowImage(Palette.alert) : migrationArrowImage(Palette.primary),
  492. }}
  493. />
  494. <EndpointLogos height={32} endpoint={targetEndpoint} />
  495. </IconRepresentation>
  496. <Button
  497. onClick={this.props.onNextClick}
  498. disabled={this.isNextButtonDisabled()}
  499. >{isLastPage ? 'Finish' : 'Next'}</Button>
  500. </Navigation>
  501. )
  502. }
  503. render() {
  504. if (!this.props.page) {
  505. return null
  506. }
  507. return (
  508. <Wrapper>
  509. {this.renderHeader()}
  510. {this.renderBody()}
  511. <Footer>
  512. {this.renderNavigationActions()}
  513. <WizardBreadcrumbs
  514. selected={this.props.page}
  515. pages={this.props.pages}
  516. />
  517. </Footer>
  518. </Wrapper>
  519. )
  520. }
  521. }
  522. export default WizardPageContent