MinionPoolModal.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
  1. /*
  2. Copyright (C) 2020 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 { observe } from 'mobx'
  18. import StatusImage from '../../../ui/StatusComponents/StatusImage/StatusImage'
  19. import Button from '../../../ui/Button/Button'
  20. import LoadingButton from '../../../ui/LoadingButton/LoadingButton'
  21. import type { Endpoint as EndpointType } from '../../../../@types/Endpoint'
  22. import { Field, isEnumSeparator } from '../../../../@types/Field'
  23. import ObjectUtils from '../../../../utils/ObjectUtils'
  24. import KeyboardManager from '../../../../utils/KeyboardManager'
  25. import MinionPoolModalContent from './MinionPoolModalContent'
  26. import minionPoolStore from '../../../../stores/MinionPoolStore'
  27. import minionPoolImage from './images/minion-pool.svg'
  28. import notificationStore from '../../../../stores/NotificationStore'
  29. import providerStore, { getFieldChangeOptions } from '../../../../stores/ProviderStore'
  30. import { MinionPool } from '../../../../@types/MinionPool'
  31. import { ThemeProps } from '../../../Theme'
  32. const Wrapper = styled.div<any>`
  33. padding: 24px 0 32px 0;
  34. display: flex;
  35. align-items: center;
  36. flex-direction: column;
  37. min-height: 0;
  38. `
  39. const MinionPoolImageWrapper = styled.div`
  40. ${ThemeProps.exactSize('128px')}
  41. background: url('${minionPoolImage}') center no-repeat;
  42. `
  43. const Content = styled.div<any>`
  44. width: 100%;
  45. display: flex;
  46. flex-direction: column;
  47. min-height: 0;
  48. `
  49. const LoadingWrapper = styled.div<any>`
  50. display: flex;
  51. flex-direction: column;
  52. align-items: center;
  53. margin: 32px 0;
  54. `
  55. const LoadingText = styled.div<any>`
  56. font-size: 18px;
  57. margin-top: 32px;
  58. `
  59. const Buttons = styled.div<any>`
  60. display: flex;
  61. justify-content: space-between;
  62. margin-top: 32px;
  63. flex-shrink: 0;
  64. padding: 0 32px;
  65. `
  66. type Props = {
  67. cancelButtonText: string,
  68. endpoint: EndpointType,
  69. minionPool?: MinionPool | null,
  70. editableData?: any | null
  71. platform: 'source' | 'destination',
  72. onCancelClick: () => void,
  73. onResizeUpdate?: (scrollableRef: HTMLElement, scrollOffset?: number) => void,
  74. onRequestClose: () => void,
  75. onUpdateComplete?: (redirectoTo: string) => void,
  76. }
  77. type State = {
  78. invalidFields: any[],
  79. editableData: any | null
  80. saving: boolean
  81. }
  82. @observer
  83. class MinionPoolModal extends React.Component<Props, State> {
  84. static defaultProps = {
  85. cancelButtonText: 'Cancel',
  86. }
  87. state: State = {
  88. invalidFields: [],
  89. editableData: null,
  90. saving: false,
  91. }
  92. scrollableRef!: HTMLElement
  93. minionPoolStoreObserver!: () => void
  94. UNSAFE_componentWillMount() {
  95. this.UNSAFE_componentWillReceiveProps(this.props)
  96. this.minionPoolStoreObserver = observe(minionPoolStore, () => {
  97. if (this.props.onResizeUpdate) this.props.onResizeUpdate(this.scrollableRef)
  98. })
  99. }
  100. componentDidMount() {
  101. const loadSchema = async () => {
  102. if (!this.props.endpoint) {
  103. return
  104. }
  105. await minionPoolStore.loadMinionPoolSchema(this.props.endpoint.type, this.props.platform)
  106. await providerStore.loadProviders()
  107. const providers = providerStore.providers
  108. if (!providers) {
  109. return
  110. }
  111. await minionPoolStore.loadOptions({
  112. providers,
  113. optionsType: this.props.platform,
  114. endpoint: this.props.endpoint,
  115. envData: this.envData,
  116. useCache: true,
  117. })
  118. this.fillRequiredDefaults()
  119. }
  120. loadSchema()
  121. KeyboardManager.onEnter('minion-pool', () => {
  122. this.create()
  123. }, 2)
  124. }
  125. UNSAFE_componentWillReceiveProps(props: Props) {
  126. if (props.editableData) {
  127. this.setState(prevState => ({
  128. editableData: {
  129. ...ObjectUtils.flatten(props.editableData || {}),
  130. ...prevState.editableData,
  131. },
  132. }))
  133. }
  134. if (props.platform) {
  135. this.setState(prevState => ({
  136. editableData: {
  137. ...prevState.editableData,
  138. platform: props.platform,
  139. },
  140. }))
  141. }
  142. if (props.onResizeUpdate) props.onResizeUpdate(this.scrollableRef)
  143. }
  144. componentWillUnmount() {
  145. KeyboardManager.removeKeyDown('minion-pool')
  146. this.minionPoolStoreObserver()
  147. }
  148. get isLoading() {
  149. return minionPoolStore.loadingMinionPoolSchema
  150. || minionPoolStore.loadingMinionPools
  151. || minionPoolStore.optionsPrimaryLoading
  152. || providerStore.providersLoading
  153. }
  154. get envData() {
  155. let envData: any = null
  156. Object.keys(this.state.editableData).forEach(prop => {
  157. if (!minionPoolStore.minionPoolDefaultSchema.find(f => f.name === prop)) {
  158. envData = envData || {}
  159. envData[prop] = this.state.editableData[prop]
  160. }
  161. })
  162. return envData
  163. }
  164. getFieldValue(field?: Field | null) {
  165. if (!field || !this.state.editableData) {
  166. return ''
  167. }
  168. if (this.state.editableData[field.name] != null) {
  169. return this.state.editableData[field.name]
  170. }
  171. if (Object.keys(field).find(k => k === 'default')) {
  172. return field.default
  173. }
  174. if (field.type === 'integer' || field.type === 'boolean') {
  175. return null
  176. }
  177. return ''
  178. }
  179. findInvalidFields = () => {
  180. const invalidFields = minionPoolStore.minionPoolCombinedSchema.filter(field => {
  181. if (field.required) {
  182. const value = this.getFieldValue(field)
  183. if (value === null || value === '' || value.length === 0) {
  184. return true
  185. }
  186. if (!field.enum) {
  187. return false
  188. }
  189. // When loading new options as a result of destination options calls,
  190. // the value stored in the state may no longer be found in the field's enum.
  191. // Example: When changing the AD of an OCI minion pool,
  192. // although the Subnet ID may show 'Choose Value', the modal would still let you hit 'Update'.
  193. if (!field.enum.find(f => (!isEnumSeparator(f) ? (typeof f === 'string' ? f === value : (f.value === value || f.id === value)) : false))) {
  194. return true
  195. }
  196. }
  197. return false
  198. }).map(f => f.name)
  199. return invalidFields
  200. }
  201. highlightRequired() {
  202. const invalidFields = this.findInvalidFields()
  203. this.setState({ invalidFields })
  204. if (invalidFields.length > 0) {
  205. notificationStore.alert('Please fill the required fields', 'error')
  206. return true
  207. }
  208. return false
  209. }
  210. async create() {
  211. if (this.highlightRequired()) {
  212. return
  213. }
  214. this.setState({ saving: true })
  215. try {
  216. if (this.props.minionPool?.id) {
  217. await this.update()
  218. } else {
  219. await this.add()
  220. }
  221. } catch (err) {
  222. console.error(err)
  223. this.setState({ saving: false })
  224. }
  225. }
  226. async update() {
  227. const stateMinionPool = {
  228. ...this.state.editableData,
  229. id: this.props.minionPool?.id,
  230. }
  231. delete stateMinionPool.platform
  232. delete stateMinionPool.endpoint_id
  233. await minionPoolStore.update(this.props.endpoint.type, stateMinionPool)
  234. if (this.props.onUpdateComplete) {
  235. this.props.onUpdateComplete(`/minion-pools/${this.props.minionPool?.id}`)
  236. }
  237. }
  238. async add() {
  239. await minionPoolStore.add(this.props.endpoint.type, this.props.endpoint.id, this.state.editableData)
  240. notificationStore.alert('Minion Pool created', 'success')
  241. this.props.onRequestClose()
  242. }
  243. fillRequiredDefaults() {
  244. this.setState(prevState => {
  245. const minionPool: any = { ...prevState.editableData }
  246. const requiredFieldsDefaults = minionPoolStore.minionPoolCombinedSchema
  247. .filter(f => f.required && f.default != null)
  248. requiredFieldsDefaults.forEach(f => {
  249. if (minionPool[f.name] == null) {
  250. minionPool[f.name] = f.default
  251. }
  252. })
  253. return { editableData: minionPool }
  254. })
  255. }
  256. async loadExtraOptions(field: Field | null, type: 'source' | 'destination', useCache: boolean = true) {
  257. const envData = getFieldChangeOptions({
  258. providerName: this.props.endpoint.type,
  259. schema: minionPoolStore.minionPoolEnvSchema,
  260. data: this.envData,
  261. field,
  262. type,
  263. })
  264. if (!envData) {
  265. return
  266. }
  267. await minionPoolStore.loadOptions({
  268. providers: providerStore.providers!,
  269. optionsType: type,
  270. endpoint: this.props.endpoint,
  271. envData,
  272. useCache,
  273. })
  274. this.fillRequiredDefaults()
  275. }
  276. handleFieldChange(field: Field, value: any) {
  277. this.setState(prevState => {
  278. const minionPool: any = { ...prevState.editableData }
  279. if (field.type === 'array') {
  280. const arrayItems = minionPool[field.name] || []
  281. value = arrayItems.find((v: any) => v === value)
  282. ? arrayItems.filter((v: any) => v !== value) : [...arrayItems, value]
  283. }
  284. minionPool[field.name] = value
  285. return { editableData: minionPool }
  286. }, () => {
  287. if (field.type !== 'string' || field.enum) {
  288. this.loadExtraOptions(field, this.props.platform, true)
  289. }
  290. })
  291. }
  292. handleCancelClick() {
  293. this.props.onCancelClick()
  294. }
  295. renderButtons() {
  296. let actionButton = (
  297. <Button
  298. large
  299. onClick={() => this.create()}
  300. >Save
  301. </Button>
  302. )
  303. if (this.state.saving) {
  304. actionButton = <LoadingButton large>Saving ...</LoadingButton>
  305. }
  306. return (
  307. <Buttons>
  308. <Button
  309. large
  310. secondary
  311. onClick={() => {
  312. this.handleCancelClick()
  313. }}
  314. >{this.props.cancelButtonText}
  315. </Button>
  316. {actionButton}
  317. </Buttons>
  318. )
  319. }
  320. renderContent() {
  321. return (
  322. <Content>
  323. <MinionPoolModalContent
  324. endpoint={this.props.endpoint}
  325. platform={this.props.platform}
  326. optionsLoading={minionPoolStore.optionsSecondaryLoading}
  327. optionsLoadingSkipFields={minionPoolStore.minionPoolDefaultSchema.map(f => f.name)}
  328. envOptionsDisabled={this.props.minionPool != null && this.props.minionPool.status !== 'DEALLOCATED'}
  329. defaultSchema={minionPoolStore.minionPoolDefaultSchema}
  330. envSchema={minionPoolStore.minionPoolEnvSchema}
  331. invalidFields={this.state.invalidFields}
  332. disabled={this.state.saving}
  333. cancelButtonText={this.props.cancelButtonText}
  334. getFieldValue={field => this.getFieldValue(field)}
  335. onFieldChange={(field, value) => {
  336. if (field) {
  337. this.handleFieldChange(field, value)
  338. }
  339. }}
  340. onCreateClick={() => { this.create() }}
  341. onCancelClick={() => { this.handleCancelClick() }}
  342. scrollableRef={ref => { this.scrollableRef = ref }}
  343. onResizeUpdate={() => {
  344. if (this.props.onResizeUpdate) {
  345. this.props.onResizeUpdate(this.scrollableRef)
  346. }
  347. }}
  348. />
  349. {this.renderButtons()}
  350. </Content>
  351. )
  352. }
  353. renderLoading() {
  354. return (
  355. <LoadingWrapper>
  356. <StatusImage loading />
  357. <LoadingText>Loading Pool Options ...</LoadingText>
  358. </LoadingWrapper>
  359. )
  360. }
  361. render() {
  362. return (
  363. <Wrapper>
  364. <MinionPoolImageWrapper />
  365. {!this.isLoading ? this.renderContent() : null}
  366. {this.isLoading ? this.renderLoading() : null}
  367. </Wrapper>
  368. )
  369. }
  370. }
  371. export default MinionPoolModal