ReplicaMigrationOptions.tsx 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  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 { observer } from 'mobx-react'
  16. import styled from 'styled-components'
  17. import Button from '../../atoms/Button'
  18. import FieldInput from '../../molecules/FieldInput'
  19. import ToggleButtonBar from '../../atoms/ToggleButtonBar'
  20. import WizardScripts from '../WizardScripts'
  21. import LabelDictionary from '../../../utils/LabelDictionary'
  22. import KeyboardManager from '../../../utils/KeyboardManager'
  23. import StyleProps from '../../styleUtils/StyleProps'
  24. import replicaMigrationImage from './images/replica-migration.svg'
  25. import replicaMigrationFields from './replicaMigrationFields'
  26. import type { Field } from '../../../@types/Field'
  27. import type { Instance, InstanceScript } from '../../../@types/Instance'
  28. import { TransferItemDetails } from '../../../@types/MainItem'
  29. import { MinionPool } from '../../../@types/MinionPool'
  30. import { INSTANCE_OSMORPHING_MINION_POOL_MAPPINGS } from '../WizardOptions/WizardOptions'
  31. import { ProviderMigrationCloneDiskDisabledOption } from '../../../@types/Config'
  32. const Wrapper = styled.div<any>`
  33. display: flex;
  34. flex-direction: column;
  35. align-items: center;
  36. padding: 0 32px 32px 32px;
  37. min-height: 0;
  38. `
  39. const Image = styled.div<any>`
  40. ${StyleProps.exactWidth('288px')}
  41. ${StyleProps.exactHeight('96px')}
  42. background: url('${replicaMigrationImage}') center no-repeat;
  43. margin: 80px 0;
  44. `
  45. const OptionsBody = styled.div<any>`
  46. display: flex;
  47. flex-direction: column;
  48. `
  49. const ScriptsBody = styled.div<any>`
  50. display: flex;
  51. flex-direction: column;
  52. width: 100%;
  53. overflow: auto;
  54. min-height: 0;
  55. margin-bottom: 32px;
  56. `
  57. const Form = styled.div<any>`
  58. display: flex;
  59. flex-wrap: wrap;
  60. margin-left: -64px;
  61. justify-content: space-between;
  62. margin: 0 auto;
  63. `
  64. const Buttons = styled.div<any>`
  65. display: flex;
  66. justify-content: space-between;
  67. width: 100%;
  68. `
  69. const FieldInputStyled = styled(FieldInput)`
  70. width: 224px;
  71. justify-content: space-between;
  72. margin-bottom: 32px;
  73. `
  74. type Props = {
  75. instances: Instance[],
  76. transferItem: TransferItemDetails | null,
  77. minionPools: MinionPool[]
  78. loadingInstances: boolean,
  79. defaultSkipOsMorphing?: boolean | null,
  80. disabledCloneDisk?: ProviderMigrationCloneDiskDisabledOption | null,
  81. onCancelClick: () => void,
  82. onMigrateClick: (
  83. fields: Field[],
  84. uploadedScripts: InstanceScript[],
  85. removedScripts: InstanceScript[],
  86. minionPoolMappings: { [instance: string]: string }
  87. ) => void,
  88. onResizeUpdate?: (scrollableRef: HTMLElement, scrollOffset?: number) => void,
  89. }
  90. type State = {
  91. fields: Field[],
  92. selectedBarButton: string,
  93. uploadedScripts: InstanceScript[],
  94. removedScripts: InstanceScript[],
  95. minionPoolMappings: {[instance: string]: string}
  96. }
  97. @observer
  98. class ReplicaMigrationOptions extends React.Component<Props, State> {
  99. state: State = {
  100. fields: [],
  101. selectedBarButton: 'options',
  102. uploadedScripts: [],
  103. removedScripts: [],
  104. minionPoolMappings: {},
  105. }
  106. scrollableRef!: HTMLElement
  107. UNSAFE_componentWillMount() {
  108. const mappings = this.props.transferItem?.instance_osmorphing_minion_pool_mappings || {}
  109. this.setState({
  110. fields: replicaMigrationFields.map(f => {
  111. if (f.name === 'skip_os_morphing') {
  112. return { ...f, value: this.props.defaultSkipOsMorphing || null }
  113. }
  114. if (f.name === 'clone_disks' && this.props.disabledCloneDisk) {
  115. return {
  116. ...f,
  117. disabled: true,
  118. value: this.props.disabledCloneDisk.defaultValue,
  119. description: this.props.disabledCloneDisk.description,
  120. }
  121. }
  122. return f
  123. }),
  124. minionPoolMappings: { ...mappings },
  125. })
  126. }
  127. componentDidMount() {
  128. KeyboardManager.onEnter('migration-options', () => { this.migrate() }, 2)
  129. }
  130. componentDidUpdate(_: Props, prevState: State) {
  131. if (prevState.selectedBarButton !== this.state.selectedBarButton) {
  132. if (this.props.onResizeUpdate) {
  133. this.props.onResizeUpdate(this.scrollableRef, 0)
  134. }
  135. }
  136. }
  137. componentWillUnmount() {
  138. KeyboardManager.removeKeyDown('migration-options')
  139. }
  140. migrate() {
  141. this.props.onMigrateClick(
  142. this.state.fields,
  143. this.state.uploadedScripts,
  144. this.state.removedScripts,
  145. this.state.minionPoolMappings,
  146. )
  147. }
  148. handleValueChange(field: Field, value: boolean) {
  149. this.setState(prevState => {
  150. const fields = prevState.fields.map(f => {
  151. const newField = { ...f }
  152. if (f.name === field.name) {
  153. newField.value = value
  154. }
  155. return newField
  156. })
  157. return { fields }
  158. })
  159. }
  160. handleCanceScript(global: string | null, instanceName: string | null) {
  161. this.setState(prevState => ({
  162. uploadedScripts: prevState.uploadedScripts
  163. .filter(s => (global ? s.global !== global : s.instanceId !== instanceName)),
  164. }))
  165. }
  166. handleScriptUpload(script: InstanceScript) {
  167. this.setState(prevState => ({
  168. uploadedScripts: [
  169. ...prevState.uploadedScripts,
  170. script,
  171. ],
  172. }))
  173. }
  174. handleScriptRemove(script: InstanceScript) {
  175. this.setState(prevState => ({
  176. removedScripts: [
  177. ...prevState.removedScripts,
  178. script,
  179. ],
  180. }))
  181. }
  182. renderField(field: Field) {
  183. return (
  184. <FieldInputStyled
  185. width={224}
  186. key={field.name}
  187. name={field.name}
  188. type={field.type}
  189. value={field.value || field.default}
  190. minimum={field.minimum}
  191. maximum={field.maximum}
  192. layout="page"
  193. label={field.label || LabelDictionary.get(field.name)}
  194. onChange={value => this.handleValueChange(field, value)}
  195. description={field.description || LabelDictionary.getDescription(field.name)}
  196. disabled={field.disabled}
  197. />
  198. )
  199. }
  200. renderMinionPoolMappings() {
  201. const minionPools = this.props.minionPools
  202. .filter(m => m.endpoint_id === this.props.transferItem?.destination_endpoint_id)
  203. if (!minionPools.length) {
  204. return null
  205. }
  206. const properties: Field[] = this.props.instances.map(instance => ({
  207. name: instance.instance_name || instance.id,
  208. label: instance.name,
  209. type: 'string',
  210. enum: minionPools.map(minionPool => ({
  211. name: minionPool.name,
  212. id: minionPool.id,
  213. })),
  214. }))
  215. return (
  216. <FieldInputStyled
  217. width={500}
  218. style={{ marginBottom: '64px' }}
  219. name={INSTANCE_OSMORPHING_MINION_POOL_MAPPINGS}
  220. type="object"
  221. valueCallback={field => this.state.minionPoolMappings
  222. && this.state.minionPoolMappings[field.name]}
  223. layout="page"
  224. label="Instance OSMorphing Minion Pool Mappings"
  225. onChange={(value, field) => this.setState(prevState => {
  226. const minionPoolMappings = { ...prevState.minionPoolMappings }
  227. minionPoolMappings[field!.name] = value
  228. return { minionPoolMappings }
  229. })}
  230. properties={properties}
  231. labelRenderer={(propName: string) => (
  232. propName.indexOf('/') > -1 ? propName.split('/')[propName.split('/').length - 1] : propName
  233. )}
  234. />
  235. )
  236. }
  237. renderOptions() {
  238. return (
  239. <>
  240. <Form>
  241. {this.state.fields.map(field => this.renderField(field))}
  242. </Form>
  243. {this.renderMinionPoolMappings()}
  244. </>
  245. )
  246. }
  247. renderScripts() {
  248. return (
  249. <WizardScripts
  250. instances={this.props.instances}
  251. loadingInstances={this.props.loadingInstances}
  252. onScriptUpload={s => { this.handleScriptUpload(s) }}
  253. onScriptDataRemove={s => { this.handleScriptRemove(s) }}
  254. onCancelScript={(g, i) => { this.handleCanceScript(g, i) }}
  255. uploadedScripts={this.state.uploadedScripts}
  256. removedScripts={this.state.removedScripts}
  257. userScriptData={this.props.transferItem?.user_scripts}
  258. scrollableRef={(r: HTMLElement) => { this.scrollableRef = r }}
  259. layout="modal"
  260. />
  261. )
  262. }
  263. renderBody() {
  264. const Body = this.state.selectedBarButton === 'options' ? OptionsBody : ScriptsBody
  265. return (
  266. <Body>
  267. <ToggleButtonBar
  268. items={[{ label: 'Options', value: 'options' }, { label: 'User Scripts', value: 'script' }]}
  269. selectedValue={this.state.selectedBarButton}
  270. onChange={item => { this.setState({ selectedBarButton: item.value }) }}
  271. style={{ marginBottom: '32px' }}
  272. />
  273. {this.state.selectedBarButton === 'options' ? this.renderOptions() : this.renderScripts()}
  274. </Body>
  275. )
  276. }
  277. render() {
  278. return (
  279. <Wrapper>
  280. <Image />
  281. {this.renderBody()}
  282. <Buttons>
  283. <Button secondary onClick={this.props.onCancelClick} data-test-id="rmOptions-cancelButton">Cancel</Button>
  284. <Button onClick={() => { this.migrate() }} data-test-id="rmOptions-execButton">Migrate</Button>
  285. </Buttons>
  286. </Wrapper>
  287. )
  288. }
  289. }
  290. export default ReplicaMigrationOptions