WizardOptions.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 * as React from 'react'
  16. import styled from 'styled-components'
  17. import { observer } from 'mobx-react'
  18. import autobind from 'autobind-decorator'
  19. import configLoader from '../../../utils/Config'
  20. import StyleProps from '../../styleUtils/StyleProps'
  21. import ToggleButtonBar from '../../atoms/ToggleButtonBar'
  22. import FieldInput from '../../molecules/FieldInput'
  23. import StatusImage from '../../atoms/StatusImage'
  24. import type { Field } from '../../../types/Field'
  25. import type { Instance } from '../../../types/Instance'
  26. import type { StorageBackend } from '../../../types/Endpoint'
  27. import { executionOptions, migrationFields } from '../../../constants'
  28. import LabelDictionary from '../../../utils/LabelDictionary'
  29. import Palette from '../../styleUtils/Palette'
  30. import endpointImage from './images/endpoint.svg'
  31. const Wrapper = styled.div`
  32. display: flex;
  33. min-height: 0;
  34. flex-direction: column;
  35. width: 100%;
  36. `
  37. const Options = styled.div`
  38. display: flex;
  39. flex-direction: column;
  40. min-height: 0;
  41. `
  42. const Fields = styled.div`
  43. ${props => props.padding ? `padding: ${props.padding}px;` : ''}
  44. display: flex;
  45. flex-direction: column;
  46. overflow: auto;
  47. `
  48. const Group = styled.div`
  49. display: flex;
  50. flex-direction: column;
  51. flex-shrink: 0;
  52. `
  53. const GroupName = styled.div`
  54. display: flex;
  55. align-items: center;
  56. margin: 48px 0 24px 0;
  57. `
  58. const GroupNameText = styled.div`
  59. margin: 0 32px;
  60. font-size: 16px;
  61. `
  62. const GroupNameBar = styled.div`
  63. flex-grow: 1;
  64. background: ${Palette.grayscale[3]};
  65. height: 1px;
  66. `
  67. const GroupFields = styled.div`
  68. display: flex;
  69. justify-content: space-between;
  70. `
  71. const OneColumn = styled.div``
  72. const Column = styled.div`
  73. margin-top: -16px;
  74. `
  75. const FieldInputStyled = styled(FieldInput)`
  76. width: ${props => props.width || StyleProps.inputSizes.wizard.width}px;
  77. justify-content: space-between;
  78. margin-top: 16px;
  79. `
  80. const LoadingWrapper = styled.div`
  81. margin-top: 32px;
  82. display: flex;
  83. flex-direction: column;
  84. align-items: center;
  85. `
  86. const LoadingText = styled.div`
  87. margin-top: 38px;
  88. font-size: 18px;
  89. `
  90. const EndpointImage = styled.div`
  91. ${StyleProps.exactSize('96px')};
  92. background: url('${endpointImage}') center no-repeat;
  93. `
  94. const NoSourceFieldsWrapper = styled.div`
  95. margin-top: 16px;
  96. display: flex;
  97. flex-direction: column;
  98. align-items: center;
  99. `
  100. const NoSourceFieldsMessage = styled.div`
  101. font-size: 18px;
  102. margin-top: 16px;
  103. `
  104. const NoSourceFieldsSubMessage = styled.div`
  105. margin-top: 16px;
  106. color: ${Palette.grayscale[4]};
  107. `
  108. export const shouldRenderField = (field: Field) => {
  109. return (field.type !== 'array' || (field.enum && field.enum.length && field.enum.length > 0)) &&
  110. (field.type !== 'object' || field.properties)
  111. }
  112. type FieldRender = {
  113. field: Field,
  114. component: React.Node,
  115. column: number,
  116. }
  117. type Props = {
  118. fields: Field[],
  119. isSource?: boolean,
  120. selectedInstances?: ?Instance[],
  121. data?: ?{ [string]: mixed },
  122. getFieldValue?: (fieldName: string, defaultValue: any) => any,
  123. onChange: (field: Field, value: any) => void,
  124. useAdvancedOptions?: boolean,
  125. hasStorageMap: boolean,
  126. storageBackends?: StorageBackend[],
  127. storageConfigDefault?: string,
  128. onAdvancedOptionsToggle?: (showAdvanced: boolean) => void,
  129. wizardType: string,
  130. oneColumnStyle?: { [string]: mixed },
  131. fieldWidth?: number,
  132. onScrollableRef?: (ref: HTMLElement) => void,
  133. availableHeight?: number,
  134. layout?: 'page' | 'modal',
  135. loading?: boolean,
  136. optionsLoading?: boolean,
  137. optionsLoadingSkipFields?: string[],
  138. dictionaryKey: string,
  139. }
  140. @observer
  141. class WizardOptions extends React.Component<Props> {
  142. componentDidMount() {
  143. window.addEventListener('resize', this.handleResize)
  144. }
  145. componentWillUnmount() {
  146. window.removeEventListener('resize', this.handleResize, false)
  147. }
  148. getFieldValue(fieldName: string, defaultValue: any) {
  149. if (this.props.getFieldValue) {
  150. return this.props.getFieldValue(fieldName, defaultValue)
  151. }
  152. if (!this.props.data || this.props.data[fieldName] === undefined) {
  153. return defaultValue
  154. }
  155. return this.props.data[fieldName]
  156. }
  157. getDefaultFieldsSchema() {
  158. let fieldsSchema = []
  159. if (this.props.wizardType === 'migration' || this.props.wizardType === 'replica') {
  160. fieldsSchema.push({ name: 'description', type: 'string' })
  161. }
  162. if (this.props.wizardType === 'migration' || this.props.wizardType === 'migration-destination-options-edit') {
  163. fieldsSchema.unshift({ name: 'skip_os_morphing', type: 'boolean', default: false })
  164. }
  165. if (this.props.selectedInstances && this.props.selectedInstances.length > 1) {
  166. let dictionaryLabel = LabelDictionary.get('separate_vm')
  167. let label = this.props.wizardType === 'migration' ? dictionaryLabel : dictionaryLabel.replace('Migration', 'Replica')
  168. fieldsSchema.unshift({ name: 'separate_vm', label, type: 'boolean', default: true })
  169. }
  170. if (this.props.wizardType === 'replica') {
  171. fieldsSchema.push({ name: 'execute_now', type: 'boolean', default: true })
  172. let executeNowValue = this.getFieldValue('execute_now', true)
  173. if (executeNowValue) {
  174. fieldsSchema.push({
  175. name: 'execute_now_options',
  176. type: 'object',
  177. properties: executionOptions,
  178. })
  179. }
  180. } else if (this.props.wizardType === 'migration' || this.props.wizardType === 'migration-destination-options-edit') {
  181. fieldsSchema = [...fieldsSchema, ...migrationFields]
  182. }
  183. return fieldsSchema
  184. }
  185. isPassword(fieldName: string): boolean {
  186. return fieldName.indexOf('password') > -1 || Boolean(configLoader.config.passwordFields.find(f => f === fieldName))
  187. }
  188. @autobind
  189. handleResize() {
  190. this.setState({})
  191. }
  192. generateGroups(fields: FieldRender[]) {
  193. let groups: Array<{ fields: FieldRender[], name?: string }> = [{ fields }]
  194. let workerFields = fields.filter(f => f.field.name.indexOf('migr_') === 0)
  195. if (workerFields.length > 1) {
  196. groups = [
  197. { fields: fields.filter(f => f.field.name.indexOf('migr_') === -1) },
  198. { name: 'Temporary Migration Worker Options', fields: workerFields.map((f, i) => ({ ...f, column: i % 2 })) },
  199. ]
  200. }
  201. fields.forEach(f => {
  202. if (f.field.groupName) {
  203. groups[0].fields = groups[0].fields ? groups[0].fields.filter(gf => gf.field.name !== f.field.name) : []
  204. let group = groups.find(g => g.name && g.name === f.field.groupName)
  205. if (!group) {
  206. groups.push({
  207. name: f.field.groupName,
  208. fields: [f],
  209. })
  210. } else {
  211. group.fields.push(f)
  212. }
  213. }
  214. })
  215. return groups
  216. }
  217. renderOptionsField(field: Field) {
  218. let additionalProps
  219. if (field.type === 'object' && field.properties) {
  220. additionalProps = {
  221. valueCallback: f => this.getFieldValue(f.name, f.default),
  222. onChange: (value, f) => { this.props.onChange(f, value) },
  223. properties: field.properties,
  224. }
  225. } else {
  226. additionalProps = {
  227. value: this.getFieldValue(field.name, field.default),
  228. onChange: value => { this.props.onChange(field, value) },
  229. }
  230. }
  231. let optionsLoadingReqFields = this.props.optionsLoadingSkipFields || []
  232. return (
  233. <FieldInputStyled
  234. layout={this.props.layout || 'page'}
  235. key={field.name}
  236. name={field.name}
  237. type={field.type}
  238. minimum={field.minimum}
  239. maximum={field.maximum}
  240. label={field.label || LabelDictionary.get(field.name, this.props.dictionaryKey)}
  241. description={field.description || LabelDictionary.getDescription(field.name, this.props.dictionaryKey)}
  242. password={this.isPassword(field.name)}
  243. enum={field.enum}
  244. addNullValue
  245. required={field.required}
  246. data-test-id={`wOptions-field-${field.name}`}
  247. width={this.props.fieldWidth || StyleProps.inputSizes.wizard.width}
  248. nullableBoolean={field.nullableBoolean}
  249. disabledLoading={this.props.optionsLoading && !optionsLoadingReqFields.find(fn => fn === field.name)}
  250. {...additionalProps}
  251. />
  252. )
  253. }
  254. renderNoFieldsMessage() {
  255. return (
  256. <NoSourceFieldsWrapper>
  257. <EndpointImage />
  258. <NoSourceFieldsMessage>No Source Options</NoSourceFieldsMessage>
  259. <NoSourceFieldsSubMessage>There are no options for the specified source cloud provider.</NoSourceFieldsSubMessage>
  260. </NoSourceFieldsWrapper>
  261. )
  262. }
  263. renderOptionsFields() {
  264. if (this.props.fields.length === 0 && this.props.isSource) {
  265. return this.renderNoFieldsMessage()
  266. }
  267. let fieldsSchema: Field[] = this.getDefaultFieldsSchema()
  268. let nonNullableBooleans: string[] = fieldsSchema.filter(f => f.type === 'boolean').map(f => f.name)
  269. fieldsSchema = fieldsSchema.concat(this.props.fields.filter(f => f.required))
  270. if (this.props.useAdvancedOptions) {
  271. fieldsSchema = fieldsSchema.concat(this.props.fields.filter(f => !f.required))
  272. }
  273. // Add subfields for enums which have them
  274. let subFields = []
  275. fieldsSchema.forEach(f => {
  276. if (!f.enum || !f.subFields) {
  277. return
  278. }
  279. let value = this.getFieldValue(f.name, f.default)
  280. if (!f.subFields) {
  281. return
  282. }
  283. let subField = f.subFields.find(f => f.name === `${String(value)}_options`)
  284. if (subField && subField.properties) {
  285. subFields = [...subFields, ...subField.properties]
  286. }
  287. })
  288. fieldsSchema = [...fieldsSchema, ...subFields]
  289. let executeNowColumn
  290. let fields: FieldRender[] = fieldsSchema.filter(f => shouldRenderField(f)).map((field, i) => {
  291. let column: number = i % 2
  292. if (field.name === 'execute_now') {
  293. executeNowColumn = column
  294. }
  295. if (field.name === 'execute_now_options') {
  296. column = executeNowColumn
  297. }
  298. if (field.type === 'boolean' && !nonNullableBooleans.find(name => name === field.name)) {
  299. field.nullableBoolean = true
  300. }
  301. return {
  302. column,
  303. component: this.renderOptionsField(field),
  304. field,
  305. }
  306. })
  307. let availableHeight = this.props.availableHeight || (window.innerHeight - 450)
  308. if (fields.length * 96 < availableHeight) {
  309. return (
  310. <Fields padding={this.props.layout === 'page' ? null : 32}>
  311. <Group>
  312. <GroupFields style={{ justifyContent: 'center' }}>
  313. <OneColumn style={this.props.oneColumnStyle}>
  314. {fields.map(f => f.component)}
  315. </OneColumn>
  316. </GroupFields>
  317. </Group>
  318. </Fields>
  319. )
  320. }
  321. let groups = this.generateGroups(fields)
  322. return (
  323. <Fields innerRef={this.props.onScrollableRef} padding={this.props.layout === 'page' ? null : 32}>
  324. {groups.map(g => (
  325. <Group key={g.name || 0}>
  326. {g.name ? (
  327. <GroupName>
  328. <GroupNameBar />
  329. <GroupNameText>{g.name}</GroupNameText>
  330. <GroupNameBar />
  331. </GroupName>
  332. ) : null}
  333. <GroupFields>
  334. <Column left>
  335. {g.fields.map(f => f.column === 0 && f.component)}
  336. </Column>
  337. <Column right>
  338. {g.fields.map(f => f.column === 1 && f.component)}
  339. </Column>
  340. </GroupFields>
  341. </Group>
  342. ))}
  343. </Fields>
  344. )
  345. }
  346. renderLoading() {
  347. if (!this.props.loading) {
  348. return null
  349. }
  350. return (
  351. <LoadingWrapper>
  352. <StatusImage loading />
  353. <LoadingText>Loading options...</LoadingText>
  354. </LoadingWrapper>
  355. )
  356. }
  357. renderOptions() {
  358. if (this.props.loading) {
  359. return null
  360. }
  361. let onAdvancedOptionsToggle = this.props.onAdvancedOptionsToggle
  362. return (
  363. <Options>
  364. {onAdvancedOptionsToggle ? <ToggleButtonBar
  365. style={{ marginBottom: '46px' }}
  366. items={[{ label: 'Simple', value: 'simple' }, { label: 'Advanced', value: 'advanced' }]}
  367. selectedValue={this.props.useAdvancedOptions ? 'advanced' : 'simple'}
  368. onChange={item => { onAdvancedOptionsToggle(item.value === 'advanced') }}
  369. /> : null}
  370. {this.renderOptionsFields()}
  371. </Options>
  372. )
  373. }
  374. render() {
  375. return (
  376. <Wrapper>
  377. <input type="password" style={{ position: 'absolute', top: '-99999px', left: '-99999px' }} />
  378. {this.renderOptions()}
  379. {this.renderLoading()}
  380. </Wrapper>
  381. )
  382. }
  383. }
  384. export default WizardOptions