WizardPageContent.tsx 22 KB

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