| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634 |
- /*
- Copyright (C) 2017 Cloudbase Solutions SRL
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as
- published by the Free Software Foundation, either version 3 of the
- License, or (at your option) any later version.
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
- import React from 'react'
- import styled from 'styled-components'
- import { observer } from 'mobx-react'
- import EndpointLogos from '@src/components/modules/EndpointModule/EndpointLogos'
- import WizardType from '@src/components/modules/WizardModule/WizardType'
- import Button from '@src/components/ui/Button'
- import InfoIcon from '@src/components/ui/InfoIcon'
- import WizardBreadcrumbs from '@src/components/modules/WizardModule/WizardBreadcrumbs'
- import WizardEndpointList from '@src/components/modules/WizardModule/WizardEndpointList'
- import WizardInstances from '@src/components/modules/WizardModule/WizardInstances'
- import WizardNetworks, { WizardNetworksChangeObject } from '@src/components/modules/WizardModule/WizardNetworks'
- import WizardStorage from '@src/components/modules/WizardModule/WizardStorage'
- import WizardOptions from '@src/components/modules/WizardModule/WizardOptions'
- import WizardScripts from '@src/components/modules/WizardModule/WizardScripts'
- import Schedule from '@src/components/modules/TransferModule/Schedule'
- import WizardSummary from '@src/components/modules/WizardModule/WizardSummary'
- import { ThemePalette, ThemeProps } from '@src/components/Theme'
- import { providerTypes, wizardPages, migrationFields } from '@src/constants'
- import configLoader from '@src/utils/Config'
- import type { WizardData, WizardPage } from '@src/@types/WizardData'
- import { Endpoint, EndpointUtils, StorageMap } from '@src/@types/Endpoint'
- import type {
- Instance, InstanceScript,
- } from '@src/@types/Instance'
- import type { Field } from '@src/@types/Field'
- import type { Schedule as ScheduleType } from '@src/@types/Schedule'
- import instanceStore from '@src/stores/InstanceStore'
- import providerStore from '@src/stores/ProviderStore'
- import endpointStore from '@src/stores/EndpointStore'
- import networkStore from '@src/stores/NetworkStore'
- import { ProviderTypes } from '@src/@types/Providers'
- import minionPoolStore from '@src/stores/MinionPoolStore'
- import LoadingButton from '@src/components/ui/LoadingButton'
- import transferItemIcon from './images/transferItemIcon'
- const Wrapper = styled.div<any>`
- ${ThemeProps.exactWidth(`${parseInt(ThemeProps.contentWidth, 10) + 64}px`)}
- margin: 64px auto 32px auto;
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- display: flex;
- flex-direction: column;
- `
- const Header = styled.div<any>`
- display: flex;
- position: relative;
- margin-bottom: 32px;
- align-items: center;
- `
- const HeaderLabel = styled.div<any>`
- text-align: center;
- font-size: 32px;
- font-weight: ${ThemeProps.fontWeights.light};
- color: ${ThemePalette.primary};
- width: 100%;
- `
- const HeaderReload = styled.div<any>`
- display: flex;
- align-items: center;
- position: absolute;
- right: 0;
- `
- const HeaderReloadLabel = styled.div<any>`
- font-size: 10px;
- color: ${ThemePalette.grayscale[4]};
- &:hover {
- color: ${ThemePalette.primary};
- }
- cursor: pointer;
- `
- const Body = styled.div<any>`
- flex-grow: 1;
- overflow: auto;
- display: flex;
- justify-content: center;
- padding: 0 32px;
- `
- const Navigation = styled.div<any>`
- display: flex;
- justify-content: space-between;
- padding: 16px 32px 0 32px;
- margin-bottom: 80px;
- `
- const IconRepresentation = styled.div<any>`
- display: flex;
- justify-content: center;
- flex-grow: 1;
- margin: 0 76px;
- `
- const Footer = styled.div<any>``
- const WizardTypeIcon = styled.div<any>`
- width: 60px;
- height: 32px;
- display: flex;
- justify-content: center;
- align-items: center;
- margin: 0 32px;
- `
- export const isOptionsPageValid = (data: any, schema: Field[]) => {
- const isValid = (field: Field): boolean => {
- if (data) {
- const fieldValue = data[field.name]
- if (fieldValue === null) {
- return false
- }
- if (fieldValue === undefined) {
- return field.default != null
- }
- return Boolean(fieldValue)
- }
- return field.default != null
- }
- if (!schema || schema.length === 0) {
- return true
- }
- let required = schema.filter(f => f.required && f.type !== 'object')
- schema.forEach(f => {
- if (f.type === 'object' && f.properties && f.properties.filter && f.properties.filter(p => isValid(p)).length > 0) {
- required = required.concat(f.properties.filter(p => p.required))
- }
- if (f.enum && f.subFields) {
- const value = data && data[f.name]
- const subField = f.subFields.find(sf => sf.name === `${String(value)}_options`)
- if (subField && subField.properties) {
- required = required.concat(subField.properties.filter(p => p.required))
- }
- }
- })
- let validFieldsCount = 0
- required.forEach(f => {
- if (isValid(f)) {
- validFieldsCount += 1
- }
- })
- if (validFieldsCount === required.length) {
- return true
- }
- return false
- }
- type Props = {
- page: { id: string, title: string },
- type: 'replica' | 'migration',
- nextButtonDisabled: boolean,
- providerStore: typeof providerStore,
- instanceStore: typeof instanceStore,
- networkStore: typeof networkStore,
- endpointStore: typeof endpointStore,
- minionPoolStore: typeof minionPoolStore,
- wizardData: WizardData,
- schedules: ScheduleType[],
- storageMap: StorageMap[],
- onStorageReloadClick: () => void,
- defaultStorage: { value: string | null, busType?: string | null } | undefined,
- hasStorageMap: boolean,
- hasSourceOptions: boolean,
- pages: WizardPage[],
- uploadedUserScripts: InstanceScript[],
- showLoadingButton: boolean,
- onTypeChange: (isReplicaChecked: boolean | null) => void,
- onBackClick: () => void,
- onNextClick: () => void,
- onSourceEndpointChange: (endpoint: Endpoint) => void,
- onTargetEndpointChange: (endpoint: Endpoint) => void,
- onAddEndpoint: (provider: ProviderTypes, fromSource: boolean) => void,
- onInstancesSearchInputChange: (searchText: string) => void,
- onInstancesReloadClick: () => void,
- onInstanceClick: (instance: Instance) => void,
- onInstancePageClick: (page: number) => void,
- onDestOptionsChange: (field: Field, value: any, parentFieldName?: string) => void,
- onSourceOptionsChange: (field: Field, value: any, parentFieldName?: string) => void,
- onNetworkChange: (changeObject: WizardNetworksChangeObject) => void,
- onStorageChange: (mapping: StorageMap) => void,
- onDefaultStorageChange: (value: string | null, busType?: string | null) => void,
- onAddScheduleClick: (schedule: ScheduleType) => void,
- onScheduleChange: (scheduleId: string, schedule: ScheduleType) => void,
- onScheduleRemove: (scheudleId: string) => void,
- onContentRef: (ref: any) => void,
- onReloadOptionsClick: () => void,
- onReloadNetworksClick: () => void,
- onUserScriptUpload: (instanceScript: InstanceScript) => void,
- onCancelUploadedScript: (global: string | null, instanceName: string | null) => void,
- }
- type TimezoneValue = 'local' | 'utc'
- type State = {
- useAdvancedOptions: boolean,
- timezone: TimezoneValue,
- }
- @observer
- class WizardPageContent extends React.Component<Props, State> {
- state: State = {
- useAdvancedOptions: false,
- timezone: 'local',
- }
- componentDidMount() {
- this.props.onContentRef(this)
- }
- componentWillUnmount() {
- this.props.onContentRef(null)
- }
- getProvidersType(type: string) {
- return type === 'source' ? providerTypes.SOURCE_REPLICA : providerTypes.TARGET_REPLICA
- }
- getProviders(direction: string): ProviderTypes[] {
- const validProviders: {
- [provider in ProviderTypes]: true
- } = {} as { [provider in ProviderTypes]: true }
- const providerType = this.getProvidersType(direction)
- const providersObject = this.props.providerStore.providers
- if (!providersObject) {
- return []
- }
- Object.keys(providersObject).forEach(provider => {
- const usableProvider = provider as ProviderTypes
- if (providersObject[usableProvider].types.findIndex(t => t === providerType) > -1) {
- validProviders[usableProvider] = true
- }
- })
- return this.props.providerStore.providerNames.filter(p => validProviders[p])
- }
- areOptionsLoading(type: 'source' | 'destination'): boolean {
- if (type === 'source') {
- return this.props.providerStore.sourceSchemaLoading
- || this.props.providerStore.sourceOptionsPrimaryLoading
- || this.props.minionPoolStore.loadingMinionPools
- }
- return this.props.providerStore.destinationSchemaLoading
- || this.props.providerStore.destinationOptionsPrimaryLoading
- || this.props.minionPoolStore.loadingMinionPools
- }
- isNetworksPageValid() {
- if (this.props.networkStore.loading || this.props.instanceStore.loadingInstancesDetails) {
- return false
- }
- const instances = this.props.instanceStore.instancesDetails
- if (instances.length === 0) {
- return true
- }
- if (instances.find(i => i.devices)) {
- if (instances.find(i => i.devices.nics && i.devices.nics.length > 0)) {
- return this.props.wizardData.networks && this.props.wizardData.networks.length > 0
- }
- return true
- }
- return false
- }
- isNextButtonDisabled() {
- if (this.props.nextButtonDisabled) {
- return true
- }
- switch (this.props.page.id) {
- case 'source':
- return !this.props.wizardData.source
- case 'target':
- return !this.props.wizardData.target
- case 'vms':
- return !this.props.wizardData.selectedInstances
- || !this.props.wizardData.selectedInstances.length
- case 'source-options':
- return !isOptionsPageValid(
- this.props.wizardData.sourceOptions,
- this.props.providerStore.sourceSchema,
- )
- case 'dest-options':
- return !isOptionsPageValid(
- this.props.wizardData.destOptions,
- this.props.providerStore.destinationSchema,
- )
- case 'networks':
- return !this.isNetworksPageValid()
- default:
- return false
- }
- }
- handleAdvancedOptionsToggle(useAdvancedOptions: boolean) {
- this.setState({ useAdvancedOptions })
- }
- handleTimezoneChange(timezone: TimezoneValue) {
- this.setState({ timezone })
- }
- renderHeader() {
- let title = this.props.page.title
- const pageId = this.props.page.id
- if (pageId === 'type') {
- title += ` ${this.props.type.charAt(0).toUpperCase() + this.props.type.substr(1)}`
- }
- const optionsReload = (type: 'source' | 'destination') => ({
- label: 'Reload Options',
- action: () => { this.props.onReloadOptionsClick() },
- tip: 'Options may be cached by the UI. Here you can reload them from the API.',
- visible: !this.areOptionsLoading(type),
- })
- const reloadPages: any = {
- 'source-options': optionsReload('source'),
- 'dest-options': optionsReload('destination'),
- networks: {
- label: 'Reload Networks',
- action: () => { this.props.onReloadNetworksClick() },
- tip: 'Networks and instances info may be cached by the UI. Here you can reload them from the API.',
- visible: !this.props.instanceStore.loadingInstancesDetails,
- },
- }
- return (
- <Header>
- <HeaderLabel>{title}</HeaderLabel>
- {reloadPages[pageId]?.visible ? (
- <HeaderReload>
- <HeaderReloadLabel onClick={() => { reloadPages[pageId].action() }}>
- {reloadPages[pageId].label}
- </HeaderReloadLabel>
- <InfoIcon
- text={reloadPages[pageId].tip}
- marginBottom={0}
- marginLeft={8}
- filled
- />
- </HeaderReload>
- ) : null}
- </Header>
- )
- }
- renderBody() {
- let body = null
- const getOptionsLoadingSkipFields = (type: 'source' | 'destination') => {
- const extraOptionsConfig = configLoader.config.extraOptionsApiCalls.find(o => {
- const provider = type === 'source' ? this.props.wizardData.source && this.props.wizardData.source.type
- : this.props.wizardData.target && this.props.wizardData.target.type
- return o.name === provider && o.types.find(t => t === type)
- })
- let optionsLoadingRequiredFields: string[] = []
- if (extraOptionsConfig) {
- optionsLoadingRequiredFields = extraOptionsConfig.requiredFields
- }
- return optionsLoadingRequiredFields
- }
- const getDefaultStorage = (): { value: string | null, busType?: string | null } => {
- if (this.props.defaultStorage) {
- return this.props.defaultStorage
- }
- if (endpointStore.storageConfigDefault) {
- const busTypeInfo = EndpointUtils.getBusTypeStorageId(endpointStore.storageBackends, endpointStore.storageConfigDefault || null)
- const defaultStorage: { value: string | null, busType?: string | null } = {
- value: busTypeInfo.id,
- }
- if (busTypeInfo.busType) {
- defaultStorage.busType = busTypeInfo.busType
- }
- return defaultStorage
- }
- return { value: null }
- }
- switch (this.props.page.id) {
- case 'type':
- body = (
- <WizardType
- selected={this.props.type}
- onChange={this.props.onTypeChange}
- />
- )
- break
- case 'source':
- body = (
- <WizardEndpointList
- providers={this.getProviders('source')}
- loading={this.props.providerStore.providersLoading}
- otherEndpoint={this.props.wizardData.target}
- selectedEndpoint={this.props.wizardData.source}
- endpoints={this.props.endpointStore.endpoints}
- onChange={this.props.onSourceEndpointChange}
- onAddEndpoint={type => { this.props.onAddEndpoint(type, true) }}
- />
- )
- break
- case 'target':
- body = (
- <WizardEndpointList
- providers={this.getProviders('target')}
- loading={this.props.providerStore.providersLoading}
- otherEndpoint={this.props.wizardData.source}
- selectedEndpoint={this.props.wizardData.target}
- endpoints={this.props.endpointStore.endpoints}
- onChange={this.props.onTargetEndpointChange}
- onAddEndpoint={type => { this.props.onAddEndpoint(type, false) }}
- />
- )
- break
- case 'vms':
- body = (
- <WizardInstances
- instances={this.props.instanceStore.instances}
- instancesPerPage={this.props.instanceStore.instancesPerPage}
- chunksLoading={this.props.instanceStore.chunksLoading}
- currentPage={this.props.instanceStore.currentPage}
- searchText={this.props.instanceStore.searchText}
- loading={this.props.instanceStore.instancesLoading}
- searching={this.props.instanceStore.searching}
- searchNotFound={this.props.instanceStore.searchNotFound}
- reloading={this.props.instanceStore.reloading}
- onSearchInputChange={this.props.onInstancesSearchInputChange}
- onReloadClick={this.props.onInstancesReloadClick}
- onInstanceClick={this.props.onInstanceClick}
- onPageClick={this.props.onInstancePageClick}
- selectedInstances={this.props.wizardData.selectedInstances || []}
- hasSourceOptions={this.props.hasSourceOptions}
- />
- )
- break
- case 'source-options':
- body = (
- <WizardOptions
- loading={this.areOptionsLoading('source')}
- minionPools={this.props.minionPoolStore.minionPools
- .filter(m => m.platform === 'source' && m.endpoint_id === this.props.wizardData.source?.id)}
- optionsLoading={this.props.providerStore.sourceOptionsSecondaryLoading}
- optionsLoadingSkipFields={getOptionsLoadingSkipFields('source')}
- fields={this.props.providerStore.sourceSchema}
- onChange={this.props.onSourceOptionsChange}
- data={this.props.wizardData.sourceOptions}
- useAdvancedOptions
- hasStorageMap={false}
- wizardType={`${this.props.type}-source-options`}
- layout="page"
- isSource
- dictionaryKey={`${this.props.wizardData.source ? this.props.wizardData.source.type : ''}-source`}
- />
- )
- break
- case 'dest-options':
- body = (
- <WizardOptions
- loading={this.areOptionsLoading('destination')}
- minionPools={this.props.minionPoolStore.minionPools
- .filter(m => m.platform === 'destination' && m.endpoint_id === this.props.wizardData.target?.id)}
- optionsLoading={this.props.providerStore.destinationOptionsSecondaryLoading}
- optionsLoadingSkipFields={[
- ...getOptionsLoadingSkipFields('destination'), 'title', 'execute_now',
- 'execute_now_options', ...migrationFields.map(f => f.name)]}
- selectedInstances={this.props.wizardData.selectedInstances}
- showSeparatePerVm={
- Boolean(this.props.wizardData.selectedInstances
- && this.props.wizardData.selectedInstances.length > 1)
- }
- fields={this.props.providerStore.destinationSchema}
- onChange={this.props.onDestOptionsChange}
- data={this.props.wizardData.destOptions}
- useAdvancedOptions={this.state.useAdvancedOptions}
- hasStorageMap={this.props.hasStorageMap}
- storageBackends={this.props.endpointStore.storageBackends}
- wizardType={this.props.type}
- onAdvancedOptionsToggle={useAdvancedOptions => {
- this.handleAdvancedOptionsToggle(useAdvancedOptions)
- }}
- layout="page"
- dictionaryKey={`${this.props.wizardData.target ? this.props.wizardData.target.type : ''}-destination`}
- />
- )
- break
- case 'networks':
- body = (
- <WizardNetworks
- networks={this.props.networkStore.networks}
- selectedNetworks={this.props.wizardData.networks}
- loading={this.props.networkStore.loading}
- instancesDetails={this.props.instanceStore.instancesDetails}
- loadingInstancesDetails={this.props.instanceStore.loadingInstancesDetails}
- onChange={this.props.onNetworkChange}
- />
- )
- break
- case 'storage':
- body = (
- <WizardStorage
- loading={endpointStore.storageLoading}
- onReloadClick={this.props.onStorageReloadClick}
- storageBackends={this.props.endpointStore.storageBackends}
- instancesDetails={this.props.instanceStore.instancesDetails}
- storageMap={this.props.storageMap}
- onChange={this.props.onStorageChange}
- defaultStorage={getDefaultStorage()}
- onDefaultStorageChange={this.props.onDefaultStorageChange}
- defaultStorageLayout="page"
- />
- )
- break
- case 'scripts':
- body = (
- <WizardScripts
- instances={this.props.instanceStore.instancesDetails}
- onScriptUpload={this.props.onUserScriptUpload}
- onCancelScript={this.props.onCancelUploadedScript}
- uploadedScripts={this.props.uploadedUserScripts}
- userScriptData={null}
- removedScripts={[]}
- onScriptDataRemove={() => {}}
- />
- )
- break
- case 'schedule':
- body = (
- <Schedule
- schedules={this.props.schedules}
- onAddScheduleClick={this.props.onAddScheduleClick}
- onChange={this.props.onScheduleChange}
- onRemove={this.props.onScheduleRemove}
- timezone={this.state.timezone}
- onTimezoneChange={timezone => { this.handleTimezoneChange(timezone) }}
- secondaryEmpty
- />
- )
- break
- case 'summary':
- body = (
- <WizardSummary
- data={this.props.wizardData}
- schedules={this.props.schedules}
- defaultStorage={this.props.defaultStorage}
- storageMap={this.props.storageMap}
- wizardType={this.props.type}
- instancesDetails={this.props.instanceStore.instancesDetails}
- sourceSchema={this.props.providerStore.sourceSchema}
- destinationSchema={this.props.providerStore.destinationSchema}
- uploadedUserScripts={this.props.uploadedUserScripts}
- minionPools={this.props.minionPoolStore.minionPools}
- />
- )
- break
- default:
- }
- return <Body>{body}</Body>
- }
- renderNavigationActions() {
- const sourceEndpoint = this.props.wizardData.source && this.props.wizardData.source.type
- const targetEndpoint = this.props.wizardData.target && this.props.wizardData.target.type
- const currentPageIndex = wizardPages.findIndex(p => p.id === this.props.page.id)
- const isLastPage = currentPageIndex === wizardPages.length - 1
- return (
- <Navigation>
- <Button secondary onClick={this.props.onBackClick}>Back</Button>
- <IconRepresentation>
- <EndpointLogos height={32} endpoint={(sourceEndpoint || '') as any} />
- <WizardTypeIcon
- dangerouslySetInnerHTML={{
- __html: this.props.type === 'replica'
- ? transferItemIcon(ThemePalette.alert) : transferItemIcon(ThemePalette.primary),
- }}
- />
- <EndpointLogos height={32} endpoint={targetEndpoint} />
- </IconRepresentation>
- {this.props.showLoadingButton ? (
- <LoadingButton>Loading ...</LoadingButton>
- ) : (
- <Button
- onClick={this.props.onNextClick}
- disabled={this.isNextButtonDisabled()}
- >{isLastPage ? 'Finish' : 'Next'}
- </Button>
- )}
- </Navigation>
- )
- }
- render() {
- if (!this.props.page) {
- return null
- }
- return (
- <Wrapper>
- {this.renderHeader()}
- {this.renderBody()}
- <Footer>
- {this.renderNavigationActions()}
- <WizardBreadcrumbs
- selected={this.props.page}
- pages={this.props.pages}
- />
- </Footer>
- </Wrapper>
- )
- }
- }
- export default WizardPageContent
|